claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
1,232 lines • 64.1 kB
JavaScript
/**
* V3 CLI Memory Command
* Memory operations for AgentDB integration
*/
import { output } from '../output.js';
import { select, confirm, input } from '../prompt.js';
import { callMCPTool, MCPClientError } from '../mcp-client.js';
// Memory backends
const BACKENDS = [
{ value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW indexing (150x-12,500x faster)' },
{ value: 'sqlite', label: 'SQLite', hint: 'Lightweight local storage' },
{ value: 'hybrid', label: 'Hybrid', hint: 'SQLite + AgentDB (recommended)' },
{ value: 'memory', label: 'In-Memory', hint: 'Fast but non-persistent' }
];
// #2105: shared --path option for memory subcommands.
// Precedence: --path > CLAUDE_FLOW_DB_PATH env var > default root
const DB_PATH_OPTION = {
name: 'path',
description: 'Override DB file path (also: CLAUDE_FLOW_DB_PATH env var). ' +
'Precedence: --path > CLAUDE_FLOW_DB_PATH > CLAUDE_FLOW_MEMORY_PATH/memory.db > cwd/.swarm/memory.db',
type: 'string',
};
// Store command
const storeCommand = {
name: 'store',
description: 'Store data in memory',
options: [
{
name: 'key',
short: 'k',
description: 'Storage key/namespace',
type: 'string',
required: true
},
{
name: 'value',
// Note: No short flag - global -v is reserved for verbose
description: 'Value to store (use --value)',
type: 'string'
},
{
name: 'namespace',
short: 'n',
description: 'Memory namespace',
type: 'string',
default: 'default'
},
{
name: 'ttl',
description: 'Time to live in seconds',
type: 'number'
},
{
name: 'tags',
description: 'Comma-separated tags',
type: 'string'
},
{
name: 'vector',
description: 'Store as vector embedding',
type: 'boolean',
default: false
},
{
name: 'upsert',
short: 'u',
description: 'Update if key exists (insert or replace)',
type: 'boolean',
default: false
},
DB_PATH_OPTION
],
examples: [
{ command: 'claude-flow memory store -k "api/auth" -v "JWT implementation"', description: 'Store text' },
{ command: 'claude-flow memory store -k "pattern/singleton" --vector', description: 'Store vector' },
{ command: 'claude-flow memory store -k "pattern" -v "updated" --upsert', description: 'Update existing' }
],
action: async (ctx) => {
const key = ctx.flags.key;
let value = ctx.flags.value || ctx.args[0];
const namespace = ctx.flags.namespace;
const ttl = ctx.flags.ttl;
const tags = ctx.flags.tags ? ctx.flags.tags.split(',') : [];
const asVector = ctx.flags.vector;
const upsert = ctx.flags.upsert;
if (!key) {
output.printError('Key is required. Use --key or -k');
return { success: false, exitCode: 1 };
}
if (!value && ctx.interactive) {
value = await input({
message: 'Enter value to store:',
validate: (v) => v.length > 0 || 'Value is required'
});
}
if (!value) {
output.printError('Value is required. Use --value');
return { success: false, exitCode: 1 };
}
const storeData = {
key,
namespace,
value,
ttl,
tags,
asVector,
storedAt: new Date().toISOString(),
size: Buffer.byteLength(value, 'utf8')
};
output.printInfo(`Storing in ${namespace}/${key}...`);
// Use direct sql.js storage with automatic embedding generation
try {
const { storeEntry, resolveDbPath: _rdbStore } = await import('../memory/memory-initializer.js');
const dbPath = _rdbStore(ctx.flags.path);
if (asVector) {
output.writeln(output.dim(' Generating embedding vector...'));
}
const result = await storeEntry({
key,
value,
namespace,
generateEmbeddingFlag: true, // Always generate embeddings for semantic search
tags,
ttl,
upsert,
dbPath
});
if (!result.success) {
output.printError(result.error || 'Failed to store');
return { success: false, exitCode: 1 };
}
output.writeln();
output.printTable({
columns: [
{ key: 'property', header: 'Property', width: 15 },
{ key: 'val', header: 'Value', width: 40 }
],
data: [
{ property: 'Key', val: key },
{ property: 'Namespace', val: namespace },
{ property: 'Size', val: `${storeData.size} bytes` },
{ property: 'TTL', val: ttl ? `${ttl}s` : 'None' },
{ property: 'Tags', val: tags.length > 0 ? tags.join(', ') : 'None' },
{ property: 'Vector', val: result.embedding ? `Yes (${result.embedding.dimensions}-dim)` : 'No' },
{ property: 'ID', val: result.id.substring(0, 20) }
]
});
output.writeln();
output.printSuccess('Data stored successfully');
return { success: true, data: { ...storeData, id: result.id, embedding: result.embedding } };
}
catch (error) {
output.printError(`Failed to store: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { success: false, exitCode: 1 };
}
}
};
// Retrieve command
const retrieveCommand = {
name: 'retrieve',
aliases: ['get'],
description: 'Retrieve data from memory',
options: [
{
name: 'key',
short: 'k',
description: 'Storage key',
type: 'string'
},
{
name: 'namespace',
short: 'n',
description: 'Memory namespace',
type: 'string',
default: 'default'
},
// #2073: --format is the GLOBAL option (parser.ts:78) with choices
// ['text', 'json', 'table'] and default 'text'. The retrieve handler
// discriminates: 'json' emits parseable JSON, anything else (text/box/...)
// emits the human-readable box. No per-command override needed; we just
// document the behavior in the help text via examples.
{
// #2073: --value-only emits ONLY the value string (no wrapper).
// Designed for piping into JSON.parse without any cleanup.
name: 'value-only',
description: 'Print only the stored value to stdout (no wrapper)',
type: 'boolean',
default: false
},
DB_PATH_OPTION
],
action: async (ctx) => {
const key = ctx.flags.key || ctx.args[0];
const namespace = ctx.flags.namespace;
if (!key) {
output.printError('Key is required');
return { success: false, exitCode: 1 };
}
// Use sql.js directly for consistent data access
try {
const { getEntry, resolveDbPath: _rdbRetrieve } = await import('../memory/memory-initializer.js');
const dbPathRetrieve = _rdbRetrieve(ctx.flags.path);
const result = await getEntry({ key, namespace, dbPath: dbPathRetrieve });
if (!result.success) {
output.printError(`Failed to retrieve: ${result.error}`);
return { success: false, exitCode: 1 };
}
if (!result.found || !result.entry) {
output.printWarning(`Key not found: ${key}`);
return { success: false, exitCode: 1, data: { key, found: false } };
}
const entry = result.entry;
// #2073: --value-only emits just the raw value (no decoration) for
// piping into JSON.parse / jq / other downstream parsers without
// any cleanup.
if (ctx.flags['value-only'] || ctx.flags.valueOnly) {
// Use process.stdout.write directly to bypass any printer-side
// transformation of quotes/structural characters.
process.stdout.write(entry.content);
if (process.stdout.isTTY)
process.stdout.write('\n');
return { success: true, data: entry };
}
if (ctx.flags.format === 'json') {
output.printJson(entry);
return { success: true, data: entry };
}
output.writeln();
output.printBox([
`Namespace: ${entry.namespace}`,
`Key: ${entry.key}`,
`Size: ${entry.content.length} bytes`,
`Access Count: ${entry.accessCount}`,
`Tags: ${entry.tags.length > 0 ? entry.tags.join(', ') : 'None'}`,
`Vector: ${entry.hasEmbedding ? 'Yes' : 'No'}`,
'',
output.bold('Value:'),
entry.content
].join('\n'), 'Memory Entry');
return { success: true, data: entry };
}
catch (error) {
output.printError(`Failed to retrieve: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { success: false, exitCode: 1 };
}
}
};
// Search command
const searchCommand = {
name: 'search',
description: 'Search memory with semantic/vector search',
options: [
{
name: 'query',
short: 'q',
description: 'Search query',
type: 'string',
required: true
},
{
name: 'namespace',
short: 'n',
description: 'Memory namespace',
type: 'string'
},
{
name: 'limit',
short: 'l',
description: 'Maximum results',
type: 'number',
default: 10
},
{
name: 'threshold',
description: 'Similarity threshold (0-1)',
type: 'number',
default: 0.7
},
{
name: 'type',
short: 't',
description: 'Search type (semantic, keyword, hybrid)',
type: 'string',
default: 'semantic',
choices: ['semantic', 'keyword', 'hybrid']
},
{
name: 'build-hnsw',
description: 'Build/rebuild HNSW index before searching (enables 150x-12,500x speedup)',
type: 'boolean',
default: false
},
{
name: 'smart',
short: 's',
description: 'Use SmartRetrieval pipeline (query expansion, RRF, MMR, recency)',
type: 'boolean',
default: false
},
DB_PATH_OPTION
],
examples: [
{ command: 'claude-flow memory search -q "authentication patterns"', description: 'Semantic search' },
{ command: 'claude-flow memory search -q "JWT" -t keyword', description: 'Keyword search' },
{ command: 'claude-flow memory search -q "test" --build-hnsw', description: 'Build HNSW index and search' },
{ command: 'claude-flow memory search -q "auth patterns" --smart', description: 'SmartRetrieval with RRF + MMR' }
],
action: async (ctx) => {
const query = ctx.flags.query || ctx.args[0];
const namespace = ctx.flags.namespace || 'all';
const limit = ctx.flags.limit || 10;
const threshold = ctx.flags.threshold || 0.3;
const searchType = ctx.flags.type || 'semantic';
const buildHnsw = (ctx.flags['build-hnsw'] || ctx.flags.buildHnsw);
if (!query) {
output.printError('Query is required. Use --query or -q');
return { success: false, exitCode: 1 };
}
// Build/rebuild HNSW index if requested
if (buildHnsw) {
output.printInfo('Building HNSW index...');
try {
const { getHNSWIndex, getHNSWStatus } = await import('../memory/memory-initializer.js');
const startTime = Date.now();
const index = await getHNSWIndex({ forceRebuild: true });
const buildTime = Date.now() - startTime;
if (index) {
const status = getHNSWStatus();
output.printSuccess(`HNSW index built (${status.entryCount} vectors, ${buildTime}ms)`);
output.writeln(output.dim(` Dimensions: ${status.dimensions}, Metric: cosine`));
output.writeln(output.dim(` Search speedup: ${status.entryCount > 10000 ? '12,500x' : status.entryCount > 1000 ? '150x' : '10x'}`));
}
else {
output.printWarning('HNSW index not available (install @ruvector/core for acceleration)');
}
output.writeln();
}
catch (error) {
output.printWarning(`HNSW build failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
output.writeln(output.dim(' Falling back to brute-force search'));
output.writeln();
}
}
output.printInfo(`Searching: "${query}" (${searchType})`);
output.writeln();
// Use direct sql.js search with vector similarity
try {
const { searchEntries, resolveDbPath: _rdbSearch } = await import('../memory/memory-initializer.js');
const dbPathSearch = _rdbSearch(ctx.flags.path);
const useSmart = (ctx.flags.smart || ctx.flags.s);
let results;
let searchTimeMs;
let smartStats;
let backendLabel = 'HNSW + sql.js';
// #1846: feature-detect smartSearch — older published builds of
// @claude-flow/memory don't expose it. Fall through to plain
// semantic search with a one-line warning instead of throwing.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let smartSearchFn;
if (useSmart) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const memMod = await import('@claude-flow/memory');
if (typeof memMod.smartSearch === 'function') {
smartSearchFn = memMod.smartSearch;
}
}
catch {
/* memory package not loadable */
}
if (!smartSearchFn) {
output.printWarning('Smart search requested but smartSearch is not available on the installed @claude-flow/memory build (#1846). Falling back to standard semantic search.');
}
}
if (useSmart && smartSearchFn) {
// Adapt searchEntries to the SearchFn interface
const rawSearch = async (req) => {
const r = await searchEntries({
query: req.query,
namespace: req.namespace || namespace,
limit: req.limit || limit * 3,
threshold: req.threshold ?? threshold,
dbPath: dbPathSearch,
});
return {
results: r.results.map(e => ({
id: e.id,
key: e.key,
content: e.content,
score: e.score,
namespace: e.namespace,
})),
};
};
const smartResult = await smartSearchFn(rawSearch, {
query,
namespace,
limit,
threshold,
});
results = smartResult.results.map((r) => ({
key: r.key,
score: r.score,
namespace: r.namespace,
preview: r.content,
}));
searchTimeMs = smartResult.stats.durationMs;
smartStats = smartResult.stats;
backendLabel = 'SmartRetrieval (RRF + MMR + Recency)';
}
else {
const searchResult = await searchEntries({
query,
namespace,
limit,
threshold,
dbPath: dbPathSearch
});
if (!searchResult.success) {
output.printError(searchResult.error || 'Search failed');
return { success: false, exitCode: 1 };
}
results = searchResult.results.map(r => ({
key: r.key,
score: r.score,
namespace: r.namespace,
preview: r.content
}));
searchTimeMs = searchResult.searchTime;
}
if (ctx.flags.format === 'json') {
output.printJson({ query, searchType, results, searchTime: `${searchTimeMs}ms`, ...(smartStats ? { stats: smartStats } : {}) });
return { success: true, data: results };
}
// Performance stats
output.writeln(output.dim(` Search time: ${searchTimeMs}ms`));
if (useSmart && smartStats) {
output.writeln(output.dim(` Backend: ${backendLabel}`));
output.writeln(output.dim(` Variants: ${smartStats.variantCount}, Raw candidates: ${smartStats.rawCandidateCount}`));
}
output.writeln();
if (results.length === 0) {
output.printWarning('No results found');
output.writeln(output.dim('Try: claude-flow memory store -k "key" --value "data"'));
return { success: true, data: [] };
}
output.printTable({
columns: [
{ key: 'key', header: 'Key', width: 20 },
{ key: 'score', header: 'Score', width: 8, align: 'right', format: (v) => Number(v).toFixed(2) },
{ key: 'namespace', header: 'Namespace', width: 12 },
{ key: 'preview', header: 'Preview', width: 35 }
],
data: results
});
output.writeln();
output.printInfo(`Found ${results.length} results`);
return { success: true, data: results };
}
catch (error) {
output.printError(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { success: false, exitCode: 1 };
}
}
};
// List command
const listCommand = {
name: 'list',
aliases: ['ls'],
description: 'List memory entries',
options: [
{
name: 'namespace',
short: 'n',
description: 'Filter by namespace',
type: 'string'
},
{
name: 'tags',
short: 't',
description: 'Filter by tags (comma-separated)',
type: 'string'
},
{
name: 'limit',
short: 'l',
description: 'Maximum entries',
type: 'number',
default: 20
},
DB_PATH_OPTION
],
action: async (ctx) => {
const namespace = ctx.flags.namespace;
const limit = ctx.flags.limit;
// Use sql.js directly for consistent data access
try {
const { listEntries, resolveDbPath: _rdbList } = await import('../memory/memory-initializer.js');
const dbPathList = _rdbList(ctx.flags.path);
const listResult = await listEntries({ namespace, limit, offset: 0, dbPath: dbPathList });
if (!listResult.success) {
output.printError(`Failed to list: ${listResult.error}`);
return { success: false, exitCode: 1 };
}
// Format entries for display
const entries = listResult.entries.map(e => ({
key: e.key,
namespace: e.namespace,
size: e.size + ' B',
vector: e.hasEmbedding ? '✓' : '-',
accessCount: e.accessCount,
updated: formatRelativeTime(e.updatedAt)
}));
if (ctx.flags.format === 'json') {
output.printJson(listResult.entries);
return { success: true, data: listResult.entries };
}
output.writeln();
output.writeln(output.bold('Memory Entries'));
output.writeln();
if (entries.length === 0) {
output.printWarning('No entries found');
output.printInfo('Store data: claude-flow memory store -k "key" --value "data"');
return { success: true, data: [] };
}
output.printTable({
columns: [
{ key: 'key', header: 'Key', width: 25 },
{ key: 'namespace', header: 'Namespace', width: 12 },
{ key: 'size', header: 'Size', width: 10, align: 'right' },
{ key: 'vector', header: 'Vector', width: 8, align: 'center' },
{ key: 'accessCount', header: 'Accessed', width: 10, align: 'right' },
{ key: 'updated', header: 'Updated', width: 12 }
],
data: entries
});
output.writeln();
output.printInfo(`Showing ${entries.length} of ${listResult.total} entries`);
return { success: true, data: listResult.entries };
}
catch (error) {
output.printError(`Failed to list: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { success: false, exitCode: 1 };
}
}
};
// Helper function to format relative time
function formatRelativeTime(isoDate) {
const now = Date.now();
const date = new Date(isoDate).getTime();
const diff = now - date;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0)
return `${days}d ago`;
if (hours > 0)
return `${hours}h ago`;
if (minutes > 0)
return `${minutes}m ago`;
return 'just now';
}
// Delete command
const deleteCommand = {
name: 'delete',
aliases: ['rm'],
description: 'Delete memory entry',
options: [
{
name: 'key',
short: 'k',
description: 'Storage key',
type: 'string'
},
{
name: 'namespace',
short: 'n',
description: 'Memory namespace',
type: 'string',
default: 'default'
},
{
name: 'force',
short: 'f',
description: 'Skip confirmation',
type: 'boolean',
default: false
},
DB_PATH_OPTION
],
examples: [
{ command: 'claude-flow memory delete -k "mykey"', description: 'Delete entry with default namespace' },
{ command: 'claude-flow memory delete -k "lesson" -n "lessons"', description: 'Delete entry from specific namespace' },
{ command: 'claude-flow memory delete mykey -f', description: 'Delete without confirmation' }
],
action: async (ctx) => {
// Support both --key flag and positional argument
const key = ctx.flags.key || ctx.args[0];
const namespace = ctx.flags.namespace || 'default';
const force = ctx.flags.force;
if (!key) {
output.printError('Key is required. Use: memory delete -k "key" [-n "namespace"]');
return { success: false, exitCode: 1 };
}
if (!force && ctx.interactive) {
const confirmed = await confirm({
message: `Delete memory entry "${key}" from namespace "${namespace}"?`,
default: false
});
if (!confirmed) {
output.printInfo('Operation cancelled');
return { success: true };
}
}
// Use sql.js directly for consistent data access (Issue #980)
try {
const { deleteEntry, resolveDbPath: _rdbDelete } = await import('../memory/memory-initializer.js');
const dbPathDelete = _rdbDelete(ctx.flags.path);
const result = await deleteEntry({ key, namespace, dbPath: dbPathDelete });
if (!result.success) {
output.printError(result.error || 'Failed to delete');
return { success: false, exitCode: 1 };
}
if (result.deleted) {
output.printSuccess(`Deleted "${key}" from namespace "${namespace}"`);
output.printInfo(`Remaining entries: ${result.remainingEntries}`);
}
else {
output.printWarning(`Key not found: "${key}" in namespace "${namespace}"`);
}
return { success: result.deleted, data: result };
}
catch (error) {
output.printError(`Failed to delete: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { success: false, exitCode: 1 };
}
}
};
// Stats command
const statsCommand = {
name: 'stats',
description: 'Show memory statistics',
options: [DB_PATH_OPTION],
action: async (ctx) => {
// Call MCP memory/stats tool for real statistics
try {
const statsResult = await callMCPTool('memory_stats', {});
const stats = {
backend: statsResult.backend,
entries: {
total: statsResult.totalEntries,
vectors: 0, // Would need vector backend support
text: statsResult.totalEntries
},
storage: {
total: statsResult.totalSize,
location: statsResult.location
},
version: statsResult.version,
oldestEntry: statsResult.oldestEntry,
newestEntry: statsResult.newestEntry
};
if (ctx.flags.format === 'json') {
output.printJson(stats);
return { success: true, data: stats };
}
output.writeln();
output.writeln(output.bold('Memory Statistics'));
output.writeln();
output.writeln(output.bold('Overview'));
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 20 },
{ key: 'value', header: 'Value', width: 30, align: 'right' }
],
data: [
{ metric: 'Backend', value: stats.backend },
{ metric: 'Version', value: stats.version },
{ metric: 'Total Entries', value: stats.entries.total.toLocaleString() },
{ metric: 'Total Storage', value: stats.storage.total },
{ metric: 'Location', value: stats.storage.location }
]
});
output.writeln();
output.writeln(output.bold('Timeline'));
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 20 },
{ key: 'value', header: 'Value', width: 30, align: 'right' }
],
data: [
{ metric: 'Oldest Entry', value: stats.oldestEntry || 'N/A' },
{ metric: 'Newest Entry', value: stats.newestEntry || 'N/A' }
]
});
// #1622 — Surface the active embedding provider in `memory stats` so
// users can tell which backend resolved at runtime (the 6-level
// fallback chain in loadEmbeddingModel ranges from full ONNX to a
// 128-dim hash that has no semantic understanding). Calling
// loadEmbeddingModel() is cheap when the model is already cached;
// a fresh call still resolves quickly because we only need the
// metadata, not a real embedding.
try {
const { loadEmbeddingModel, getHNSWStatus } = await import('../memory/memory-initializer.js');
const embedding = await loadEmbeddingModel({ verbose: false });
const hnsw = getHNSWStatus();
// Map model name → semantic capability so users can spot the
// hash-fallback case without reading docs.
const semanticProviders = new Set([
'Xenova/all-MiniLM-L6-v2',
'Xenova/all-mpnet-base-v2',
'Xenova/bge-small-en-v1.5',
'agentic-flow',
'agentic-flow/reasoningbank',
'ruvector/onnx',
'cached',
]);
const isSemantic = embedding.success && semanticProviders.has(embedding.modelName);
output.writeln();
output.writeln(output.bold('Embedding'));
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 20 },
{ key: 'value', header: 'Value', width: 30, align: 'right' }
],
data: [
{
metric: 'Provider',
value: embedding.success
? embedding.modelName
: output.warning(`unavailable: ${embedding.error || 'unknown'}`),
},
{ metric: 'Dimensions', value: String(embedding.dimensions) },
{
metric: 'Semantic Search',
value: isSemantic
? output.success('yes')
: output.warning('no — using hash fallback'),
},
{
metric: 'HNSW Index',
// ruflo#1989 / #1987: `hnsw.entryCount` is in-process JS state
// (the live HNSW index of the current Node process). A fresh
// `memory stats` invocation has never indexed anything, so it
// reports 0 even when the persistent DB has thousands of
// entries with embeddings. Use the persistent count from the
// MCP tool (`entriesWithEmbeddings`, which is the actual
// count of rows that have a vector) as the source of truth.
value: (() => {
const persisted = typeof statsResult.entriesWithEmbeddings === 'number'
? statsResult.entriesWithEmbeddings
: null;
const live = hnsw.entryCount || 0;
const total = persisted !== null ? Math.max(persisted, live) : live;
if (!hnsw.available)
return output.dim('not active');
if (total === 0)
return output.warning('available but not initialized');
return output.success(`active (${total.toLocaleString()} entries)`);
})(),
},
]
});
}
catch (e) {
// Don't fail the whole stats command if introspection breaks —
// the rest of the dashboard is still useful.
output.writeln();
output.writeln(output.bold('Embedding'));
output.printInfo(`Provider info unavailable: ${e instanceof Error ? e.message : String(e)}`);
}
output.writeln();
output.printInfo('V3 Performance: 150x-12,500x faster search with HNSW indexing');
return { success: true, data: stats };
}
catch (error) {
output.printError(`Failed to get stats: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { success: false, exitCode: 1 };
}
}
};
// Configure command
const configureCommand = {
name: 'configure',
aliases: ['config'],
description: 'Configure memory backend',
options: [
{
name: 'backend',
short: 'b',
description: 'Memory backend',
type: 'string',
choices: BACKENDS.map(b => b.value)
},
{
name: 'path',
description: 'Storage path',
type: 'string'
},
{
name: 'cache-size',
description: 'Cache size in MB',
type: 'number'
},
{
name: 'hnsw-m',
description: 'HNSW M parameter',
type: 'number',
default: 16
},
{
name: 'hnsw-ef',
description: 'HNSW ef parameter',
type: 'number',
default: 200
}
],
action: async (ctx) => {
let backend = ctx.flags.backend;
if (!backend && ctx.interactive) {
backend = await select({
message: 'Select memory backend:',
options: BACKENDS,
default: 'hybrid'
});
}
const config = {
backend: backend || 'hybrid',
path: ctx.flags.path || './data/memory',
cacheSize: ctx.flags.cacheSize || 256,
hnsw: {
m: ctx.flags.hnswM || 16,
ef: ctx.flags.hnswEf || 200
}
};
output.writeln();
output.printInfo('Memory Configuration');
output.writeln();
output.printTable({
columns: [
{ key: 'setting', header: 'Setting', width: 20 },
{ key: 'value', header: 'Value', width: 25 }
],
data: [
{ setting: 'Backend', value: config.backend },
{ setting: 'Storage Path', value: config.path },
{ setting: 'Cache Size', value: `${config.cacheSize} MB` },
{ setting: 'HNSW M', value: config.hnsw.m },
{ setting: 'HNSW ef', value: config.hnsw.ef }
]
});
output.writeln();
output.printSuccess('Memory configuration updated');
return { success: true, data: config };
}
};
// Cleanup command
const cleanupCommand = {
name: 'cleanup',
description: 'Clean up stale and expired memory entries',
options: [
{
name: 'dry-run',
short: 'd',
description: 'Show what would be deleted',
type: 'boolean',
default: false
},
{
name: 'older-than',
short: 'o',
description: 'Delete entries older than (e.g., "7d", "30d")',
type: 'string'
},
{
name: 'expired-only',
short: 'e',
description: 'Only delete expired TTL entries',
type: 'boolean',
default: false
},
{
name: 'low-quality',
short: 'l',
description: 'Delete low quality patterns (threshold)',
type: 'number'
},
{
name: 'namespace',
short: 'n',
description: 'Clean specific namespace only',
type: 'string'
},
{
name: 'force',
short: 'f',
description: 'Skip confirmation',
type: 'boolean',
default: false
}
],
examples: [
{ command: 'claude-flow memory cleanup --dry-run', description: 'Preview cleanup' },
{ command: 'claude-flow memory cleanup --older-than 30d', description: 'Delete entries older than 30 days' },
{ command: 'claude-flow memory cleanup --expired-only', description: 'Clean expired entries' }
],
action: async (ctx) => {
const dryRun = ctx.flags.dryRun;
const force = ctx.flags.force;
if (dryRun) {
output.writeln(output.warning('DRY RUN - No changes will be made'));
}
output.printInfo('Analyzing memory for cleanup...');
try {
const result = await callMCPTool('memory_cleanup', {
dryRun,
olderThan: ctx.flags.olderThan,
expiredOnly: ctx.flags.expiredOnly,
lowQualityThreshold: ctx.flags.lowQuality,
namespace: ctx.flags.namespace,
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.writeln(output.bold('Cleanup Analysis'));
output.printTable({
columns: [
{ key: 'category', header: 'Category', width: 20 },
{ key: 'count', header: 'Count', width: 15, align: 'right' }
],
data: [
{ category: 'Expired (TTL)', count: result.candidates.expired },
{ category: 'Stale (unused)', count: result.candidates.stale },
{ category: 'Low Quality', count: result.candidates.lowQuality },
{ category: output.bold('Total'), count: output.bold(String(result.candidates.total)) }
]
});
if (!dryRun && result.candidates.total > 0 && !force) {
const confirmed = await confirm({
message: `Delete ${result.candidates.total} entries (${result.freed.formatted})?`,
default: false
});
if (!confirmed) {
output.printInfo('Cleanup cancelled');
return { success: true, data: result };
}
}
if (!dryRun) {
output.writeln();
output.printSuccess(`Cleaned ${result.deleted.entries} entries`);
output.printList([
`Vectors removed: ${result.deleted.vectors}`,
`Patterns removed: ${result.deleted.patterns}`,
`Space freed: ${result.freed.formatted}`,
`Duration: ${result.duration}ms`
]);
}
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Cleanup error: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Compress command
const compressCommand = {
name: 'compress',
description: 'Compress and optimize memory storage',
options: [
{
name: 'level',
short: 'l',
description: 'Compression level (fast, balanced, max)',
type: 'string',
choices: ['fast', 'balanced', 'max'],
default: 'balanced'
},
{
name: 'target',
short: 't',
description: 'Target (vectors, text, patterns, all)',
type: 'string',
choices: ['vectors', 'text', 'patterns', 'all'],
default: 'all'
},
{
name: 'quantize',
short: 'z',
description: 'Enable vector quantization (reduces memory 4-32x)',
type: 'boolean',
default: false
},
{
name: 'bits',
description: 'Quantization bits (4, 8, 16)',
type: 'number',
default: 8
},
{
name: 'rebuild-index',
short: 'r',
description: 'Rebuild HNSW index after compression',
type: 'boolean',
default: true
}
],
examples: [
{ command: 'claude-flow memory compress', description: 'Balanced compression' },
{ command: 'claude-flow memory compress --quantize --bits 4', description: '4-bit quantization (32x reduction)' },
{ command: 'claude-flow memory compress -l max -t vectors', description: 'Max compression on vectors' }
],
action: async (ctx) => {
const level = ctx.flags.level || 'balanced';
const target = ctx.flags.target || 'all';
const quantize = ctx.flags.quantize;
const bits = ctx.flags.bits || 8;
const rebuildIndex = ctx.flags.rebuildIndex ?? true;
output.writeln();
output.writeln(output.bold('Memory Compression'));
output.writeln(output.dim(`Level: ${level}, Target: ${target}, Quantize: ${quantize ? `${bits}-bit` : 'no'}`));
output.writeln();
const spinner = output.createSpinner({ text: 'Analyzing current storage...', spinner: 'dots' });
spinner.start();
try {
const result = await callMCPTool('memory_compress', {
level,
target,
quantize,
bits,
rebuildIndex,
});
spinner.succeed('Compression complete');
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.writeln(output.bold('Storage Comparison'));
output.printTable({
columns: [
{ key: 'category', header: 'Category', width: 15 },
{ key: 'before', header: 'Before', width: 12, align: 'right' },
{ key: 'after', header: 'After', width: 12, align: 'right' },
{ key: 'saved', header: 'Saved', width: 12, align: 'right' }
],
data: [
{ category: 'Vectors', before: result.before.vectorsSize, after: result.after.vectorsSize, saved: '-' },
{ category: 'Text', before: result.before.textSize, after: result.after.textSize, saved: '-' },
{ category: 'Patterns', before: result.before.patternsSize, after: result.after.patternsSize, saved: '-' },
{ category: 'Index', before: result.before.indexSize, after: result.after.indexSize, saved: '-' },
{ category: output.bold('Total'), before: result.before.totalSize, after: result.after.totalSize, saved: output.success(result.compression.formattedSaved) }
]
});
output.writeln();
output.printBox([
`Compression Ratio: ${result.compression.ratio.toFixed(2)}x`,
`Space Saved: ${result.compression.formattedSaved}`,
`Quantization: ${result.compression.quantizationApplied ? `Yes (${bits}-bit)` : 'No'}`,
`Index Rebuilt: ${result.compression.indexRebuilt ? 'Yes' : 'No'}`,
`Duration: ${(result.duration / 1000).toFixed(1)}s`
].join('\n'), 'Results');
if (result.performance) {
output.writeln();
output.writeln(output.bold('Performance Impact'));
output.printList([
`Search latency: ${result.performance.searchLatencyBefore.toFixed(2)}ms → ${result.performance.searchLatencyAfter.toFixed(2)}ms`,
`Speedup: ${output.success(result.performance.searchSpeedup)}`
]);
}
return { success: true, data: result };
}
catch (error) {
spinner.fail('Compression failed');
if (error instanceof MCPClientError) {
output.printError(`Compression error: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Export command
const exportCommand = {
name: 'export',
description: 'Export memory to file',
options: [
{
name: 'output',
short: 'o',
description: 'Output file path',
type: 'string',
required: true
},
{
name: 'format',
short: 'f',
description: 'Export format (json, csv, binary)',
type: 'string',
choices: ['json', 'csv', 'binary'],
default: 'json'
},
{
name: 'namespace',
short: 'n',
description: 'Export specific namespace',
type: 'string'
},
{
name: 'include-vectors',
description: 'Include vector embeddings',
type: 'boolean',
default: true
}
],
examples: [
{ command: 'claude-flow memory export -o ./backup.json', description: 'Export all to JSON' },
{ command: 'claude-flow memory export -o ./data.csv -f csv', description: 'Export to CSV' }
],
action: async (ctx) => {
const outputPath = ctx.flags.output;
const format = ctx.flags.format || 'json';
if (!outputPath) {
output.printError('Output path is required. Use --output or -o');
return { success: false, exitCode: 1 };
}
output.printInfo(`Exporting memory to ${outputPath}...`);
try {
const result = await callMCPTool('memory_export', {
outputPath,
format,
namespace: ctx.flags.namespace,
includeVectors: ctx.flags.includeVectors ?? true,
});
output.printSuccess(`Exported to ${result.outputPath}`);
output.printList([
`Entries: ${result.exported.entries}`,
`Vectors: ${result.exported.vectors}`,
`Patterns: ${result.exported.patterns}`,
`File size: ${result.fileSize}`
]);
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Export error: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Import command
const importCommand = {
name: 'import',
description: 'Import memory from file',
options: [
{
name: 'input',
short: 'i',
description: 'Input file path',
type: 'string',
required: true
},
{
name: 'merge',
short: 'm',
description: 'Merge with existing (skip duplicates)',
type: 'boolean',
default: true
},
{
name: 'namespace',
short: 'n',
description: 'Import into specific namespace',
type: 'string'
}
],
examples: [
{ command: 'claude-flow memory import -i ./backup.json', description: 'Import from file' },
{ command: 'claude-flow memory import -i ./data.json -n archive', description: 'Import to namespace' }
],
action: async (ctx) => {
const inputPath = ctx.flags.input || ctx.args[0];
if (!inputPath) {
output.printError('Input path is required. Use --input or -i');
return { success: false, exitCode: 1 };
}
output.printInfo(`Importing memory from ${inputPath}...`);
try {
const result = await callMCPTool('memory_import', {
inputPath,
merge: ctx.flags.merge ?? true,
namespace: ctx.flags.namespace,
});
output.printSuccess(`Imported from ${result.inputPath}`);
output.printList([
`Entries: ${result.imported.entries}`,
`Vectors: ${result.imported.vectors}`,
`Patterns: ${result.imported.patterns}`,
`Skipped (duplicates): ${result.skipped}`,
`Duration: ${result.duration}ms`
]);
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Import error: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Init subcommand - initialize memory database using sql.js
const initMemoryCommand = {
name: 'init',
description: 'Initialize memory database with sql.js (WASM SQLite) - includes ve