Compare commits

..

10 Commits

Author SHA1 Message Date
Jarvis
2abcfee211 chore(mosaic): bump version to 0.0.17 (avoid collision with other agent)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/manual/ci Pipeline failed
2026-04-04 19:52:04 -05:00
Jarvis
799f9e76bf chore(mosaic): bump version to 0.0.16
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-04-04 19:51:30 -05:00
Jarvis
49abf3bb4c fix(mosaic): add @mosaic/cli dependency + postinstall hint
Some checks failed
ci/woodpecker/push/ci Pipeline failed
@mosaic/mosaic is a metapackage but was missing @mosaic/cli as a dep,
so npm install -g @mosaic/mosaic only gave you mosaic-wizard, not mosaic.

Changes:
- Add @mosaic/cli as dependency (brings in mosaic binary)
- Add postinstall script with guidance message
2026-04-04 17:19:31 -05:00
c0d0fd44b7 refactor(storage): replace better-sqlite3 with PGlite adapter (#378)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 21:58:14 +00:00
30c0fb1308 fix: remediate npm deprecation warnings in @mosaic/gateway 0.0.3 (#377)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 21:03:54 +00:00
26fac4722f fix: gateway install preserves npm prefix via registry flag (#376)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 20:36:15 +00:00
e3f64c79d9 chore: move gateway default port from 4000 to 14242 (#375)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 20:17:40 +00:00
cbd5e8c626 fix: scope Gitea registry to @mosaic packages only (#374)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 19:09:14 +00:00
7560c7dee7 fix: gateway install uses Gitea registry instead of npmjs (#373)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-04 18:59:40 +00:00
982a0e8f83 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.11 (#372)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 18:47:03 +00:00
37 changed files with 1205 additions and 1288 deletions

View File

@@ -23,8 +23,8 @@ VALKEY_URL=redis://localhost:6380
# ─── Gateway ─────────────────────────────────────────────────────────────────
# TCP port the NestJS/Fastify gateway listens on (default: 4000)
GATEWAY_PORT=4000
# TCP port the NestJS/Fastify gateway listens on (default: 14242)
GATEWAY_PORT=14242
# Comma-separated list of allowed CORS origins.
# Must include the web app origin in production.
@@ -37,12 +37,12 @@ GATEWAY_CORS_ORIGIN=http://localhost:3000
BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
# Public base URL of the gateway (used by BetterAuth for callback URLs)
BETTER_AUTH_URL=http://localhost:4000
BETTER_AUTH_URL=http://localhost:14242
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
# Public gateway URL — accessible from the browser, not just the server.
NEXT_PUBLIC_GATEWAY_URL=http://localhost:4000
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
@@ -121,12 +121,12 @@ OTEL_SERVICE_NAME=mosaic-gateway
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
# DISCORD_BOT_TOKEN=
# DISCORD_GUILD_ID=
# DISCORD_GATEWAY_URL=http://localhost:4000
# DISCORD_GATEWAY_URL=http://localhost:14242
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
# TELEGRAM_BOT_TOKEN=
# TELEGRAM_GATEWAY_URL=http://localhost:4000
# TELEGRAM_GATEWAY_URL=http://localhost:14242
# ─── SSO Providers (add credentials to enable) ───────────────────────────────

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/gateway",
"version": "0.0.2",
"version": "0.1.0",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
@@ -28,8 +28,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.80.0",
"@fastify/helmet": "^13.0.2",
"@mariozechner/pi-ai": "~0.57.1",
"@mariozechner/pi-coding-agent": "~0.57.1",
"@mariozechner/pi-ai": "^0.65.0",
"@mariozechner/pi-coding-agent": "^0.65.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"@mosaic/auth": "workspace:^",
"@mosaic/brain": "workspace:^",
@@ -49,7 +49,7 @@
"@nestjs/platform-socket.io": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.0.0",
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
"@opentelemetry/auto-instrumentations-node": "^0.72.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
"@opentelemetry/resources": "^2.6.0",

View File

@@ -62,7 +62,7 @@ function restoreEnv(saved: Map<EnvKey, string | undefined>): void {
}
function makeRegistry(): ModelRegistry {
return new ModelRegistry(AuthStorage.inMemory());
return ModelRegistry.inMemory(AuthStorage.inMemory());
}
// ---------------------------------------------------------------------------

View File

@@ -67,7 +67,7 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
async onModuleInit(): Promise<void> {
const authStorage = AuthStorage.inMemory();
this.registry = new ModelRegistry(authStorage);
this.registry = ModelRegistry.inMemory(authStorage);
// Build the default set of adapters that rely on the registry
this.adapters = [

View File

@@ -14,7 +14,7 @@ import { SsoController } from './sso.controller.js';
useFactory: (db: Db): Auth =>
createAuth({
db,
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:4000',
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242',
secret: process.env['BETTER_AUTH_SECRET'],
}),
inject: [DB],

View File

@@ -59,7 +59,7 @@ async function bootstrap(): Promise<void> {
mountAuthHandler(app);
mountMcpHandler(app, app.get(McpService));
const port = Number(process.env['GATEWAY_PORT'] ?? 4000);
const port = Number(process.env['GATEWAY_PORT'] ?? 14242);
await app.listen(port, '0.0.0.0');
logger.log(`Gateway listening on port ${port}`);
}

View File

@@ -48,7 +48,7 @@ class TelegramChannelPluginAdapter implements IChannelPlugin {
}
}
const DEFAULT_GATEWAY_URL = 'http://localhost:4000';
const DEFAULT_GATEWAY_URL = 'http://localhost:14242';
function createPluginRegistry(): IChannelPlugin[] {
const plugins: IChannelPlugin[] = [];

View File

@@ -5,7 +5,7 @@ import { defineConfig, devices } from '@playwright/test';
*
* Assumes:
* - Next.js web app running on http://localhost:3000
* - NestJS gateway running on http://localhost:4000
* - NestJS gateway running on http://localhost:14242
*
* Run with: pnpm --filter @mosaic/web test:e2e
*/

View File

@@ -1,4 +1,4 @@
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
export interface ApiRequestInit extends Omit<RequestInit, 'body'> {
body?: unknown;

View File

@@ -2,7 +2,7 @@ import { createAuthClient } from 'better-auth/react';
import { adminClient, genericOAuthClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000',
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242',
plugins: [adminClient(), genericOAuthClient()],
});

View File

@@ -1,6 +1,6 @@
import { io, type Socket } from 'socket.io-client';
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
let socket: Socket | null = null;

View File

@@ -93,7 +93,7 @@ packages/cli/src/tui/
cd /home/jwoltje/src/mosaic-mono-v1-worktrees/tui-improvements
pnpm --filter @mosaic/cli exec tsx src/cli.ts tui
# or after build:
node packages/cli/dist/cli.js tui --gateway http://localhost:4000
node packages/cli/dist/cli.js tui --gateway http://localhost:14242
```
### Quality Gates

View File

@@ -229,11 +229,11 @@ external clients. Authentication requires a valid BetterAuth session (cookie or
### Gateway
| Variable | Default | Description |
| --------------------- | ----------------------- | ---------------------------------------------- |
| `GATEWAY_PORT` | `4000` | Port the gateway listens on |
| `GATEWAY_CORS_ORIGIN` | `http://localhost:3000` | Allowed CORS origin for browser clients |
| `BETTER_AUTH_URL` | `http://localhost:4000` | Public URL of the gateway (used by BetterAuth) |
| Variable | Default | Description |
| --------------------- | ------------------------ | ---------------------------------------------- |
| `GATEWAY_PORT` | `14242` | Port the gateway listens on |
| `GATEWAY_CORS_ORIGIN` | `http://localhost:3000` | Allowed CORS origin for browser clients |
| `BETTER_AUTH_URL` | `http://localhost:14242` | Public URL of the gateway (used by BetterAuth) |
### SSO (Optional)
@@ -292,13 +292,13 @@ Each OIDC provider requires its client ID, client secret, and issuer URL togethe
### Plugins
| Variable | Description |
| ---------------------- | ------------------------------------------------------------------------- |
| `DISCORD_BOT_TOKEN` | Discord bot token (enables Discord plugin) |
| `DISCORD_GUILD_ID` | Discord guild/server ID |
| `DISCORD_GATEWAY_URL` | Gateway URL for Discord plugin to call (default: `http://localhost:4000`) |
| `TELEGRAM_BOT_TOKEN` | Telegram bot token (enables Telegram plugin) |
| `TELEGRAM_GATEWAY_URL` | Gateway URL for Telegram plugin to call |
| Variable | Description |
| ---------------------- | -------------------------------------------------------------------------- |
| `DISCORD_BOT_TOKEN` | Discord bot token (enables Discord plugin) |
| `DISCORD_GUILD_ID` | Discord guild/server ID |
| `DISCORD_GATEWAY_URL` | Gateway URL for Discord plugin to call (default: `http://localhost:14242`) |
| `TELEGRAM_BOT_TOKEN` | Telegram bot token (enables Telegram plugin) |
| `TELEGRAM_GATEWAY_URL` | Gateway URL for Telegram plugin to call |
### Observability
@@ -309,9 +309,9 @@ Each OIDC provider requires its client ID, client secret, and issuer URL togethe
### Web App
| Variable | Default | Description |
| ------------------------- | ----------------------- | -------------------------------------- |
| `NEXT_PUBLIC_GATEWAY_URL` | `http://localhost:4000` | Gateway URL used by the Next.js client |
| Variable | Default | Description |
| ------------------------- | ------------------------ | -------------------------------------- |
| `NEXT_PUBLIC_GATEWAY_URL` | `http://localhost:14242` | Gateway URL used by the Next.js client |
### Coordination

View File

@@ -194,7 +194,7 @@ server {
# WebSocket support (for chat.gateway.ts / Socket.IO)
location /socket.io/ {
proxy_pass http://127.0.0.1:4000;
proxy_pass http://127.0.0.1:14242;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -204,7 +204,7 @@ server {
# REST + auth
location / {
proxy_pass http://127.0.0.1:4000;
proxy_pass http://127.0.0.1:14242;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -234,11 +234,11 @@ server {
# /etc/caddy/Caddyfile
your-domain.example.com {
reverse_proxy /socket.io/* localhost:4000 {
reverse_proxy /socket.io/* localhost:14242 {
header_up Upgrade {http.upgrade}
header_up Connection {http.connection}
}
reverse_proxy localhost:4000
reverse_proxy localhost:14242
}
app.your-domain.example.com {
@@ -328,7 +328,7 @@ MaxRetentionSec=30day
- Set `BETTER_AUTH_SECRET` to a cryptographically random value (`openssl rand -base64 32`).
- Restrict `GATEWAY_CORS_ORIGIN` to your exact frontend origin — do not use `*`.
- Run services as a dedicated non-root system user (e.g., `mosaic`).
- Firewall: only expose ports 80/443 externally; keep 4000 and 3000 bound to `127.0.0.1`.
- Firewall: only expose ports 80/443 externally; keep 14242 and 3000 bound to `127.0.0.1`.
- Set `AGENT_FILE_SANDBOX_DIR` to a directory outside the application root to prevent agent tools from accessing source code.
- If using `AGENT_USER_TOOLS`, enumerate only the tools non-admin users need.

View File

@@ -112,11 +112,11 @@ DATABASE_URL=postgresql://mosaic:mosaic@localhost:5433/mosaic
BETTER_AUTH_SECRET=change-me-to-a-random-secret
# Gateway
GATEWAY_PORT=4000
GATEWAY_PORT=14242
GATEWAY_CORS_ORIGIN=http://localhost:3000
# Web
NEXT_PUBLIC_GATEWAY_URL=http://localhost:4000
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
# Optional: Ollama
OLLAMA_BASE_URL=http://localhost:11434
@@ -141,7 +141,7 @@ migrations in production).
pnpm --filter @mosaic/gateway exec tsx src/main.ts
```
The gateway starts on port `4000` by default.
The gateway starts on port `14242` by default.
### 6. Start the Web App
@@ -395,7 +395,7 @@ directory are defined there.
## API Endpoint Reference
All endpoints are served by the gateway at `http://localhost:4000` by default.
All endpoints are served by the gateway at `http://localhost:14242` by default.
### Authentication

View File

@@ -16,7 +16,7 @@
### Prerequisites
Mosaic Stack requires a running gateway. Your administrator provides the URL
(default: `http://localhost:4000`) and creates your account.
(default: `http://localhost:14242`) and creates your account.
### Logging In (Web)
@@ -177,7 +177,7 @@ mosaic --help
### Signing In
```bash
mosaic login --gateway http://localhost:4000 --email you@example.com
mosaic login --gateway http://localhost:14242 --email you@example.com
```
You are prompted for a password if `--password` is not supplied. The session
@@ -191,12 +191,12 @@ mosaic tui
Options:
| Flag | Default | Description |
| ----------------------- | ----------------------- | ---------------------------------- |
| `--gateway <url>` | `http://localhost:4000` | Gateway URL |
| `--conversation <id>` | — | Resume a specific conversation |
| `--model <modelId>` | server default | Model to use (e.g. `llama3.2`) |
| `--provider <provider>` | server default | Provider (e.g. `ollama`, `openai`) |
| Flag | Default | Description |
| ----------------------- | ------------------------ | ---------------------------------- |
| `--gateway <url>` | `http://localhost:14242` | Gateway URL |
| `--conversation <id>` | — | Resume a specific conversation |
| `--model <modelId>` | server default | Model to use (e.g. `llama3.2`) |
| `--provider <provider>` | server default | Provider (e.g. `ollama`, `openai`) |
If no valid session exists you are prompted to sign in before the TUI launches.

View File

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

View File

@@ -23,10 +23,5 @@
"turbo": "^2.0.0",
"typescript": "^5.8.0",
"vitest": "^2.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3"
]
}
}

View File

@@ -35,7 +35,7 @@ export function createAuth(config: AuthConfig) {
provider: 'pg',
usePlural: true,
}),
baseURL: baseURL ?? process.env['BETTER_AUTH_URL'] ?? 'http://localhost:4000',
baseURL: baseURL ?? process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242',
secret: secret ?? process.env['BETTER_AUTH_SECRET'],
basePath: '/api/auth',
trustedOrigins,

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/cli",
"version": "0.0.11",
"version": "0.0.15",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",

View File

@@ -33,7 +33,7 @@ registerLaunchCommands(program);
program
.command('login')
.description('Sign in to a Mosaic gateway')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.option('-e, --email <email>', 'Email address')
.option('-p, --password <password>', 'Password')
.action(async (opts: { gateway: string; email?: string; password?: string }) => {
@@ -67,7 +67,7 @@ program
program
.command('tui')
.description('Launch interactive TUI connected to the gateway')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.option('-c, --conversation <id>', 'Resume a conversation by ID')
.option('-m, --model <modelId>', 'Model ID to use (e.g. gpt-4o, llama3.2)')
.option('-p, --provider <provider>', 'Provider to use (e.g. openai, ollama)')
@@ -208,7 +208,7 @@ const sessionsCmd = program.command('sessions').description('Manage active agent
sessionsCmd
.command('list')
.description('List active agent sessions')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.action(async (opts: { gateway: string }) => {
const { withAuth } = await import('./commands/with-auth.js');
const auth = await withAuth(opts.gateway);
@@ -243,7 +243,7 @@ sessionsCmd
sessionsCmd
.command('resume <id>')
.description('Resume an existing agent session in the TUI')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.action(async (id: string, opts: { gateway: string }) => {
const { loadSession, validateSession } = await import('./auth.js');
@@ -276,7 +276,7 @@ sessionsCmd
sessionsCmd
.command('destroy <id>')
.description('Terminate an active agent session')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.action(async (id: string, opts: { gateway: string }) => {
const { withAuth } = await import('./commands/with-auth.js');
const auth = await withAuth(opts.gateway);

View File

@@ -34,7 +34,7 @@ export function registerAgentCommand(program: Command) {
const cmd = program
.command('agent')
.description('Manage agent configurations')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.option('--list', 'List all agents')
.option('--new', 'Create a new agent')
.option('--show <idOrName>', 'Show agent details')

View File

@@ -17,7 +17,7 @@ function resolveOpts(raw: GatewayParentOpts): { host: string; port: number; toke
const meta = readMeta();
return {
host: raw.host ?? meta?.host ?? 'localhost',
port: parseInt(raw.port, 10) || meta?.port || 4000,
port: parseInt(raw.port, 10) || meta?.port || 14242,
token: raw.token ?? meta?.adminToken,
};
}
@@ -28,7 +28,7 @@ export function registerGatewayCommand(program: Command): void {
.description('Manage the Mosaic gateway daemon')
.helpOption('--help', 'Display help')
.option('-h, --host <host>', 'Gateway host', 'localhost')
.option('-p, --port <port>', 'Gateway port', '4000')
.option('-p, --port <port>', 'Gateway port', '14242')
.option('-t, --token <token>', 'Admin API token')
.action(() => {
gw.outputHelp();

View File

@@ -91,24 +91,14 @@ export function resolveGatewayEntry(): string {
return meta.entryPoint;
}
// Try to resolve from globally installed @mosaicstack/gateway
try {
const req = createRequire(import.meta.url);
const pkgPath = req.resolve('@mosaicstack/gateway/package.json');
const mainEntry = join(resolve(pkgPath, '..'), 'dist', 'main.js');
if (existsSync(mainEntry)) return mainEntry;
} catch {
// Not installed globally via @mosaicstack
}
// Try @mosaic/gateway (workspace / dev)
// Try to resolve from globally installed @mosaic/gateway
try {
const req = createRequire(import.meta.url);
const pkgPath = req.resolve('@mosaic/gateway/package.json');
const mainEntry = join(resolve(pkgPath, '..'), 'dist', 'main.js');
if (existsSync(mainEntry)) return mainEntry;
} catch {
// Not available
// Not installed globally
}
throw new Error('Cannot find gateway entry point. Run `mosaic gateway install` first.');
@@ -217,9 +207,11 @@ function sleep(ms: number): Promise<void> {
// ─── npm install helper ─────────────────────────────────────────────────────
const GITEA_REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
export function installGatewayPackage(): void {
console.log('Installing @mosaicstack/gateway...');
execSync('npm install -g @mosaicstack/gateway@latest', {
console.log('Installing @mosaic/gateway from Gitea registry...');
execSync(`npm install -g @mosaic/gateway@latest --@mosaic:registry=${GITEA_REGISTRY}`, {
stdio: 'inherit',
timeout: 120_000,
});
@@ -227,7 +219,7 @@ export function installGatewayPackage(): void {
export function uninstallGatewayPackage(): void {
try {
execSync('npm uninstall -g @mosaicstack/gateway', {
execSync('npm uninstall -g @mosaic/gateway', {
stdio: 'inherit',
timeout: 60_000,
});
@@ -238,15 +230,15 @@ export function uninstallGatewayPackage(): void {
export function getInstalledGatewayVersion(): string | null {
try {
const output = execSync('npm ls -g @mosaicstack/gateway --json --depth=0', {
const output = execSync('npm ls -g @mosaic/gateway --json --depth=0', {
encoding: 'utf-8',
timeout: 15_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
const data = JSON.parse(output) as {
dependencies?: { '@mosaicstack/gateway'?: { version?: string } };
dependencies?: { '@mosaic/gateway'?: { version?: string } };
};
return data.dependencies?.['@mosaicstack/gateway']?.version ?? null;
return data.dependencies?.['@mosaic/gateway']?.version ?? null;
} catch {
return null;
}

View File

@@ -67,7 +67,7 @@ async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOp
const tier = tierAnswer === '2' ? 'team' : 'local';
const port =
opts.port !== 4000
opts.port !== 14242
? opts.port
: parseInt(
(await prompt(rl, `Gateway port [${opts.port.toString()}]: `)) || opts.port.toString(),
@@ -121,7 +121,7 @@ async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOp
tier === 'local'
? {
tier: 'local',
storage: { type: 'sqlite', path: join(GATEWAY_HOME, 'data.db') },
storage: { type: 'pglite', dataDir: join(GATEWAY_HOME, 'storage-pglite') },
queue: { type: 'local', dataDir: join(GATEWAY_HOME, 'queue') },
memory: { type: 'keyword' },
}
@@ -142,7 +142,7 @@ async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOp
entryPoint = resolveGatewayEntry();
} catch {
console.error('Error: Gateway package not found after install.');
console.error('Check that @mosaicstack/gateway installed correctly.');
console.error('Check that @mosaic/gateway installed correctly.');
return;
}

View File

@@ -40,7 +40,7 @@ export function registerMissionCommand(program: Command) {
const cmd = program
.command('mission')
.description('Manage missions')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.option('--list', 'List all missions')
.option('--init', 'Create a new mission')
.option('--plan <idOrName>', 'Run PRD wizard for a mission')
@@ -86,7 +86,7 @@ export function registerMissionCommand(program: Command) {
cmd
.command('task')
.description('Manage mission tasks')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.option('--list', 'List tasks for a mission')
.option('--new', 'Create a task')
.option('--update <taskId>', 'Update a task')

View File

@@ -6,7 +6,7 @@ export function registerPrdyCommand(program: Command) {
const cmd = program
.command('prdy')
.description('PRD wizard — create and manage Product Requirement Documents')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:4000')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.option('--init [name]', 'Create a new PRD')
.option('--update [name]', 'Update an existing PRD')
.option('--project <idOrName>', 'Scope to project')

View File

@@ -27,6 +27,7 @@
"@mosaic/storage": "workspace:^"
},
"devDependencies": {
"eslint": "^9.0.0",
"typescript": "^5.8.0",
"vitest": "^2.0.0"
},

View File

@@ -26,7 +26,7 @@ export interface MosaicConfig {
export const DEFAULT_LOCAL_CONFIG: MosaicConfig = {
tier: 'local',
storage: { type: 'sqlite', path: '.mosaic/data.db' },
storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' },
queue: { type: 'local', dataDir: '.mosaic/queue' },
memory: { type: 'keyword' },
};
@@ -43,7 +43,7 @@ export const DEFAULT_TEAM_CONFIG: MosaicConfig = {
/* ------------------------------------------------------------------ */
const VALID_TIERS = new Set<string>(['local', 'team']);
const VALID_STORAGE_TYPES = new Set<string>(['postgres', 'sqlite', 'files']);
const VALID_STORAGE_TYPES = new Set<string>(['postgres', 'pglite', 'files']);
const VALID_QUEUE_TYPES = new Set<string>(['bullmq', 'local']);
const VALID_MEMORY_TYPES = new Set<string>(['pgvector', 'sqlite-vec', 'keyword']);

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/mosaic",
"version": "0.0.11",
"version": "0.0.17",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
@@ -20,12 +20,14 @@
}
},
"scripts": {
"postinstall": "echo '\nMosaic Stack installed. Run `mosaic-wizard` to bootstrap your config, or `mosaic --help` for CLI commands.\n'",
"build": "tsc",
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@mosaic/cli": "workspace:*",
"@mosaic/forge": "workspace:*",
"@mosaic/macp": "workspace:*",
"@mosaic/prdy": "workspace:*",

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/storage",
"version": "0.0.2",
"version": "0.0.3",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaic/mosaic-stack.git",
@@ -21,12 +21,11 @@
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@electric-sql/pglite": "^0.2.17",
"@mosaic/db": "workspace:^",
"@mosaic/types": "workspace:*",
"better-sqlite3": "^12.8.0"
"@mosaic/types": "workspace:*"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"typescript": "^5.8.0",
"vitest": "^2.0.0"
},

View File

@@ -1,11 +1,12 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { SqliteAdapter } from './sqlite.js';
import { PgliteAdapter } from './pglite.js';
describe('SqliteAdapter', () => {
let adapter: SqliteAdapter;
describe('PgliteAdapter', () => {
let adapter: PgliteAdapter;
beforeEach(async () => {
adapter = new SqliteAdapter({ type: 'sqlite', path: ':memory:' });
// In-memory PGlite instance — no dataDir = memory mode
adapter = new PgliteAdapter({ type: 'pglite' });
await adapter.migrate();
});
@@ -80,7 +81,7 @@ describe('SqliteAdapter', () => {
it('supports limit and offset', async () => {
for (let i = 0; i < 5; i++) {
await adapter.create('users', { name: `User${i}`, idx: i });
await adapter.create('users', { name: `User${i.toString()}`, idx: i });
}
const page = await adapter.find('users', undefined, {

View File

@@ -0,0 +1,290 @@
import { PGlite } from '@electric-sql/pglite';
import { randomUUID } from 'node:crypto';
import type { StorageAdapter, StorageConfig } from '../types.js';
/* eslint-disable @typescript-eslint/no-explicit-any */
const COLLECTIONS = [
'users',
'sessions',
'accounts',
'projects',
'missions',
'tasks',
'agents',
'conversations',
'messages',
'preferences',
'insights',
'skills',
'events',
'routing_rules',
'provider_credentials',
'agent_logs',
'teams',
'team_members',
'mission_tasks',
'tickets',
'summarization_jobs',
'appreciations',
'verifications',
] as const;
function buildFilterClause(filter?: Record<string, unknown>): {
clause: string;
params: unknown[];
} {
if (!filter || Object.keys(filter).length === 0) return { clause: '', params: [] };
const conditions: string[] = [];
const params: unknown[] = [];
let paramIdx = 1;
for (const [key, value] of Object.entries(filter)) {
if (key === 'id') {
conditions.push(`id = $${paramIdx.toString()}`);
params.push(value);
paramIdx++;
} else {
conditions.push(`data->>'${key}' = $${paramIdx.toString()}`);
params.push(typeof value === 'object' ? JSON.stringify(value) : value);
paramIdx++;
}
}
return { clause: ` WHERE ${conditions.join(' AND ')}`, params };
}
type PgClient = PGlite | { query: PGlite['query'] };
async function pgCreate<T extends Record<string, unknown>>(
pg: PgClient,
collection: string,
data: T,
): Promise<T & { id: string }> {
const id = (data as any).id ?? randomUUID();
const rest = Object.fromEntries(Object.entries(data).filter(([k]) => k !== 'id'));
await pg.query(`INSERT INTO ${collection} (id, data) VALUES ($1, $2::jsonb)`, [
id,
JSON.stringify(rest),
]);
return { ...data, id } as T & { id: string };
}
async function pgRead<T extends Record<string, unknown>>(
pg: PgClient,
collection: string,
id: string,
): Promise<T | null> {
const result = await pg.query<{ id: string; data: Record<string, unknown> }>(
`SELECT id, data FROM ${collection} WHERE id = $1`,
[id],
);
const row = result.rows[0];
if (!row) return null;
return { id: row.id, ...(row.data as object) } as unknown as T;
}
async function pgUpdate(
pg: PgClient,
collection: string,
id: string,
data: Record<string, unknown>,
): Promise<boolean> {
const existing = await pg.query<{ data: Record<string, unknown> }>(
`SELECT data FROM ${collection} WHERE id = $1`,
[id],
);
const row = existing.rows[0];
if (!row) return false;
const merged = { ...(row.data as object), ...data };
const result = await pg.query(
`UPDATE ${collection} SET data = $1::jsonb, updated_at = now() WHERE id = $2`,
[JSON.stringify(merged), id],
);
return (result.affectedRows ?? 0) > 0;
}
async function pgDelete(pg: PgClient, collection: string, id: string): Promise<boolean> {
const result = await pg.query(`DELETE FROM ${collection} WHERE id = $1`, [id]);
return (result.affectedRows ?? 0) > 0;
}
async function pgFind<T extends Record<string, unknown>>(
pg: PgClient,
collection: string,
filter?: Record<string, unknown>,
opts?: { limit?: number; offset?: number; orderBy?: string; order?: 'asc' | 'desc' },
): Promise<T[]> {
const { clause, params } = buildFilterClause(filter);
let paramIdx = params.length + 1;
let query = `SELECT id, data FROM ${collection}${clause}`;
if (opts?.orderBy) {
const dir = opts.order === 'desc' ? 'DESC' : 'ASC';
const col =
opts.orderBy === 'id'
? 'id'
: opts.orderBy === 'created_at' || opts.orderBy === 'updated_at'
? opts.orderBy
: `data->>'${opts.orderBy}'`;
query += ` ORDER BY ${col} ${dir}`;
}
if (opts?.limit !== undefined) {
query += ` LIMIT $${paramIdx.toString()}`;
params.push(opts.limit);
paramIdx++;
}
if (opts?.offset !== undefined) {
query += ` OFFSET $${paramIdx.toString()}`;
params.push(opts.offset);
paramIdx++;
}
const result = await pg.query<{ id: string; data: Record<string, unknown> }>(query, params);
return result.rows.map((row) => ({ id: row.id, ...(row.data as object) }) as unknown as T);
}
async function pgCount(
pg: PgClient,
collection: string,
filter?: Record<string, unknown>,
): Promise<number> {
const { clause, params } = buildFilterClause(filter);
const result = await pg.query<{ count: string }>(
`SELECT COUNT(*) as count FROM ${collection}${clause}`,
params,
);
return parseInt(result.rows[0]?.count ?? '0', 10);
}
export class PgliteAdapter implements StorageAdapter {
readonly name = 'pglite';
private pg: PGlite;
constructor(config: Extract<StorageConfig, { type: 'pglite' }>) {
this.pg = new PGlite(config.dataDir);
}
async create<T extends Record<string, unknown>>(
collection: string,
data: T,
): Promise<T & { id: string }> {
return pgCreate(this.pg, collection, data);
}
async read<T extends Record<string, unknown>>(collection: string, id: string): Promise<T | null> {
return pgRead(this.pg, collection, id);
}
async update(collection: string, id: string, data: Record<string, unknown>): Promise<boolean> {
return pgUpdate(this.pg, collection, id, data);
}
async delete(collection: string, id: string): Promise<boolean> {
return pgDelete(this.pg, collection, id);
}
async find<T extends Record<string, unknown>>(
collection: string,
filter?: Record<string, unknown>,
opts?: { limit?: number; offset?: number; orderBy?: string; order?: 'asc' | 'desc' },
): Promise<T[]> {
return pgFind(this.pg, collection, filter, opts);
}
async findOne<T extends Record<string, unknown>>(
collection: string,
filter: Record<string, unknown>,
): Promise<T | null> {
const results = await this.find<T>(collection, filter, { limit: 1 });
return results[0] ?? null;
}
async count(collection: string, filter?: Record<string, unknown>): Promise<number> {
return pgCount(this.pg, collection, filter);
}
async transaction<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T> {
return this.pg.transaction(async (tx) => {
const txAdapter = new PgliteTxAdapter(tx as unknown as PgClient);
return fn(txAdapter);
});
}
async migrate(): Promise<void> {
for (const name of COLLECTIONS) {
await this.pg.query(`
CREATE TABLE IF NOT EXISTS ${name} (
id TEXT PRIMARY KEY,
data JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`);
}
}
async close(): Promise<void> {
await this.pg.close();
}
}
/**
* Transaction wrapper that delegates to the PGlite transaction connection.
*/
class PgliteTxAdapter implements StorageAdapter {
readonly name = 'pglite';
private pg: PgClient;
constructor(pg: PgClient) {
this.pg = pg;
}
async create<T extends Record<string, unknown>>(
collection: string,
data: T,
): Promise<T & { id: string }> {
return pgCreate(this.pg, collection, data);
}
async read<T extends Record<string, unknown>>(collection: string, id: string): Promise<T | null> {
return pgRead(this.pg, collection, id);
}
async update(collection: string, id: string, data: Record<string, unknown>): Promise<boolean> {
return pgUpdate(this.pg, collection, id, data);
}
async delete(collection: string, id: string): Promise<boolean> {
return pgDelete(this.pg, collection, id);
}
async find<T extends Record<string, unknown>>(
collection: string,
filter?: Record<string, unknown>,
opts?: { limit?: number; offset?: number; orderBy?: string; order?: 'asc' | 'desc' },
): Promise<T[]> {
return pgFind(this.pg, collection, filter, opts);
}
async findOne<T extends Record<string, unknown>>(
collection: string,
filter: Record<string, unknown>,
): Promise<T | null> {
const results = await this.find<T>(collection, filter, { limit: 1 });
return results[0] ?? null;
}
async count(collection: string, filter?: Record<string, unknown>): Promise<number> {
return pgCount(this.pg, collection, filter);
}
async transaction<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T> {
// Already inside a transaction — run directly
return fn(this);
}
async migrate(): Promise<void> {
// No-op inside transaction
}
async close(): Promise<void> {
// No-op inside transaction
}
}

View File

@@ -1,283 +0,0 @@
import Database from 'better-sqlite3';
import { randomUUID } from 'node:crypto';
import type { StorageAdapter, StorageConfig } from '../types.js';
/* eslint-disable @typescript-eslint/no-explicit-any */
const COLLECTIONS = [
'users',
'sessions',
'accounts',
'projects',
'missions',
'tasks',
'agents',
'conversations',
'messages',
'preferences',
'insights',
'skills',
'events',
'routing_rules',
'provider_credentials',
'agent_logs',
'teams',
'team_members',
'mission_tasks',
'tickets',
'summarization_jobs',
'appreciations',
'verifications',
] as const;
function buildFilterClause(filter?: Record<string, unknown>): {
clause: string;
params: unknown[];
} {
if (!filter || Object.keys(filter).length === 0) return { clause: '', params: [] };
const conditions: string[] = [];
const params: unknown[] = [];
for (const [key, value] of Object.entries(filter)) {
if (key === 'id') {
conditions.push('id = ?');
params.push(value);
} else {
conditions.push(`json_extract(data_json, '$.${key}') = ?`);
params.push(typeof value === 'object' ? JSON.stringify(value) : value);
}
}
return { clause: ` WHERE ${conditions.join(' AND ')}`, params };
}
export class SqliteAdapter implements StorageAdapter {
readonly name = 'sqlite';
private db: Database.Database;
constructor(config: Extract<StorageConfig, { type: 'sqlite' }>) {
this.db = new Database(config.path);
this.db.pragma('journal_mode = WAL');
this.db.pragma('foreign_keys = ON');
}
async create<T extends Record<string, unknown>>(
collection: string,
data: T,
): Promise<T & { id: string }> {
const id = (data as any).id ?? randomUUID();
const now = new Date().toISOString();
const rest = Object.fromEntries(Object.entries(data).filter(([k]) => k !== 'id'));
this.db
.prepare(
`INSERT INTO ${collection} (id, data_json, created_at, updated_at) VALUES (?, ?, ?, ?)`,
)
.run(id, JSON.stringify(rest), now, now);
return { ...data, id } as T & { id: string };
}
async read<T extends Record<string, unknown>>(collection: string, id: string): Promise<T | null> {
const row = this.db.prepare(`SELECT * FROM ${collection} WHERE id = ?`).get(id) as any;
if (!row) return null;
return { id: row.id, ...JSON.parse(row.data_json as string) } as T;
}
async update(collection: string, id: string, data: Record<string, unknown>): Promise<boolean> {
const existing = this.db
.prepare(`SELECT data_json FROM ${collection} WHERE id = ?`)
.get(id) as any;
if (!existing) return false;
const merged = { ...JSON.parse(existing.data_json as string), ...data };
const now = new Date().toISOString();
const result = this.db
.prepare(`UPDATE ${collection} SET data_json = ?, updated_at = ? WHERE id = ?`)
.run(JSON.stringify(merged), now, id);
return result.changes > 0;
}
async delete(collection: string, id: string): Promise<boolean> {
const result = this.db.prepare(`DELETE FROM ${collection} WHERE id = ?`).run(id);
return result.changes > 0;
}
async find<T extends Record<string, unknown>>(
collection: string,
filter?: Record<string, unknown>,
opts?: { limit?: number; offset?: number; orderBy?: string; order?: 'asc' | 'desc' },
): Promise<T[]> {
const { clause, params } = buildFilterClause(filter);
let query = `SELECT * FROM ${collection}${clause}`;
if (opts?.orderBy) {
const dir = opts.order === 'desc' ? 'DESC' : 'ASC';
const col =
opts.orderBy === 'id' || opts.orderBy === 'created_at' || opts.orderBy === 'updated_at'
? opts.orderBy
: `json_extract(data_json, '$.${opts.orderBy}')`;
query += ` ORDER BY ${col} ${dir}`;
}
if (opts?.limit) {
query += ` LIMIT ?`;
params.push(opts.limit);
}
if (opts?.offset) {
query += ` OFFSET ?`;
params.push(opts.offset);
}
const rows = this.db.prepare(query).all(...params) as any[];
return rows.map((row) => ({ id: row.id, ...JSON.parse(row.data_json as string) }) as T);
}
async findOne<T extends Record<string, unknown>>(
collection: string,
filter: Record<string, unknown>,
): Promise<T | null> {
const results = await this.find<T>(collection, filter, { limit: 1 });
return results[0] ?? null;
}
async count(collection: string, filter?: Record<string, unknown>): Promise<number> {
const { clause, params } = buildFilterClause(filter);
const row = this.db
.prepare(`SELECT COUNT(*) as count FROM ${collection}${clause}`)
.get(...params) as any;
return row?.count ?? 0;
}
async transaction<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T> {
const txAdapter = new SqliteTxAdapter(this.db);
this.db.exec('BEGIN');
try {
const result = await fn(txAdapter);
this.db.exec('COMMIT');
return result;
} catch (err) {
this.db.exec('ROLLBACK');
throw err;
}
}
async migrate(): Promise<void> {
const createTable = (name: string) =>
this.db.exec(`
CREATE TABLE IF NOT EXISTS ${name} (
id TEXT PRIMARY KEY,
data_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
for (const collection of COLLECTIONS) {
createTable(collection);
}
}
async close(): Promise<void> {
this.db.close();
}
}
/**
* Transaction wrapper that uses the same db handle — better-sqlite3 transactions
* are connection-level, so all statements on the same Database instance within
* a db.transaction() callback participate in the transaction.
*/
class SqliteTxAdapter implements StorageAdapter {
readonly name = 'sqlite';
private db: Database.Database;
constructor(db: Database.Database) {
this.db = db;
}
async create<T extends Record<string, unknown>>(
collection: string,
data: T,
): Promise<T & { id: string }> {
const id = (data as any).id ?? randomUUID();
const now = new Date().toISOString();
const rest = Object.fromEntries(Object.entries(data).filter(([k]) => k !== 'id'));
this.db
.prepare(
`INSERT INTO ${collection} (id, data_json, created_at, updated_at) VALUES (?, ?, ?, ?)`,
)
.run(id, JSON.stringify(rest), now, now);
return { ...data, id } as T & { id: string };
}
async read<T extends Record<string, unknown>>(collection: string, id: string): Promise<T | null> {
const row = this.db.prepare(`SELECT * FROM ${collection} WHERE id = ?`).get(id) as any;
if (!row) return null;
return { id: row.id, ...JSON.parse(row.data_json as string) } as T;
}
async update(collection: string, id: string, data: Record<string, unknown>): Promise<boolean> {
const existing = this.db
.prepare(`SELECT data_json FROM ${collection} WHERE id = ?`)
.get(id) as any;
if (!existing) return false;
const merged = { ...JSON.parse(existing.data_json as string), ...data };
const now = new Date().toISOString();
const result = this.db
.prepare(`UPDATE ${collection} SET data_json = ?, updated_at = ? WHERE id = ?`)
.run(JSON.stringify(merged), now, id);
return result.changes > 0;
}
async delete(collection: string, id: string): Promise<boolean> {
const result = this.db.prepare(`DELETE FROM ${collection} WHERE id = ?`).run(id);
return result.changes > 0;
}
async find<T extends Record<string, unknown>>(
collection: string,
filter?: Record<string, unknown>,
opts?: { limit?: number; offset?: number; orderBy?: string; order?: 'asc' | 'desc' },
): Promise<T[]> {
const { clause, params } = buildFilterClause(filter);
let query = `SELECT * FROM ${collection}${clause}`;
if (opts?.orderBy) {
const dir = opts.order === 'desc' ? 'DESC' : 'ASC';
const col =
opts.orderBy === 'id' || opts.orderBy === 'created_at' || opts.orderBy === 'updated_at'
? opts.orderBy
: `json_extract(data_json, '$.${opts.orderBy}')`;
query += ` ORDER BY ${col} ${dir}`;
}
if (opts?.limit) {
query += ` LIMIT ?`;
params.push(opts.limit);
}
if (opts?.offset) {
query += ` OFFSET ?`;
params.push(opts.offset);
}
const rows = this.db.prepare(query).all(...params) as any[];
return rows.map((row) => ({ id: row.id, ...JSON.parse(row.data_json as string) }) as T);
}
async findOne<T extends Record<string, unknown>>(
collection: string,
filter: Record<string, unknown>,
): Promise<T | null> {
const results = await this.find<T>(collection, filter, { limit: 1 });
return results[0] ?? null;
}
async count(collection: string, filter?: Record<string, unknown>): Promise<number> {
const { clause, params } = buildFilterClause(filter);
const row = this.db
.prepare(`SELECT COUNT(*) as count FROM ${collection}${clause}`)
.get(...params) as any;
return row?.count ?? 0;
}
async transaction<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T> {
return fn(this);
}
async migrate(): Promise<void> {
// No-op inside transaction
}
async close(): Promise<void> {
// No-op inside transaction
}
}

View File

@@ -1,17 +1,17 @@
export type { StorageAdapter, StorageConfig } from './types.js';
export { createStorageAdapter, registerStorageAdapter } from './factory.js';
export { PostgresAdapter } from './adapters/postgres.js';
export { SqliteAdapter } from './adapters/sqlite.js';
export { PgliteAdapter } from './adapters/pglite.js';
import { registerStorageAdapter } from './factory.js';
import { PostgresAdapter } from './adapters/postgres.js';
import { SqliteAdapter } from './adapters/sqlite.js';
import { PgliteAdapter } from './adapters/pglite.js';
import type { StorageConfig } from './types.js';
registerStorageAdapter('postgres', (config: StorageConfig) => {
return new PostgresAdapter(config as Extract<StorageConfig, { type: 'postgres' }>);
});
registerStorageAdapter('sqlite', (config: StorageConfig) => {
return new SqliteAdapter(config as Extract<StorageConfig, { type: 'sqlite' }>);
registerStorageAdapter('pglite', (config: StorageConfig) => {
return new PgliteAdapter(config as Extract<StorageConfig, { type: 'pglite' }>);
});

View File

@@ -39,5 +39,5 @@ export interface StorageAdapter {
export type StorageConfig =
| { type: 'postgres'; url: string }
| { type: 'sqlite'; path: string }
| { type: 'pglite'; dataDir?: string }
| { type: 'files'; dataDir: string; format?: 'json' | 'md' };

1712
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff