import type { Command } from 'commander'; import type { MemoryAdapter } from './types.js'; /** * Build and return a connected MemoryAdapter from a connection string or * the MEMORY_DB_URL / DATABASE_URL environment variable. * * For pgvector (postgres://...) the connection string is injected into * DATABASE_URL so that PgVectorAdapter's internal createDb() picks it up. * * Throws with a human-readable message if no connection info is available. */ async function resolveAdapter(dbOption: string | undefined): Promise { const connStr = dbOption ?? process.env['MEMORY_DB_URL'] ?? process.env['DATABASE_URL']; if (!connStr) { throw new Error( 'No database connection string provided. ' + 'Pass --db or set MEMORY_DB_URL / DATABASE_URL.', ); } // Lazy imports so the module loads cleanly without a live DB during smoke tests. const { createMemoryAdapter, registerMemoryAdapter } = await import('./factory.js'); if (connStr.startsWith('postgres') || connStr.startsWith('pg')) { // PgVectorAdapter reads DATABASE_URL via createDb() — inject it here. process.env['DATABASE_URL'] = connStr; const { PgVectorAdapter } = await import('./adapters/pgvector.js'); registerMemoryAdapter('pgvector', (cfg) => new PgVectorAdapter(cfg as never)); return createMemoryAdapter({ type: 'pgvector' }); } // Keyword adapter backed by pglite storage; treat connStr as a data directory. const { KeywordAdapter } = await import('./adapters/keyword.js'); const { createStorageAdapter, registerStorageAdapter } = await import('@mosaicstack/storage'); const { PgliteAdapter } = await import('@mosaicstack/storage'); registerStorageAdapter('pglite', (cfg) => new PgliteAdapter(cfg as never)); const storage = createStorageAdapter({ type: 'pglite', dataDir: connStr }); registerMemoryAdapter('keyword', (cfg) => new KeywordAdapter(cfg as never)); return createMemoryAdapter({ type: 'keyword', storage }); } /** * Register `memory` subcommands on an existing Commander program. * Follows the registerQualityRails pattern from @mosaicstack/quality-rails. */ export function registerMemoryCommand(parent: Command): void { const memory = parent.command('memory').description('Inspect and query the Mosaic memory layer'); // ── memory search ────────────────────────────────────────────── memory .command('search ') .description('Semantic search over insights') .option('--db ', 'Database connection string (or set MEMORY_DB_URL)') .option('--limit ', 'Maximum number of results', '10') .option('--agent ', 'Filter by agent / user ID') .action(async (query: string, opts: { db?: string; limit: string; agent?: string }) => { let adapter: MemoryAdapter | undefined; try { adapter = await resolveAdapter(opts.db); const limit = parseInt(opts.limit, 10); const userId = opts.agent ?? 'system'; const results = await adapter.searchInsights(userId, query, { limit }); if (results.length === 0) { console.log('No insights found.'); } else { for (const r of results) { console.log(`[${r.id}] (score=${r.score.toFixed(3)}) ${r.content}`); } } } catch (err) { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); process.exitCode = 1; } finally { await adapter?.close(); } }); // ── memory stats ────────────────────────────────────────────────────── memory .command('stats') .description('Print memory tier info: adapter type, insight count, preference count') .option('--db ', 'Database connection string (or set MEMORY_DB_URL)') .option('--agent ', 'User / agent ID scope for counts', 'system') .action(async (opts: { db?: string; agent: string }) => { let adapter: MemoryAdapter | undefined; try { adapter = await resolveAdapter(opts.db); const adapterType = adapter.name; const insightCount = await adapter .searchInsights(opts.agent, '', { limit: 100000 }) .then((r) => r.length) .catch(() => -1); const prefCount = await adapter .listPreferences(opts.agent) .then((r) => r.length) .catch(() => -1); console.log(`adapter: ${adapterType}`); console.log(`insights: ${insightCount === -1 ? 'unavailable' : String(insightCount)}`); console.log(`preferences: ${prefCount === -1 ? 'unavailable' : String(prefCount)}`); } catch (err) { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); process.exitCode = 1; } finally { await adapter?.close(); } }); // ── memory insights ─────────────────────────────────────────────────── const insightsCmd = memory.command('insights').description('Manage insights'); insightsCmd .command('list') .description('List recent insights') .option('--db ', 'Database connection string (or set MEMORY_DB_URL)') .option('--limit ', 'Maximum number of results', '20') .option('--agent ', 'User / agent ID scope', 'system') .action(async (opts: { db?: string; limit: string; agent: string }) => { let adapter: MemoryAdapter | undefined; try { adapter = await resolveAdapter(opts.db); const limit = parseInt(opts.limit, 10); const results = await adapter.searchInsights(opts.agent, '', { limit }); if (results.length === 0) { console.log('No insights found.'); } else { for (const r of results) { console.log(`[${r.id}] ${r.content}`); } } } catch (err) { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); process.exitCode = 1; } finally { await adapter?.close(); } }); // ── memory preferences ──────────────────────────────────────────────── const prefsCmd = memory.command('preferences').description('Manage stored preferences'); prefsCmd .command('list') .description('List stored preferences') .option('--db ', 'Database connection string (or set MEMORY_DB_URL)') .option('--agent ', 'User / agent ID scope', 'system') .option('--category ', 'Filter by category') .action(async (opts: { db?: string; agent: string; category?: string }) => { let adapter: MemoryAdapter | undefined; try { adapter = await resolveAdapter(opts.db); const prefs = await adapter.listPreferences(opts.agent, opts.category); if (prefs.length === 0) { console.log('No preferences found.'); } else { for (const p of prefs) { console.log(`[${p.category}] ${p.key} = ${JSON.stringify(p.value)}`); } } } catch (err) { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); process.exitCode = 1; } finally { await adapter?.close(); } }); }