@lanonasis/cli
Version:
Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security
1,466 lines • 66.9 kB
JavaScript
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora from 'ora';
import { table } from 'table';
import wrap from 'word-wrap';
import { format } from 'date-fns';
import { MemoryIntelligenceClient } from '@lanonasis/mem-intel-sdk';
import { apiClient } from '../utils/api.js';
import { formatBytes, truncateText } from '../utils/formatting.js';
import { CLIConfig } from '../utils/config.js';
import { createTextInputHandler } from '../ux/index.js';
import * as fs from 'fs/promises';
import { exec as execCb } from 'node:child_process';
import { promisify } from 'node:util';
const exec = promisify(execCb);
const MAX_JSON_OPTION_BYTES = 1024 * 1024; // 1 MiB guardrail for CLI JSON flags
const MEMORY_TYPE_CHOICES = [
'context',
'project',
'knowledge',
'reference',
'personal',
'workflow',
];
const QUERY_SCOPE_CHOICES = [
'personal',
'team',
'organization',
'hybrid',
];
const coerceMemoryType = (value) => {
if (typeof value !== 'string')
return undefined;
const normalized = value.trim().toLowerCase();
// Backward compatibility for older docs/examples.
if (normalized === 'conversation')
return 'context';
if (MEMORY_TYPE_CHOICES.includes(normalized)) {
return normalized;
}
return undefined;
};
const coerceQueryScope = (value) => {
if (typeof value !== 'string')
return undefined;
const normalized = value.trim().toLowerCase();
if (QUERY_SCOPE_CHOICES.includes(normalized)) {
return normalized;
}
return undefined;
};
const parseMemoryTypesOption = (value) => {
if (!value?.trim())
return undefined;
const parsed = value
.split(',')
.map((entry) => coerceMemoryType(entry))
.filter((entry) => Boolean(entry));
if (parsed.length === 0) {
return undefined;
}
return [...new Set(parsed)];
};
const buildIntelligenceContextPayload = (options) => {
const payload = {};
if (options.organizationId?.trim()) {
payload.organization_id = options.organizationId.trim();
}
if (options.topicId?.trim()) {
payload.topic_id = options.topicId.trim();
}
if (options.scope) {
const scope = coerceQueryScope(options.scope);
if (!scope) {
throw new Error(`Invalid scope \"${options.scope}\". Expected one of: ${QUERY_SCOPE_CHOICES.join(', ')}`);
}
payload.query_scope = scope;
}
const memoryTypes = parseMemoryTypesOption(options.memoryTypes);
if (memoryTypes?.length) {
payload.memory_types = memoryTypes;
}
return payload;
};
const resolveInputMode = async () => {
const config = new CLIConfig();
await config.init();
const directMode = config.get('inputMode');
const userPrefs = config.get('userPreferences');
const resolved = (directMode || userPrefs?.inputMode);
return resolved === 'editor' ? 'editor' : 'inline';
};
const collectMemoryContent = async (prompt, inputMode, defaultContent) => {
if (inputMode === 'editor') {
const { content } = await inquirer.prompt([
{
type: 'editor',
name: 'content',
message: prompt,
default: defaultContent,
},
]);
return content;
}
const handler = createTextInputHandler();
return handler.collectMultilineInput(prompt, {
defaultContent,
});
};
const parseJsonOption = (value, fieldName) => {
if (!value)
return undefined;
const payloadSize = Buffer.byteLength(value, 'utf8');
if (payloadSize > MAX_JSON_OPTION_BYTES) {
throw new Error(`${fieldName} JSON payload is too large (${payloadSize} bytes). Max allowed is ${MAX_JSON_OPTION_BYTES} bytes.`);
}
try {
return JSON.parse(value);
}
catch (error) {
const message = error instanceof Error ? error.message : 'Invalid JSON';
throw new Error(`Invalid ${fieldName} JSON: ${message}`);
}
};
const isPlainObject = (value) => {
return typeof value === 'object' && value !== null && !Array.isArray(value);
};
const ensureJsonObject = (value, fieldName) => {
if (value === undefined)
return undefined;
if (!isPlainObject(value)) {
throw new Error(`${fieldName} must be a JSON object`);
}
return value;
};
const ensureBehaviorActions = (value, fieldName, options = {}) => {
if (value === undefined) {
return [];
}
if (!Array.isArray(value)) {
throw new Error(`${fieldName} must be a JSON array`);
}
if (!options.allowEmpty && value.length === 0) {
throw new Error(`${fieldName} must be a non-empty JSON array`);
}
return value.map((entry, index) => {
if (!isPlainObject(entry)) {
throw new Error(`${fieldName}[${index}] must be a JSON object`);
}
const tool = typeof entry.tool === 'string' ? entry.tool.trim() : '';
if (!tool) {
throw new Error(`${fieldName}[${index}].tool is required`);
}
const outcome = typeof entry.outcome === 'string' ? entry.outcome.trim() : '';
if (!['success', 'partial', 'failed'].includes(outcome)) {
throw new Error(`${fieldName}[${index}].outcome must be success, partial, or failed`);
}
const parsed = {
tool,
outcome: outcome,
};
const rawParameters = entry.parameters ?? entry.params;
if (rawParameters !== undefined) {
if (!isPlainObject(rawParameters)) {
throw new Error(`${fieldName}[${index}].parameters must be a JSON object`);
}
parsed.parameters = rawParameters;
}
if (entry.timestamp !== undefined) {
if (typeof entry.timestamp !== 'string' || !entry.timestamp.trim()) {
throw new Error(`${fieldName}[${index}].timestamp must be a non-empty string`);
}
parsed.timestamp = entry.timestamp;
}
if (entry.duration_ms !== undefined) {
if (typeof entry.duration_ms !== 'number' ||
!Number.isFinite(entry.duration_ms) ||
entry.duration_ms < 0) {
throw new Error(`${fieldName}[${index}].duration_ms must be a non-negative number`);
}
parsed.duration_ms = entry.duration_ms;
}
return parsed;
});
};
const ensureCompletedSteps = (value, fieldName) => {
if (value === undefined) {
return [];
}
if (!Array.isArray(value)) {
throw new Error(`${fieldName} must be a JSON array`);
}
return value.map((entry, index) => {
if (typeof entry === 'string') {
const normalized = entry.trim();
if (!normalized) {
throw new Error(`${fieldName}[${index}] must be a non-empty string`);
}
return normalized;
}
if (isPlainObject(entry)) {
const tool = typeof entry.tool === 'string'
? entry.tool?.trim()
: '';
if (!tool) {
throw new Error(`${fieldName}[${index}].tool must be a non-empty string`);
}
return tool;
}
throw new Error(`${fieldName}[${index}] must be a string or an object with a tool field`);
});
};
const clampThreshold = (value) => {
if (!Number.isFinite(value))
return 0.55;
return Math.max(0, Math.min(1, value));
};
const buildSearchThresholdPlan = (requestedThreshold, hasTagFilter) => {
const plan = [];
const pushUnique = (threshold) => {
const normalized = Number(threshold.toFixed(2));
if (!plan.some((item) => Math.abs(item - normalized) < 0.0001)) {
plan.push(normalized);
}
};
pushUnique(requestedThreshold);
if (requestedThreshold > 0.45) {
pushUnique(Math.max(0.45, requestedThreshold - 0.15));
}
if (hasTagFilter) {
// Tag-assisted recall fallback for sparse semantic scores.
pushUnique(0);
}
return plan;
};
const tokenizeSearchQuery = (input) => {
return input
.toLowerCase()
.split(/[^a-z0-9]+/g)
.map((token) => token.trim())
.filter((token) => token.length >= 2);
};
const lexicalSimilarityScore = (query, memory) => {
const tokens = tokenizeSearchQuery(query);
if (tokens.length === 0)
return 0;
const haystack = `${memory.title || ''} ${memory.content || ''} ${(memory.tags || []).join(' ')}`.toLowerCase();
const hits = tokens.filter((token) => haystack.includes(token)).length;
if (hits === 0)
return 0;
const ratio = hits / tokens.length;
return Math.max(0.35, Math.min(0.69, Number((ratio * 0.65).toFixed(3))));
};
const lexicalFallbackSearch = async (query, searchOptions) => {
const candidateLimit = Math.min(Math.max(searchOptions.limit * 8, 50), 200);
const primaryType = searchOptions.memory_types?.length === 1 ? searchOptions.memory_types[0] : undefined;
const memoriesResult = await apiClient.getMemories({
page: 1,
limit: candidateLimit,
memory_type: primaryType,
tags: searchOptions.tags?.join(','),
});
let candidates = (memoriesResult.memories || memoriesResult.data || []);
if (searchOptions.memory_types && searchOptions.memory_types.length > 1) {
const typeSet = new Set(searchOptions.memory_types);
candidates = candidates.filter((memory) => typeSet.has(memory.memory_type));
}
if (searchOptions.tags && searchOptions.tags.length > 0) {
const normalizedTags = new Set(searchOptions.tags.map((tag) => tag.toLowerCase()));
candidates = candidates.filter((memory) => (memory.tags || []).some((tag) => normalizedTags.has(tag.toLowerCase())));
}
return candidates
.map((memory) => ({
...memory,
similarity_score: lexicalSimilarityScore(query, memory),
}))
.filter((memory) => memory.similarity_score > 0)
.sort((a, b) => b.similarity_score - a.similarity_score)
.slice(0, searchOptions.limit);
};
const normalizeSearchFallbackMode = (options) => {
if (options.ci || options.fallback === false) {
return 'never';
}
const rawMode = (options.fallbackMode || 'auto').trim().toLowerCase();
if (rawMode === 'auto' || rawMode === 'lexical' || rawMode === 'never') {
return rawMode;
}
throw new Error('Invalid --fallback-mode. Expected one of: auto, lexical, never');
};
const sanitizeSearchError = (message) => {
return message
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer <redacted>')
.replace(/\b(lano|lms)_[A-Za-z0-9._-]{8,}\b/g, '$1_<redacted>')
.replace(/\b(sk|pk)_(live|test)_[A-Za-z0-9._-]{8,}\b/g, '$1_$2_<redacted>')
.replace(/\b(api[_-]?key|authorization|token)(\s*[:=]\s*)[^\s,;]+/gi, '$1$2<redacted>');
};
const toSearchJsonResult = (memory) => ({
id: memory.id,
title: memory.title,
memory_type: memory.memory_type,
tags: Array.isArray(memory.tags) ? memory.tags : [],
similarity_score: memory.similarity_score,
content_preview: truncateText(memory.content, 160) || '',
});
const resolveCurrentUserId = async () => {
const profile = await apiClient.getUserProfile();
if (!profile?.id) {
throw new Error('Unable to resolve user profile id for intelligence request');
}
return profile.id;
};
const createIntelligenceTransport = async () => {
const config = new CLIConfig();
await config.init();
await config.refreshTokenIfNeeded();
const authToken = config.getToken();
const apiKey = await config.getVendorKeyAsync();
const apiUrl = `${config.getApiUrl().replace(/\/$/, '')}/api/v1`;
const authMethod = config.getAuthMethod();
// When vendor_key is explicitly set, skip JWT even if a token exists in storage.
// JWT validates against auth-gateway's DB; vendor keys validate against mcp-core's DB.
if (authToken && authMethod !== 'vendor_key') {
return {
mode: 'sdk',
client: new MemoryIntelligenceClient({
apiUrl,
authToken,
authType: 'bearer',
allowMissingAuth: false,
}),
};
}
if (apiKey) {
if (apiKey.startsWith('lano_') || apiKey.startsWith('lms_')) {
return {
mode: 'sdk',
client: new MemoryIntelligenceClient({
apiUrl,
apiKey,
authType: 'apiKey',
allowMissingAuth: false,
}),
};
}
// Pre-hashed key (64 hex chars from oauth-client normalizeApiKey): inject via header.
// The edge functions accept pre-hashed keys after the auth.ts shared utility update.
return {
mode: 'sdk',
client: new MemoryIntelligenceClient({
apiUrl,
allowMissingAuth: true,
headers: { 'X-API-Key': apiKey },
}),
};
}
throw new Error('Authentication required. Run "lanonasis auth login" first.');
};
const printIntelligenceResult = (title, payload, options) => {
if (options.json) {
console.log(JSON.stringify(payload, null, 2));
return;
}
console.log(chalk.cyan.bold(`\n${title}`));
console.log(JSON.stringify(payload, null, 2));
};
const postIntelligenceEndpoint = async (transport, endpoint, payload) => {
if (transport.mode === 'sdk') {
if (!transport.client) {
throw new Error('SDK transport is not initialized');
}
const response = await transport.client.getHttpClient().postEnhanced(endpoint, payload);
if (response.error) {
throw new Error(response.error.message || `Request failed for ${endpoint}`);
}
return response.data;
}
return await apiClient.post(`/api/v1${endpoint}`, payload);
};
export function memoryCommands(program) {
// Create memory
program
.command('create')
.alias('add')
.description('Create a new memory entry')
.option('-t, --title <title>', 'memory title')
.option('-c, --content <content>', 'memory content')
.option('--type <type>', `memory type (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--tags <tags>', 'comma-separated tags')
.option('--topic-id <id>', 'topic ID')
.option('-i, --interactive', 'interactive mode')
.option('--json <json>', 'JSON payload (title, content, type/memory_type, tags[], topic_id)')
.option('--content-file <path>', 'Read memory content from a file (overrides --content)')
.action(async (options) => {
try {
let { title, content, type, tags, topicId, interactive, json, contentFile } = options;
// 1) JSON payload (optional)
if (json) {
let parsed;
try {
parsed = JSON.parse(json);
}
catch (err) {
const msg = err instanceof Error ? err.message : 'Invalid JSON';
throw new Error(`Invalid --json payload: ${msg}`);
}
if (!title && typeof parsed.title === 'string')
title = parsed.title;
if (!content && typeof parsed.content === 'string')
content = parsed.content;
const parsedType = parsed.memory_type ?? parsed.type;
if (!type && parsedType !== undefined) {
const coerced = coerceMemoryType(parsedType);
if (!coerced) {
throw new Error(`Invalid memory type in --json payload. Expected one of: ${MEMORY_TYPE_CHOICES.join(', ')}`);
}
type = coerced;
}
if (!tags) {
if (Array.isArray(parsed.tags)) {
tags = parsed.tags.map((t) => String(t)).join(',');
}
else if (typeof parsed.tags === 'string') {
tags = parsed.tags;
}
}
const parsedTopic = parsed.topic_id ?? parsed.topicId;
if (!topicId && typeof parsedTopic === 'string')
topicId = parsedTopic;
}
// 2) Content file (optional)
if (contentFile) {
content = await fs.readFile(contentFile, 'utf-8');
}
if (interactive || (!title || !content)) {
const inputMode = await resolveInputMode();
const answers = await inquirer.prompt([
{
type: 'input',
name: 'title',
message: 'Memory title:',
default: title,
validate: (input) => input.length > 0 || 'Title is required',
},
{
type: 'select',
name: 'type',
message: 'Memory type:',
choices: [...MEMORY_TYPE_CHOICES],
default: type || 'context',
},
{
type: 'input',
name: 'tags',
message: 'Tags (comma-separated):',
default: tags || '',
},
]);
title = answers.title;
type = answers.type;
tags = answers.tags;
const shouldPromptContent = !content || (interactive && inputMode === 'editor');
if (shouldPromptContent) {
content = await collectMemoryContent('Memory content:', inputMode, content);
}
}
if (!title || title.trim().length === 0) {
throw new Error('Title is required');
}
if (!content || content.trim().length === 0) {
throw new Error('Content is required');
}
const resolvedType = type ?? 'context';
const spinner = ora('Creating memory...').start();
const memoryData = {
title,
content,
memory_type: resolvedType
};
if (tags) {
memoryData.tags = tags.split(',').map((tag) => tag.trim()).filter(Boolean);
}
if (topicId) {
memoryData.topic_id = topicId;
}
const memory = await apiClient.createMemory(memoryData);
spinner.succeed('Memory created successfully');
console.log();
console.log(chalk.green('✓ Memory created:'));
console.log(` ID: ${chalk.cyan(memory.id)}`);
console.log(` Title: ${memory.title}`);
console.log(` Type: ${memory.memory_type}`);
if (memory.tags && memory.tags.length > 0) {
console.log(` Tags: ${memory.tags.join(', ')}`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to create memory:'), errorMessage);
process.exit(1);
}
});
// Save current working session context as a memory
program
.command('save-session')
.description('Save current session context (git branch/status + optional test summary) as a memory')
.option('-t, --title <title>', 'memory title', 'Session summary')
.option('--type <type>', `memory type (${MEMORY_TYPE_CHOICES.join(', ')})`, 'project')
.option('--tags <tags>', 'comma-separated tags', 'session,cli')
.option('--test-summary <text>', 'Optional test summary to include')
.action(async (options) => {
try {
const spinner = ora('Collecting session info...').start();
const cwd = process.cwd();
const runGit = async (cmd) => {
try {
const { stdout } = await exec(cmd, { cwd });
return String(stdout || '').trim();
}
catch {
return null;
}
};
const branch = await runGit('git rev-parse --abbrev-ref HEAD');
const status = await runGit('git status --porcelain');
const changedFiles = status
? status
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.replace(/^.. /, ''))
: [];
const lines = [];
lines.push(`# Session Summary`);
lines.push('');
lines.push(`- Date: ${new Date().toISOString()}`);
lines.push(`- CWD: ${cwd}`);
if (branch)
lines.push(`- Git branch: ${branch}`);
lines.push('');
lines.push('## Changes');
if (changedFiles.length === 0) {
lines.push('No uncommitted changes detected (or git not available).');
}
else {
lines.push(changedFiles.map((f) => `- ${f}`).join('\n'));
}
lines.push('');
if (options.testSummary) {
lines.push('## Test Summary');
lines.push(options.testSummary);
lines.push('');
}
spinner.text = 'Saving session memory...';
const resolvedType = coerceMemoryType(options.type) ?? 'project';
const memoryData = {
title: options.title || 'Session summary',
content: lines.join('\n'),
memory_type: resolvedType
};
if (options.tags) {
memoryData.tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
}
const memory = await apiClient.createMemory(memoryData);
spinner.succeed('Session saved');
console.log();
console.log(chalk.green('✓ Memory created:'));
console.log(` ID: ${chalk.cyan(memory.id)}`);
console.log(` Title: ${memory.title}`);
console.log(` Type: ${memory.memory_type}`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to save session:'), errorMessage);
process.exit(1);
}
});
// Session management helpers (sessions are stored as memory entries tagged `session,cli` by default)
program
.command('list-sessions')
.description('List saved CLI sessions (memories tagged session,cli by default)')
.option('-p, --page <page>', 'page number', '1')
.option('-l, --limit <limit>', 'number of entries per page', '20')
.option('--type <type>', 'filter by memory type', 'project')
.option('--tags <tags>', 'filter by tags (comma-separated)', 'session,cli')
.option('--sort <field>', 'sort by field (created_at, updated_at, title, last_accessed)', 'created_at')
.option('--order <order>', 'sort order (asc, desc)', 'desc')
.action(async (options) => {
try {
const spinner = ora('Fetching sessions...').start();
const params = {
page: parseInt(options.page || '1'),
limit: parseInt(options.limit || '20'),
sort: options.sort || 'created_at',
order: options.order || 'desc'
};
if (options.type)
params.memory_type = options.type;
if (options.tags)
params.tags = options.tags;
const result = await apiClient.getMemories(params);
spinner.stop();
const memories = result.memories || result.data || [];
if (memories.length === 0) {
console.log(chalk.yellow('No sessions found'));
return;
}
console.log(chalk.blue.bold(`\n📚 Sessions (${result.pagination.total} total)`));
console.log(chalk.gray(`Page ${result.pagination.page || 1} of ${result.pagination.pages || Math.ceil(result.pagination.total / result.pagination.limit)}`));
console.log();
const outputFormat = process.env.CLI_OUTPUT_FORMAT || 'table';
if (outputFormat === 'json') {
console.log(JSON.stringify(result, null, 2));
return;
}
const tableData = memories.map((memory) => [
truncateText(memory.title, 30),
memory.memory_type,
memory.tags.slice(0, 3).join(', '),
format(new Date(memory.created_at), 'MMM dd, yyyy'),
memory.access_count
]);
const tableConfig = {
header: ['Title', 'Type', 'Tags', 'Created', 'Access'],
columnDefault: {
width: 20,
wrapWord: true
},
columns: [
{ width: 30 },
{ width: 12 },
{ width: 20 },
{ width: 12 },
{ width: 8 }
]
};
console.log(table([tableConfig.header, ...tableData], {
columnDefault: tableConfig.columnDefault,
columns: tableConfig.columns
}));
if (result.pagination.pages > 1) {
console.log(chalk.gray(`\nUse --page ${result.pagination.page + 1} for next page`));
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to list sessions:'), errorMessage);
process.exit(1);
}
});
program
.command('load-session')
.description('Load a saved session by memory ID (prints the saved session context)')
.argument('<id>', 'session memory ID')
.action(async (id) => {
try {
const spinner = ora('Loading session...').start();
const memory = await apiClient.getMemory(id);
spinner.stop();
const outputFormat = process.env.CLI_OUTPUT_FORMAT || 'text';
if (outputFormat === 'json') {
console.log(JSON.stringify(memory, null, 2));
return;
}
console.log(chalk.blue.bold('\n📌 Session'));
console.log(chalk.gray(`${memory.title} (${memory.id})`));
console.log();
console.log(memory.content);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to load session:'), errorMessage);
process.exit(1);
}
});
program
.command('delete-session')
.description('Delete a saved session by memory ID')
.argument('<id>', 'session memory ID')
.option('-f, --force', 'skip confirmation')
.action(async (id, options) => {
try {
if (!options.force) {
const memory = await apiClient.getMemory(id);
const answer = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete session "${memory.title}"?`,
default: false
}
]);
if (!answer.confirm) {
console.log(chalk.yellow('Deletion cancelled'));
return;
}
}
const spinner = ora('Deleting session...').start();
await apiClient.deleteMemory(id);
spinner.succeed('Session deleted successfully');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to delete session:'), errorMessage);
process.exit(1);
}
});
// List memories
program
.command('list')
.alias('ls')
.description('List memory entries')
.option('-p, --page <page>', 'page number', '1')
.option('-l, --limit <limit>', 'number of entries per page', '20')
.option('--type <type>', 'filter by memory type')
.option('--tags <tags>', 'filter by tags (comma-separated)')
.option('--user-id <id>', 'filter by user ID (admin only)')
.option('--sort <field>', 'sort by field (created_at, updated_at, title, last_accessed)', 'created_at')
.option('--order <order>', 'sort order (asc, desc)', 'desc')
.action(async (options) => {
try {
const spinner = ora('Fetching memories...').start();
const params = {
page: parseInt(options.page || '1'),
limit: parseInt(options.limit || '20'),
sort: options.sort || 'created_at',
order: options.order || 'desc'
};
if (options.type)
params.memory_type = options.type;
if (options.tags)
params.tags = options.tags;
if (options.userId)
params.user_id = options.userId;
const result = await apiClient.getMemories(params);
spinner.stop();
const memories = result.memories || result.data || [];
if (memories.length === 0) {
console.log(chalk.yellow('No memories found'));
return;
}
console.log(chalk.blue.bold(`\n📚 Memories (${result.pagination.total} total)`));
console.log(chalk.gray(`Page ${result.pagination.page || 1} of ${result.pagination.pages || Math.ceil(result.pagination.total / result.pagination.limit)}`));
console.log();
const outputFormat = process.env.CLI_OUTPUT_FORMAT || 'table';
if (outputFormat === 'json') {
console.log(JSON.stringify(result, null, 2));
}
else {
// Table format
const tableData = memories.map((memory) => [
truncateText(memory.title, 30),
memory.memory_type,
memory.tags.slice(0, 3).join(', '),
format(new Date(memory.created_at), 'MMM dd, yyyy'),
memory.access_count
]);
const tableConfig = {
header: ['Title', 'Type', 'Tags', 'Created', 'Access'],
columnDefault: {
width: 20,
wrapWord: true
},
columns: [
{ width: 30 },
{ width: 12 },
{ width: 20 },
{ width: 12 },
{ width: 8 }
]
};
console.log(table([tableConfig.header, ...tableData], {
columnDefault: tableConfig.columnDefault,
columns: tableConfig.columns
}));
// Pagination info
if (result.pagination.pages > 1) {
console.log(chalk.gray(`\nUse --page ${result.pagination.page + 1} for next page`));
}
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to list memories:'), errorMessage);
process.exit(1);
}
});
// Search memories
program
.command('search')
.description('Search memories using semantic search')
.argument('<query...>', 'search query')
.option('-l, --limit <limit>', 'number of results', '20')
.option('--threshold <threshold>', 'similarity threshold (0-1)', '0.55')
.option('--type <types>', 'filter by memory types (comma-separated)')
.option('--tags <tags>', 'filter by tags (comma-separated)')
.option('--fallback-mode <mode>', 'fallback mode when semantic search returns no results or fails (auto, lexical, never)', 'auto')
.option('--no-fallback', 'disable CLI lexical fallback when semantic search fails or returns no results')
.option('--fail-on-fallback', 'exit non-zero if CLI lexical fallback is used')
.option('--ci', 'CI mode: disable fallback, emit JSON, and fail on backend search errors')
.option('--json', 'emit machine-readable JSON output')
.action(async (queryParts, options) => {
try {
const query = Array.isArray(queryParts) ? queryParts.join(' ').trim() : String(queryParts || '').trim();
if (!query) {
console.error(chalk.red('✖ Search query is required'));
process.exit(1);
return;
}
const startedAt = Date.now();
const fallbackMode = normalizeSearchFallbackMode(options);
const jsonOutput = Boolean(options.json || options.ci);
const requestedThreshold = clampThreshold(parseFloat(options.threshold || '0.55'));
const searchOptions = {
limit: parseInt(options.limit || '20'),
threshold: requestedThreshold
};
if (options.type) {
searchOptions.memory_types = options.type.split(',').map((t) => t.trim());
}
if (options.tags) {
searchOptions.tags = options.tags.split(',').map((t) => t.trim());
}
const thresholdPlan = buildSearchThresholdPlan(requestedThreshold, Boolean(searchOptions.tags?.length));
let result = null;
let results = [];
let thresholdUsed = requestedThreshold;
let searchStrategy = 'semantic';
let semanticSearchError = null;
let fallbackUsed = false;
const spinner = jsonOutput ? null : ora(`Searching for "${query}"...`).start();
for (const threshold of thresholdPlan) {
try {
const attempt = await apiClient.searchMemories(query, {
...searchOptions,
threshold,
});
const rawAttemptResults = (attempt.results || attempt.data || []);
// The search endpoint wraps each row as `{ memory: <entry>, similarity_score, rank }`
// instead of returning memory fields flat like get/list/update do. Unwrap so
// title/content/id survive, and map the search row's `type` to `memory_type`
// (the field name every other memory command uses).
const attemptResults = rawAttemptResults.map((raw) => {
if (raw && typeof raw === 'object') {
const row = raw;
const inner = row.memory ?? row.data;
if (inner && typeof inner === 'object' && typeof inner.id === 'string') {
const innerObj = inner;
return {
...innerObj,
memory_type: innerObj.memory_type ?? innerObj.type,
similarity_score: row.similarity_score ?? innerObj.similarity_score,
};
}
}
return raw;
});
result = attempt;
if (attemptResults.length > 0) {
results = attemptResults;
thresholdUsed = threshold;
const attemptStrategy = attempt.search_strategy;
searchStrategy = typeof attemptStrategy === 'string'
? attemptStrategy
: 'semantic';
break;
}
}
catch (error) {
semanticSearchError = sanitizeSearchError(error instanceof Error ? error.message : 'Unknown semantic search error');
break;
}
}
if (results.length === 0 && fallbackMode !== 'never') {
const lexicalResults = await lexicalFallbackSearch(query, searchOptions);
if (lexicalResults.length > 0) {
results = lexicalResults;
searchStrategy = 'cli_lexical_fallback';
fallbackUsed = true;
}
}
if (semanticSearchError && fallbackMode === 'never') {
searchStrategy = 'semantic_failed';
}
spinner?.stop();
const totalResults = typeof result?.total_results === 'number' ? result.total_results : results.length;
const searchTimeMs = typeof result?.search_time_ms === 'number' ? result.search_time_ms : Date.now() - startedAt;
const jsonPayload = {
query,
total_results: totalResults,
results: results.map(toSearchJsonResult),
search_strategy: searchStrategy,
fallback_used: fallbackUsed,
fallback_mode: fallbackMode,
semantic_error: semanticSearchError,
threshold_requested: requestedThreshold,
threshold_used: thresholdUsed,
threshold_plan: thresholdPlan,
search_time_ms: searchTimeMs,
};
if (results.length === 0) {
if (jsonOutput) {
console.log(JSON.stringify(jsonPayload, null, 2));
}
else {
console.log(chalk.yellow('No memories found matching your search'));
if (semanticSearchError) {
const fallbackHint = fallbackMode === 'never'
? 'Lexical fallback is disabled for this run.'
: 'Lexical fallback did not find local matches.';
console.log(chalk.gray(`Semantic search error: ${semanticSearchError}`));
console.log(chalk.gray(fallbackHint));
}
console.log(chalk.gray(`Tried thresholds: ${thresholdPlan.map((t) => t.toFixed(2)).join(', ')}`));
}
if (semanticSearchError && fallbackMode === 'never') {
process.exit(1);
}
return;
}
if (jsonOutput) {
console.log(JSON.stringify(jsonPayload, null, 2));
if ((options.failOnFallback || options.ci) && fallbackUsed) {
process.exit(1);
}
return;
}
if (semanticSearchError && fallbackUsed) {
console.log(chalk.yellow(`⚠ Semantic search unavailable, using lexical fallback (${semanticSearchError})`));
}
else if (fallbackUsed) {
console.log(chalk.yellow('⚠ Semantic search returned no matches; using CLI lexical fallback'));
}
console.log(chalk.blue.bold(`\n🔍 Search Results (${totalResults} found)`));
console.log(chalk.gray(`Query: "${query}" | Search time: ${searchTimeMs}ms`));
if (Math.abs(thresholdUsed - requestedThreshold) > 0.0001) {
console.log(chalk.gray(`No matches at ${requestedThreshold.toFixed(2)}; used adaptive threshold ${thresholdUsed.toFixed(2)}`));
}
if (searchStrategy) {
console.log(chalk.gray(`Search strategy: ${searchStrategy}`));
}
console.log();
results.forEach((memory, index) => {
const title = truncateText(memory.title, 120) || '(untitled memory)';
const contentPreview = truncateText(memory.content, 100) || '(no content preview)';
const scoreValue = typeof memory.similarity_score === 'number' ? memory.similarity_score : 0;
const score = (scoreValue * 100).toFixed(1);
const memoryId = memory.id || '(missing id)';
const memoryType = memory.memory_type || 'unknown';
const tags = Array.isArray(memory.tags) ? memory.tags : [];
console.log(chalk.green(`${index + 1}. ${title}`) + chalk.gray(` (${score}% match)`));
console.log(chalk.white(` ${contentPreview}`));
console.log(chalk.cyan(` ID: ${memoryId}`) + chalk.gray(` | Type: ${memoryType}`));
if (tags.length > 0) {
console.log(chalk.yellow(` Tags: ${tags.join(', ')}`));
}
console.log();
});
if ((options.failOnFallback || options.ci) && fallbackUsed) {
process.exit(1);
}
}
catch (error) {
const errorMessage = sanitizeSearchError(error instanceof Error ? error.message : 'Unknown error');
console.error(chalk.red('✖ Search failed:'), errorMessage);
process.exit(1);
}
});
// Get memory details
program
.command('get')
.alias('show')
.description('Get detailed information about a memory')
.argument('<id>', 'memory ID')
.action(async (id) => {
try {
const spinner = ora('Fetching memory...').start();
const memory = await apiClient.getMemory(id);
spinner.stop();
console.log(chalk.blue.bold('\n📄 Memory Details'));
console.log();
console.log(chalk.green('Title:'), memory.title);
console.log(chalk.green('ID:'), chalk.cyan(memory.id));
console.log(chalk.green('Type:'), memory.memory_type);
console.log(chalk.green('Created:'), format(new Date(memory.created_at), 'PPpp'));
console.log(chalk.green('Updated:'), format(new Date(memory.updated_at), 'PPpp'));
if (memory.last_accessed) {
console.log(chalk.green('Last Accessed:'), format(new Date(memory.last_accessed), 'PPpp'));
}
console.log(chalk.green('Access Count:'), memory.access_count);
if (memory.tags && memory.tags.length > 0) {
console.log(chalk.green('Tags:'), memory.tags.join(', '));
}
if (memory.topic_id) {
console.log(chalk.green('Topic ID:'), memory.topic_id);
}
console.log();
console.log(chalk.green('Content:'));
console.log(wrap(memory.content, { width: 80, indent: ' ' }));
if (memory.metadata && Object.keys(memory.metadata).length > 0) {
console.log();
console.log(chalk.green('Metadata:'));
console.log(JSON.stringify(memory.metadata, null, 2));
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to get memory:'), errorMessage);
process.exit(1);
}
});
// Update memory
program
.command('update')
.description('Update a memory entry')
.argument('<id>', 'memory ID')
.option('-t, --title <title>', 'new title')
.option('-c, --content <content>', 'new content')
.option('--type <type>', `new memory type (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--tags <tags>', 'new tags (comma-separated)')
.option('-i, --interactive', 'interactive mode')
.action(async (id, options) => {
try {
let updateData = {};
if (options.interactive) {
// First, get current memory data
const spinner = ora('Fetching current memory...').start();
const currentMemory = await apiClient.getMemory(id);
spinner.stop();
const inputMode = await resolveInputMode();
const answers = await inquirer.prompt([
{
type: 'input',
name: 'title',
message: 'Title:',
default: currentMemory.title,
},
{
type: 'select',
name: 'type',
message: 'Memory type:',
choices: [...MEMORY_TYPE_CHOICES],
default: currentMemory.memory_type,
},
{
type: 'input',
name: 'tags',
message: 'Tags (comma-separated):',
default: currentMemory.tags?.join(', ') || '',
},
]);
const content = await collectMemoryContent('Content:', inputMode, currentMemory.content);
updateData = {
title: answers.title,
content,
memory_type: answers.type,
tags: answers.tags.split(',').map((tag) => tag.trim()).filter(Boolean)
};
}
else {
if (options.title)
updateData.title = options.title;
if (options.content)
updateData.content = options.content;
if (options.type)
updateData.memory_type = options.type;
if (options.tags) {
updateData.tags = options.tags.split(',').map((tag) => tag.trim()).filter(Boolean);
}
}
if (Object.keys(updateData).length === 0) {
console.log(chalk.yellow('No updates specified'));
return;
}
const spinner = ora('Updating memory...').start();
const memory = await apiClient.updateMemory(id, updateData);
spinner.succeed('Memory updated successfully');
console.log();
console.log(chalk.green('✓ Memory updated:'));
console.log(` ID: ${chalk.cyan(memory.id)}`);
console.log(` Title: ${memory.title}`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to update memory:'), errorMessage);
process.exit(1);
}
});
// Delete memory
program
.command('delete')
.alias('rm')
.description('Delete a memory entry')
.argument('<id>', 'memory ID')
.option('-f, --force', 'skip confirmation')
.action(async (id, options) => {
try {
if (!options.force) {
const memory = await apiClient.getMemory(id);
const answer = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete "${memory.title}"?`,
default: false
}
]);
if (!answer.confirm) {
console.log(chalk.yellow('Deletion cancelled'));
return;
}
}
const spinner = ora('Deleting memory...').start();
await apiClient.deleteMemory(id);
spinner.succeed('Memory deleted successfully');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to delete memory:'), errorMessage);
process.exit(1);
}
});
// Memory statistics
program
.command('stats')
.description('Show memory statistics (admin only)')
.action(async () => {
try {
const spinner = ora('Fetching statistics...').start();
const stats = await apiClient.getMemoryStats();
spinner.stop();
console.log(chalk.blue.bold('\n📊 Memory Statistics'));
console.log();
console.log(chalk.green('Total Memories:'), stats.total_memories.toLocaleString());
console.log(chalk.green('Total Size:'), formatBytes(stats.total_size_bytes));
console.log(chalk.green('Average Access Count:'), stats.avg_access_count);
console.log();
console.log(chalk.yellow('Memories by Type:'));
Object.entries(stats.memories_by_type).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
if (stats.most_accessed_memory) {
console.log();
console.log(chalk.yellow('Most Accessed Memory:'));
console.log(` ${stats.most_accessed_memory.title} (${stats.most_accessed_memory.access_count} times)`);
}
if (stats.recent_memories.length > 0) {
console.log();
console.log(chalk.yellow('Recent Memories:'));
stats.recent_memories.forEach((memory, index) => {
console.log(` ${index + 1}. ${truncateText(memory.title, 50)}`);
});
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to get statistics:'), errorMessage);
process.exit(1);
}
});
// Intelligence commands powered by @lanonasis/mem-intel-sdk
const intelligence = program
.command('intelligence')
.description('Memory intelligence operations');
intelligence
.command('health-check')
.description('Run memory intelligence health check')
.option('--organization-id <id>', 'Optional organization context')
.option('--topic-id <id>', 'Optional topic context')
.option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
.option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--json', 'Output raw JSON payload')
.action(async (options) => {
try {
const spinner = ora('Running intelligence health check...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const result = await postIntelligenceEndpoint(transport, '/intelligence/health-check', {
user_id: userId,
response_format: 'json',
...buildIntelligenceContextPayload(options),
});
spinner.stop();
printIntelligenceResult('🩺 Intelligence Health Check', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Intelligence health check failed:'), errorMessage);
process.exit(1);
}
});
intelligence
.command('suggest-tags')
.description('Suggest tags for a memory')
.argument('<memory-id>', 'Memory ID')
.option('--max <number>', 'Maximum suggestions', '8')
.option('--organization-id <id>', 'Optional organization context')
.option('--topic-id <id>', 'Optional topic context')
.option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
.option('--json', 'Output raw JSON payload')
.action(async (memoryId, options) => {
try {
const spinner = ora('Generating tag suggestions...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const maxSuggestions = Math.max(1, Math.min(20, parseInt(options.max || '8', 10)));
const result = await postIntelligenceEndpoint(transport, '/intelligence/suggest-tags', {
memory_id: memoryId,
user_id: userId,
max_suggestions: maxSuggestions,
include_existing_tags: true,
response_format: 'json',
...buildIntelligenceContextPayload(options),
});
spinner.stop();
printIntelligenceResult('🏷️ Tag Suggestions', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to suggest tags:'), errorMessage);
process.exit(1);
}
});
intelligence
.command('find-related')
.description('Find memories related to a source memory')
.argument('<memory-id>', 'Source memory ID')
.option('--limit <number>', 'Maximum related memories', '5')
.option('--threshold <number>', 'Similarity threshold (0-1)', '0.7')
.option('--organization-id <id>', 'Optional organization context')
.option('--topic-id <id>', 'Optional topic context')
.option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
.option('--memory-types <types>', `Optional comma-separated candidate memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--json', 'Output raw JSON payload')
.action(async (memoryId, options) => {
try {
const spinner = ora('Finding related memories...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const result = await postIntelligenceEndpoint(transport, '/intelligence/find-related', {
memory_id: memoryId,
user_id: userId,
limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
response_format: 'json',
...buildIntelligenceContextPayload(options),
});
spinner.stop();
printIntelligenceResult('🔗 Related Memories', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to find related memories:'), errorMessage);
process.exit(1);
}
});
intelligence
.command('detect-duplicates')
.description('Detect duplicate memory entries')
.option('--threshold <number>', 'Similarity threshold (0-1)', '0.88')
.option('--max-pairs <number>', 'Maximum duplicate pairs to inspect', '100')
.option('--organization-id <id>', 'Optional organization context')
.option('--topic-id <id>', 'Optional topic context')
.option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
.option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--json', 'Output raw JSON payload')
.action(async (options) => {
try {
const spinner = ora('Detecting duplicates...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const result = await postIntelligenceEndpoint(transport, '/intelligence/detect-duplicates', {
user_id: userId,
similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.88'))),
max_pairs: Math.max(10, Math.min(500, parseInt(options.maxPairs || '100', 10))),
response_format: 'json',
...buildIntelligenceContextPayload(options),
});
spinner.stop();
printIntelligenceResult('🧬 Duplicate Detection', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to detect duplicates:'), errorMessage);
process.exit(1);
}
});
intelligence
.command('extract-insights')
.description('Extract insights from memory collection')
.option('--topic <topic>', 'Optional topic filter')
.option('--type <type>', `Optional memory type filter (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--max-memories <number>', 'Maximum memories to analyze', '50')
.option('--organization-id <id>', 'Optional organization context')
.option('--topic-id <id>', 'Optional topic context')
.option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
.option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--json', 'Output raw JSON payload')
.action(async (options) => {
try {
const spinner = ora('Extracting insights...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const memoryType = options.type ? coerceMemoryType(options.type) : undefined;
if (options.type && !memoryType) {
throw new Error(`Invalid type "${options.type}". Expected one of: ${MEMORY_TYPE_CHOICES.join(', ')}`);
}
const result = await postIntelligenceEndpoint(transport, '/intelligence/extract-insights', {
user_id: userId,
topic: options.topic,
memory_type: memoryType,
max_memories: Math.max(5, Math.min(200, parseInt(options.maxMemories || '50', 10))),
response_format: 'json',
...buildIntelligenceContextPayload(options),
});
spinner.stop();
printIntelligenceResult('💡 Memory Insights', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to extract insights:'), errorMessage);
process.exit(1);
}
});
intelligence
.command('analyze-patterns')
.description('Analyze memory usage patterns')
.option('--days <number>', 'Days to include in analysis', '30')
.option('--organization-id <id>', 'Optional organization context')
.option('--topic-id <id>', 'Optional topic context')
.option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
.option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
.option('--json', 'Output raw JSON payload')
.action(async (options) => {
try {
const spinner = ora('Analyzing memory patterns...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const result = await postIntelligenceEndpoint(transport, '/intelligence/analyze-patterns', {
user_id: userId,
time_range_days: Math.max(1, Math.min(365, parseInt(options.days || '30', 10))),
response_format: 'json',
...buildIntelligenceContextPayload(options),
});
spinner.stop();
printIntelligenceResult('📈 Pattern Analysis', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to analyze patterns:'), errorMessage);
process.exit(1);
}
});
// Behavior commands powered by @lanonasis/mem-intel-sdk
const behavior = program
.command('behavior')
.description('Behavior pattern intelligence operations');
behavior
.command('record')
.description('Record a behavior pattern from successful workflow steps')
.requiredOption('--trigger <text>', 'Behavior trigger description')
.requiredOption('--final-outcome <result>', 'Final outcome: success | partial | failed')
.requiredOption('--actions <json>', 'Actions JSON array. Example: [{"tool":"memory.search","params":{"query":"auth fix"}}]')
.option('--context <json>', 'Context JSON object')
.option('--confidence <number>', 'Confidence score (0-1)', '0.7')
.option('--json', 'Output raw JSON payload')
.action(async (options) => {
try {
const spinner = ora('Recording behavior pattern...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const parsedActions = parseJsonOption(options.actions, '--actions');
const actions = ensureBehaviorActions(parsedActions, '--actions');
const parsedContext = parseJsonOption(options.context, '--context');
const context = ensureJsonObject(parsedContext, '--context');
const finalOutcome = options.finalOutcome;
if (!['success', 'partial', 'failed'].includes(finalOutcome)) {
throw new Error('--final-outcome must be one of: success, partial, failed');
}
const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-record', {
user_id: userId,
trigger: options.trigger,
context: context || {},
actions,
final_outcome: finalOutcome,
confidence: Math.max(0, Math.min(1, parseFloat(options.confidence || '0.7'))),
});
spinner.stop();
printIntelligenceResult('🧠 Behavior Recorded', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to record behavior pattern:'), errorMessage);
process.exit(1);
}
});
behavior
.command('recall')
.description('Recall behavior patterns relevant to the current task')
.requiredOption('--task <text>', 'Current task description')
.option('--context <json>', 'Additional context JSON object')
.option('--limit <number>', 'Maximum patterns to return', '5')
.option('--threshold <number>', 'Similarity threshold (0-1)', '0.7')
.option('--json', 'Output raw JSON payload')
.action(async (options) => {
try {
const spinner = ora('Recalling behavior patterns...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const parsedContext = parseJsonOption(options.context, '--context');
const context = ensureJsonObject(parsedContext, '--context') || {};
const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-recall', {
user_id: userId,
context: {
...context,
current_task: options.task,
},
limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
});
spinner.stop();
printIntelligenceResult('🔁 Behavior Recall', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to recall behavior patterns:'), errorMessage);
process.exit(1);
}
});
behavior
.command('suggest')
.description('Suggest next actions from learned behavior patterns')
.requiredOption('--task <text>', 'Current task description')
.option('--state <json>', 'Additional current state JSON object')
.option('--completed-steps <json>', 'Completed steps JSON array (strings preferred)')
.option('--max-suggestions <number>', 'Maximum suggestions', '3')
.option('--json', 'Output raw JSON payload')
.action(async (options) => {
try {
const spinner = ora('Generating behavior suggestions...').start();
const transport = await createIntelligenceTransport();
const userId = await resolveCurrentUserId();
const parsedState = parseJsonOption(options.state, '--state');
const state = ensureJsonObject(parsedState, '--state') || {};
const parsedCompletedSteps = parseJsonOption(options.completedSteps, '--completed-steps');
const completedSteps = ensureCompletedSteps(parsedCompletedSteps, '--completed-steps');
const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-suggest', {
user_id: userId,
current_state: {
...state,
task_description: options.task,
completed_steps: completedSteps,
},
max_suggestions: Math.max(1, Math.min(10, parseInt(options.maxSuggestions || '3', 10))),
});
spinner.stop();
printIntelligenceResult('🎯 Behavior Suggestions', result, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(chalk.red('✖ Failed to suggest actions:'), errorMessage);
process.exit(1);
}
});
}