180 lines
7.4 KiB
TypeScript
180 lines
7.4 KiB
TypeScript
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<MemoryAdapter> {
|
|
const connStr = dbOption ?? process.env['MEMORY_DB_URL'] ?? process.env['DATABASE_URL'];
|
|
if (!connStr) {
|
|
throw new Error(
|
|
'No database connection string provided. ' +
|
|
'Pass --db <connection-string> 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 <query> ──────────────────────────────────────────────
|
|
memory
|
|
.command('search <query>')
|
|
.description('Semantic search over insights')
|
|
.option('--db <connection-string>', 'Database connection string (or set MEMORY_DB_URL)')
|
|
.option('--limit <n>', 'Maximum number of results', '10')
|
|
.option('--agent <id>', '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 <connection-string>', 'Database connection string (or set MEMORY_DB_URL)')
|
|
.option('--agent <id>', '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 <connection-string>', 'Database connection string (or set MEMORY_DB_URL)')
|
|
.option('--limit <n>', 'Maximum number of results', '20')
|
|
.option('--agent <id>', '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 <connection-string>', 'Database connection string (or set MEMORY_DB_URL)')
|
|
.option('--agent <id>', 'User / agent ID scope', 'system')
|
|
.option('--category <cat>', '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();
|
|
}
|
|
});
|
|
}
|