@lanonasis/cli 3.9.7 → 3.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import ora from 'ora';
4
4
  import { table } from 'table';
5
5
  import wrap from 'word-wrap';
6
6
  import { format } from 'date-fns';
7
+ import { MemoryIntelligenceClient } from '@lanonasis/mem-intel-sdk';
7
8
  import { apiClient } from '../utils/api.js';
8
9
  import { formatBytes, truncateText } from '../utils/formatting.js';
9
10
  import { CLIConfig } from '../utils/config.js';
@@ -12,6 +13,7 @@ import * as fs from 'fs/promises';
12
13
  import { exec as execCb } from 'node:child_process';
13
14
  import { promisify } from 'node:util';
14
15
  const exec = promisify(execCb);
16
+ const MAX_JSON_OPTION_BYTES = 1024 * 1024; // 1 MiB guardrail for CLI JSON flags
15
17
  const MEMORY_TYPE_CHOICES = [
16
18
  'context',
17
19
  'project',
@@ -57,6 +59,208 @@ const collectMemoryContent = async (prompt, inputMode, defaultContent) => {
57
59
  defaultContent,
58
60
  });
59
61
  };
62
+ const parseJsonOption = (value, fieldName) => {
63
+ if (!value)
64
+ return undefined;
65
+ const payloadSize = Buffer.byteLength(value, 'utf8');
66
+ if (payloadSize > MAX_JSON_OPTION_BYTES) {
67
+ throw new Error(`${fieldName} JSON payload is too large (${payloadSize} bytes). Max allowed is ${MAX_JSON_OPTION_BYTES} bytes.`);
68
+ }
69
+ try {
70
+ return JSON.parse(value);
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof Error ? error.message : 'Invalid JSON';
74
+ throw new Error(`Invalid ${fieldName} JSON: ${message}`);
75
+ }
76
+ };
77
+ const isPlainObject = (value) => {
78
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
79
+ };
80
+ const ensureJsonObject = (value, fieldName) => {
81
+ if (value === undefined)
82
+ return undefined;
83
+ if (!isPlainObject(value)) {
84
+ throw new Error(`${fieldName} must be a JSON object`);
85
+ }
86
+ return value;
87
+ };
88
+ const ensureBehaviorActions = (value, fieldName, options = {}) => {
89
+ if (value === undefined) {
90
+ return [];
91
+ }
92
+ if (!Array.isArray(value)) {
93
+ throw new Error(`${fieldName} must be a JSON array`);
94
+ }
95
+ if (!options.allowEmpty && value.length === 0) {
96
+ throw new Error(`${fieldName} must be a non-empty JSON array`);
97
+ }
98
+ return value.map((entry, index) => {
99
+ if (!isPlainObject(entry)) {
100
+ throw new Error(`${fieldName}[${index}] must be a JSON object`);
101
+ }
102
+ const tool = typeof entry.tool === 'string' ? entry.tool.trim() : '';
103
+ if (!tool) {
104
+ throw new Error(`${fieldName}[${index}].tool is required`);
105
+ }
106
+ const parsed = { tool };
107
+ if (entry.params !== undefined) {
108
+ if (!isPlainObject(entry.params)) {
109
+ throw new Error(`${fieldName}[${index}].params must be a JSON object`);
110
+ }
111
+ parsed.params = entry.params;
112
+ }
113
+ if (entry.timestamp !== undefined) {
114
+ if (typeof entry.timestamp !== 'string' || !entry.timestamp.trim()) {
115
+ throw new Error(`${fieldName}[${index}].timestamp must be a non-empty string`);
116
+ }
117
+ parsed.timestamp = entry.timestamp;
118
+ }
119
+ if (entry.duration_ms !== undefined) {
120
+ if (typeof entry.duration_ms !== 'number' ||
121
+ !Number.isFinite(entry.duration_ms) ||
122
+ entry.duration_ms < 0) {
123
+ throw new Error(`${fieldName}[${index}].duration_ms must be a non-negative number`);
124
+ }
125
+ parsed.duration_ms = entry.duration_ms;
126
+ }
127
+ return parsed;
128
+ });
129
+ };
130
+ const clampThreshold = (value) => {
131
+ if (!Number.isFinite(value))
132
+ return 0.55;
133
+ return Math.max(0, Math.min(1, value));
134
+ };
135
+ const buildSearchThresholdPlan = (requestedThreshold, hasTagFilter) => {
136
+ const plan = [];
137
+ const pushUnique = (threshold) => {
138
+ const normalized = Number(threshold.toFixed(2));
139
+ if (!plan.some((item) => Math.abs(item - normalized) < 0.0001)) {
140
+ plan.push(normalized);
141
+ }
142
+ };
143
+ pushUnique(requestedThreshold);
144
+ if (requestedThreshold > 0.45) {
145
+ pushUnique(Math.max(0.45, requestedThreshold - 0.15));
146
+ }
147
+ if (hasTagFilter) {
148
+ // Tag-assisted recall fallback for sparse semantic scores.
149
+ pushUnique(0);
150
+ }
151
+ return plan;
152
+ };
153
+ const tokenizeSearchQuery = (input) => {
154
+ return input
155
+ .toLowerCase()
156
+ .split(/[^a-z0-9]+/g)
157
+ .map((token) => token.trim())
158
+ .filter((token) => token.length >= 2);
159
+ };
160
+ const lexicalSimilarityScore = (query, memory) => {
161
+ const tokens = tokenizeSearchQuery(query);
162
+ if (tokens.length === 0)
163
+ return 0;
164
+ const haystack = `${memory.title || ''} ${memory.content || ''} ${(memory.tags || []).join(' ')}`.toLowerCase();
165
+ const hits = tokens.filter((token) => haystack.includes(token)).length;
166
+ if (hits === 0)
167
+ return 0;
168
+ const ratio = hits / tokens.length;
169
+ return Math.max(0.35, Math.min(0.69, Number((ratio * 0.65).toFixed(3))));
170
+ };
171
+ const lexicalFallbackSearch = async (query, searchOptions) => {
172
+ const candidateLimit = Math.min(Math.max(searchOptions.limit * 8, 50), 200);
173
+ const primaryType = searchOptions.memory_types?.length === 1 ? searchOptions.memory_types[0] : undefined;
174
+ const memoriesResult = await apiClient.getMemories({
175
+ page: 1,
176
+ limit: candidateLimit,
177
+ memory_type: primaryType,
178
+ tags: searchOptions.tags?.join(','),
179
+ });
180
+ let candidates = (memoriesResult.memories || memoriesResult.data || []);
181
+ if (searchOptions.memory_types && searchOptions.memory_types.length > 1) {
182
+ const typeSet = new Set(searchOptions.memory_types);
183
+ candidates = candidates.filter((memory) => typeSet.has(memory.memory_type));
184
+ }
185
+ if (searchOptions.tags && searchOptions.tags.length > 0) {
186
+ const normalizedTags = new Set(searchOptions.tags.map((tag) => tag.toLowerCase()));
187
+ candidates = candidates.filter((memory) => (memory.tags || []).some((tag) => normalizedTags.has(tag.toLowerCase())));
188
+ }
189
+ return candidates
190
+ .map((memory) => ({
191
+ ...memory,
192
+ similarity_score: lexicalSimilarityScore(query, memory),
193
+ }))
194
+ .filter((memory) => memory.similarity_score > 0)
195
+ .sort((a, b) => b.similarity_score - a.similarity_score)
196
+ .slice(0, searchOptions.limit);
197
+ };
198
+ const resolveCurrentUserId = async () => {
199
+ const profile = await apiClient.getUserProfile();
200
+ if (!profile?.id) {
201
+ throw new Error('Unable to resolve user profile id for intelligence request');
202
+ }
203
+ return profile.id;
204
+ };
205
+ const createIntelligenceTransport = async () => {
206
+ const config = new CLIConfig();
207
+ await config.init();
208
+ await config.refreshTokenIfNeeded();
209
+ const authToken = config.getToken();
210
+ const apiKey = await config.getVendorKeyAsync();
211
+ const apiUrl = `${config.getApiUrl().replace(/\/$/, '')}/api/v1`;
212
+ const authMethod = config.getAuthMethod();
213
+ // When vendor_key is explicitly set, skip JWT even if a token exists in storage.
214
+ // JWT validates against auth-gateway's DB; vendor keys validate against mcp-core's DB.
215
+ if (authToken && authMethod !== 'vendor_key') {
216
+ return {
217
+ mode: 'sdk',
218
+ client: new MemoryIntelligenceClient({
219
+ apiUrl,
220
+ authToken,
221
+ authType: 'bearer',
222
+ allowMissingAuth: false,
223
+ }),
224
+ };
225
+ }
226
+ if (apiKey) {
227
+ if (apiKey.startsWith('lano_') || apiKey.startsWith('lms_')) {
228
+ return {
229
+ mode: 'sdk',
230
+ client: new MemoryIntelligenceClient({
231
+ apiUrl,
232
+ apiKey,
233
+ authType: 'apiKey',
234
+ allowMissingAuth: false,
235
+ }),
236
+ };
237
+ }
238
+ // Legacy key path: use CLI API client auth middleware directly.
239
+ return { mode: 'api' };
240
+ }
241
+ throw new Error('Authentication required. Run "lanonasis auth login" first.');
242
+ };
243
+ const printIntelligenceResult = (title, payload, options) => {
244
+ if (options.json) {
245
+ console.log(JSON.stringify(payload, null, 2));
246
+ return;
247
+ }
248
+ console.log(chalk.cyan.bold(`\n${title}`));
249
+ console.log(JSON.stringify(payload, null, 2));
250
+ };
251
+ const postIntelligenceEndpoint = async (transport, endpoint, payload) => {
252
+ if (transport.mode === 'sdk') {
253
+ if (!transport.client) {
254
+ throw new Error('SDK transport is not initialized');
255
+ }
256
+ const response = await transport.client.getHttpClient().postEnhanced(endpoint, payload);
257
+ if (response.error) {
258
+ throw new Error(response.error.message || `Request failed for ${endpoint}`);
259
+ }
260
+ return response.data;
261
+ }
262
+ return await apiClient.post(`/api/v1${endpoint}`, payload);
263
+ };
60
264
  export function memoryCommands(program) {
61
265
  // Create memory
62
266
  program
@@ -123,7 +327,7 @@ export function memoryCommands(program) {
123
327
  validate: (input) => input.length > 0 || 'Title is required',
124
328
  },
125
329
  {
126
- type: 'list',
330
+ type: 'select',
127
331
  name: 'type',
128
332
  message: 'Memory type:',
129
333
  choices: [...MEMORY_TYPE_CHOICES],
@@ -469,17 +673,23 @@ export function memoryCommands(program) {
469
673
  program
470
674
  .command('search')
471
675
  .description('Search memories using semantic search')
472
- .argument('<query>', 'search query')
676
+ .argument('<query...>', 'search query')
473
677
  .option('-l, --limit <limit>', 'number of results', '20')
474
- .option('--threshold <threshold>', 'similarity threshold (0-1)', '0.7')
678
+ .option('--threshold <threshold>', 'similarity threshold (0-1)', '0.55')
475
679
  .option('--type <types>', 'filter by memory types (comma-separated)')
476
680
  .option('--tags <tags>', 'filter by tags (comma-separated)')
477
- .action(async (query, options) => {
681
+ .action(async (queryParts, options) => {
478
682
  try {
683
+ const query = Array.isArray(queryParts) ? queryParts.join(' ').trim() : String(queryParts || '').trim();
684
+ if (!query) {
685
+ console.error(chalk.red('✖ Search query is required'));
686
+ process.exit(1);
687
+ }
479
688
  const spinner = ora(`Searching for "${query}"...`).start();
689
+ const requestedThreshold = clampThreshold(parseFloat(options.threshold || '0.55'));
480
690
  const searchOptions = {
481
691
  limit: parseInt(options.limit || '20'),
482
- threshold: parseFloat(options.threshold || '0.7')
692
+ threshold: requestedThreshold
483
693
  };
484
694
  if (options.type) {
485
695
  searchOptions.memory_types = options.type.split(',').map((t) => t.trim());
@@ -487,18 +697,67 @@ export function memoryCommands(program) {
487
697
  if (options.tags) {
488
698
  searchOptions.tags = options.tags.split(',').map((t) => t.trim());
489
699
  }
490
- const result = await apiClient.searchMemories(query, searchOptions);
700
+ const thresholdPlan = buildSearchThresholdPlan(requestedThreshold, Boolean(searchOptions.tags?.length));
701
+ let result = null;
702
+ let results = [];
703
+ let thresholdUsed = requestedThreshold;
704
+ let searchStrategy = 'semantic';
705
+ let semanticSearchError = null;
706
+ for (const threshold of thresholdPlan) {
707
+ try {
708
+ const attempt = await apiClient.searchMemories(query, {
709
+ ...searchOptions,
710
+ threshold,
711
+ });
712
+ const attemptResults = (attempt.results || attempt.data || []);
713
+ result = attempt;
714
+ if (attemptResults.length > 0) {
715
+ results = attemptResults;
716
+ thresholdUsed = threshold;
717
+ const attemptStrategy = attempt.search_strategy;
718
+ searchStrategy = typeof attemptStrategy === 'string'
719
+ ? attemptStrategy
720
+ : 'semantic';
721
+ break;
722
+ }
723
+ }
724
+ catch (error) {
725
+ semanticSearchError = error instanceof Error ? error.message : 'Unknown semantic search error';
726
+ break;
727
+ }
728
+ }
729
+ if (results.length === 0) {
730
+ const lexicalResults = await lexicalFallbackSearch(query, searchOptions);
731
+ if (lexicalResults.length > 0) {
732
+ results = lexicalResults;
733
+ searchStrategy = 'cli_lexical_fallback';
734
+ }
735
+ }
491
736
  spinner.stop();
492
- const results = result.results || result.data || [];
493
737
  if (results.length === 0) {
494
738
  console.log(chalk.yellow('No memories found matching your search'));
739
+ if (semanticSearchError) {
740
+ console.log(chalk.gray(`Semantic search error: ${semanticSearchError}`));
741
+ }
742
+ console.log(chalk.gray(`Tried thresholds: ${thresholdPlan.map((t) => t.toFixed(2)).join(', ')}`));
495
743
  return;
496
744
  }
497
- console.log(chalk.blue.bold(`\n🔍 Search Results (${result.total_results || results.length} found)`));
498
- console.log(chalk.gray(`Query: "${query}" | Search time: ${result.search_time_ms || 0}ms`));
745
+ if (semanticSearchError && searchStrategy === 'cli_lexical_fallback') {
746
+ console.log(chalk.yellow(`⚠ Semantic search unavailable, using lexical fallback (${semanticSearchError})`));
747
+ }
748
+ const totalResults = typeof result?.total_results === 'number' ? result.total_results : results.length;
749
+ const searchTimeMs = typeof result?.search_time_ms === 'number' ? result.search_time_ms : 0;
750
+ console.log(chalk.blue.bold(`\n🔍 Search Results (${totalResults} found)`));
751
+ console.log(chalk.gray(`Query: "${query}" | Search time: ${searchTimeMs}ms`));
752
+ if (Math.abs(thresholdUsed - requestedThreshold) > 0.0001) {
753
+ console.log(chalk.gray(`No matches at ${requestedThreshold.toFixed(2)}; used adaptive threshold ${thresholdUsed.toFixed(2)}`));
754
+ }
755
+ if (searchStrategy) {
756
+ console.log(chalk.gray(`Search strategy: ${searchStrategy}`));
757
+ }
499
758
  console.log();
500
759
  results.forEach((memory, index) => {
501
- const score = (memory.relevance_score * 100).toFixed(1);
760
+ const score = (memory.similarity_score * 100).toFixed(1);
502
761
  console.log(chalk.green(`${index + 1}. ${memory.title}`) + chalk.gray(` (${score}% match)`));
503
762
  console.log(chalk.white(` ${truncateText(memory.content, 100)}`));
504
763
  console.log(chalk.cyan(` ID: ${memory.id}`) + chalk.gray(` | Type: ${memory.memory_type}`));
@@ -584,7 +843,7 @@ export function memoryCommands(program) {
584
843
  default: currentMemory.title,
585
844
  },
586
845
  {
587
- type: 'list',
846
+ type: 'select',
588
847
  name: 'type',
589
848
  message: 'Memory type:',
590
849
  choices: [...MEMORY_TYPE_CHOICES],
@@ -706,4 +965,278 @@ export function memoryCommands(program) {
706
965
  process.exit(1);
707
966
  }
708
967
  });
968
+ // Intelligence commands powered by @lanonasis/mem-intel-sdk
969
+ const intelligence = program
970
+ .command('intelligence')
971
+ .description('Memory intelligence operations');
972
+ intelligence
973
+ .command('health-check')
974
+ .description('Run memory intelligence health check')
975
+ .option('--json', 'Output raw JSON payload')
976
+ .action(async (options) => {
977
+ try {
978
+ const spinner = ora('Running intelligence health check...').start();
979
+ const transport = await createIntelligenceTransport();
980
+ const userId = await resolveCurrentUserId();
981
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/health-check', { user_id: userId, response_format: 'json' });
982
+ spinner.stop();
983
+ printIntelligenceResult('🩺 Intelligence Health Check', result, options);
984
+ }
985
+ catch (error) {
986
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
987
+ console.error(chalk.red('✖ Intelligence health check failed:'), errorMessage);
988
+ process.exit(1);
989
+ }
990
+ });
991
+ intelligence
992
+ .command('suggest-tags')
993
+ .description('Suggest tags for a memory')
994
+ .argument('<memory-id>', 'Memory ID')
995
+ .option('--max <number>', 'Maximum suggestions', '8')
996
+ .option('--json', 'Output raw JSON payload')
997
+ .action(async (memoryId, options) => {
998
+ try {
999
+ const spinner = ora('Generating tag suggestions...').start();
1000
+ const transport = await createIntelligenceTransport();
1001
+ const userId = await resolveCurrentUserId();
1002
+ const maxSuggestions = Math.max(1, Math.min(20, parseInt(options.max || '8', 10)));
1003
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/suggest-tags', {
1004
+ memory_id: memoryId,
1005
+ user_id: userId,
1006
+ max_suggestions: maxSuggestions,
1007
+ include_existing_tags: true,
1008
+ response_format: 'json',
1009
+ });
1010
+ spinner.stop();
1011
+ printIntelligenceResult('🏷️ Tag Suggestions', result, options);
1012
+ }
1013
+ catch (error) {
1014
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1015
+ console.error(chalk.red('✖ Failed to suggest tags:'), errorMessage);
1016
+ process.exit(1);
1017
+ }
1018
+ });
1019
+ intelligence
1020
+ .command('find-related')
1021
+ .description('Find memories related to a source memory')
1022
+ .argument('<memory-id>', 'Source memory ID')
1023
+ .option('--limit <number>', 'Maximum related memories', '5')
1024
+ .option('--threshold <number>', 'Similarity threshold (0-1)', '0.7')
1025
+ .option('--json', 'Output raw JSON payload')
1026
+ .action(async (memoryId, options) => {
1027
+ try {
1028
+ const spinner = ora('Finding related memories...').start();
1029
+ const transport = await createIntelligenceTransport();
1030
+ const userId = await resolveCurrentUserId();
1031
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/find-related', {
1032
+ memory_id: memoryId,
1033
+ user_id: userId,
1034
+ limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
1035
+ similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
1036
+ response_format: 'json',
1037
+ });
1038
+ spinner.stop();
1039
+ printIntelligenceResult('🔗 Related Memories', result, options);
1040
+ }
1041
+ catch (error) {
1042
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1043
+ console.error(chalk.red('✖ Failed to find related memories:'), errorMessage);
1044
+ process.exit(1);
1045
+ }
1046
+ });
1047
+ intelligence
1048
+ .command('detect-duplicates')
1049
+ .description('Detect duplicate memory entries')
1050
+ .option('--threshold <number>', 'Similarity threshold (0-1)', '0.88')
1051
+ .option('--max-pairs <number>', 'Maximum duplicate pairs to inspect', '100')
1052
+ .option('--json', 'Output raw JSON payload')
1053
+ .action(async (options) => {
1054
+ try {
1055
+ const spinner = ora('Detecting duplicates...').start();
1056
+ const transport = await createIntelligenceTransport();
1057
+ const userId = await resolveCurrentUserId();
1058
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/detect-duplicates', {
1059
+ user_id: userId,
1060
+ similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.88'))),
1061
+ max_pairs: Math.max(10, Math.min(500, parseInt(options.maxPairs || '100', 10))),
1062
+ response_format: 'json',
1063
+ });
1064
+ spinner.stop();
1065
+ printIntelligenceResult('🧬 Duplicate Detection', result, options);
1066
+ }
1067
+ catch (error) {
1068
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1069
+ console.error(chalk.red('✖ Failed to detect duplicates:'), errorMessage);
1070
+ process.exit(1);
1071
+ }
1072
+ });
1073
+ intelligence
1074
+ .command('extract-insights')
1075
+ .description('Extract insights from memory collection')
1076
+ .option('--topic <topic>', 'Optional topic filter')
1077
+ .option('--type <type>', `Optional memory type filter (${MEMORY_TYPE_CHOICES.join(', ')})`)
1078
+ .option('--max-memories <number>', 'Maximum memories to analyze', '50')
1079
+ .option('--json', 'Output raw JSON payload')
1080
+ .action(async (options) => {
1081
+ try {
1082
+ const spinner = ora('Extracting insights...').start();
1083
+ const transport = await createIntelligenceTransport();
1084
+ const userId = await resolveCurrentUserId();
1085
+ const memoryType = options.type ? coerceMemoryType(options.type) : undefined;
1086
+ if (options.type && !memoryType) {
1087
+ throw new Error(`Invalid type "${options.type}". Expected one of: ${MEMORY_TYPE_CHOICES.join(', ')}`);
1088
+ }
1089
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/extract-insights', {
1090
+ user_id: userId,
1091
+ topic: options.topic,
1092
+ memory_type: memoryType,
1093
+ max_memories: Math.max(5, Math.min(200, parseInt(options.maxMemories || '50', 10))),
1094
+ response_format: 'json',
1095
+ });
1096
+ spinner.stop();
1097
+ printIntelligenceResult('💡 Memory Insights', result, options);
1098
+ }
1099
+ catch (error) {
1100
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1101
+ console.error(chalk.red('✖ Failed to extract insights:'), errorMessage);
1102
+ process.exit(1);
1103
+ }
1104
+ });
1105
+ intelligence
1106
+ .command('analyze-patterns')
1107
+ .description('Analyze memory usage patterns')
1108
+ .option('--days <number>', 'Days to include in analysis', '30')
1109
+ .option('--json', 'Output raw JSON payload')
1110
+ .action(async (options) => {
1111
+ try {
1112
+ const spinner = ora('Analyzing memory patterns...').start();
1113
+ const transport = await createIntelligenceTransport();
1114
+ const userId = await resolveCurrentUserId();
1115
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/analyze-patterns', {
1116
+ user_id: userId,
1117
+ time_range_days: Math.max(1, Math.min(365, parseInt(options.days || '30', 10))),
1118
+ response_format: 'json',
1119
+ });
1120
+ spinner.stop();
1121
+ printIntelligenceResult('📈 Pattern Analysis', result, options);
1122
+ }
1123
+ catch (error) {
1124
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1125
+ console.error(chalk.red('✖ Failed to analyze patterns:'), errorMessage);
1126
+ process.exit(1);
1127
+ }
1128
+ });
1129
+ // Behavior commands powered by @lanonasis/mem-intel-sdk
1130
+ const behavior = program
1131
+ .command('behavior')
1132
+ .description('Behavior pattern intelligence operations');
1133
+ behavior
1134
+ .command('record')
1135
+ .description('Record a behavior pattern from successful workflow steps')
1136
+ .requiredOption('--trigger <text>', 'Behavior trigger description')
1137
+ .requiredOption('--final-outcome <result>', 'Final outcome: success | partial | failed')
1138
+ .requiredOption('--actions <json>', 'Actions JSON array. Example: [{"tool":"memory.search","params":{"query":"auth fix"}}]')
1139
+ .option('--context <json>', 'Context JSON object')
1140
+ .option('--confidence <number>', 'Confidence score (0-1)', '0.7')
1141
+ .option('--json', 'Output raw JSON payload')
1142
+ .action(async (options) => {
1143
+ try {
1144
+ const spinner = ora('Recording behavior pattern...').start();
1145
+ const transport = await createIntelligenceTransport();
1146
+ const userId = await resolveCurrentUserId();
1147
+ const parsedActions = parseJsonOption(options.actions, '--actions');
1148
+ const actions = ensureBehaviorActions(parsedActions, '--actions');
1149
+ const parsedContext = parseJsonOption(options.context, '--context');
1150
+ const context = ensureJsonObject(parsedContext, '--context');
1151
+ const finalOutcome = options.finalOutcome;
1152
+ if (!['success', 'partial', 'failed'].includes(finalOutcome)) {
1153
+ throw new Error('--final-outcome must be one of: success, partial, failed');
1154
+ }
1155
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-record', {
1156
+ user_id: userId,
1157
+ trigger: options.trigger,
1158
+ context: context || {},
1159
+ actions,
1160
+ final_outcome: finalOutcome,
1161
+ confidence: Math.max(0, Math.min(1, parseFloat(options.confidence || '0.7'))),
1162
+ });
1163
+ spinner.stop();
1164
+ printIntelligenceResult('🧠 Behavior Recorded', result, options);
1165
+ }
1166
+ catch (error) {
1167
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1168
+ console.error(chalk.red('✖ Failed to record behavior pattern:'), errorMessage);
1169
+ process.exit(1);
1170
+ }
1171
+ });
1172
+ behavior
1173
+ .command('recall')
1174
+ .description('Recall behavior patterns relevant to the current task')
1175
+ .requiredOption('--task <text>', 'Current task description')
1176
+ .option('--context <json>', 'Additional context JSON object')
1177
+ .option('--limit <number>', 'Maximum patterns to return', '5')
1178
+ .option('--threshold <number>', 'Similarity threshold (0-1)', '0.7')
1179
+ .option('--json', 'Output raw JSON payload')
1180
+ .action(async (options) => {
1181
+ try {
1182
+ const spinner = ora('Recalling behavior patterns...').start();
1183
+ const transport = await createIntelligenceTransport();
1184
+ const userId = await resolveCurrentUserId();
1185
+ const parsedContext = parseJsonOption(options.context, '--context');
1186
+ const context = ensureJsonObject(parsedContext, '--context') || {};
1187
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-recall', {
1188
+ user_id: userId,
1189
+ context: {
1190
+ ...context,
1191
+ current_task: options.task,
1192
+ },
1193
+ limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
1194
+ similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
1195
+ });
1196
+ spinner.stop();
1197
+ printIntelligenceResult('🔁 Behavior Recall', result, options);
1198
+ }
1199
+ catch (error) {
1200
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1201
+ console.error(chalk.red('✖ Failed to recall behavior patterns:'), errorMessage);
1202
+ process.exit(1);
1203
+ }
1204
+ });
1205
+ behavior
1206
+ .command('suggest')
1207
+ .description('Suggest next actions from learned behavior patterns')
1208
+ .requiredOption('--task <text>', 'Current task description')
1209
+ .option('--state <json>', 'Additional current state JSON object')
1210
+ .option('--completed-steps <json>', 'Completed steps JSON array')
1211
+ .option('--max-suggestions <number>', 'Maximum suggestions', '3')
1212
+ .option('--json', 'Output raw JSON payload')
1213
+ .action(async (options) => {
1214
+ try {
1215
+ const spinner = ora('Generating behavior suggestions...').start();
1216
+ const transport = await createIntelligenceTransport();
1217
+ const userId = await resolveCurrentUserId();
1218
+ const parsedState = parseJsonOption(options.state, '--state');
1219
+ const state = ensureJsonObject(parsedState, '--state') || {};
1220
+ const parsedCompletedSteps = parseJsonOption(options.completedSteps, '--completed-steps');
1221
+ const completedSteps = parsedCompletedSteps === undefined
1222
+ ? undefined
1223
+ : ensureBehaviorActions(parsedCompletedSteps, '--completed-steps', { allowEmpty: true });
1224
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-suggest', {
1225
+ user_id: userId,
1226
+ current_state: {
1227
+ ...state,
1228
+ task_description: options.task,
1229
+ completed_steps: completedSteps,
1230
+ },
1231
+ max_suggestions: Math.max(1, Math.min(10, parseInt(options.maxSuggestions || '3', 10))),
1232
+ });
1233
+ spinner.stop();
1234
+ printIntelligenceResult('🎯 Behavior Suggestions', result, options);
1235
+ }
1236
+ catch (error) {
1237
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1238
+ console.error(chalk.red('✖ Failed to suggest actions:'), errorMessage);
1239
+ process.exit(1);
1240
+ }
1241
+ });
709
1242
  }
@@ -280,7 +280,7 @@ export class InteractiveMemoryCreator {
280
280
  console.log(chalk.cyan(`📎 I noticed this looks like ${suggestions.contentType}. Would you like to:`));
281
281
  const { topicChoice } = await inquirer.prompt([
282
282
  {
283
- type: 'list',
283
+ type: 'select',
284
284
  name: 'topicChoice',
285
285
  message: 'Select topic:',
286
286
  choices: [
@@ -336,7 +336,7 @@ export class InteractiveMemoryCreator {
336
336
  // Memory type selection
337
337
  const { memoryType } = await inquirer.prompt([
338
338
  {
339
- type: 'list',
339
+ type: 'select',
340
340
  name: 'memoryType',
341
341
  message: 'Memory Type:',
342
342
  choices: [
@@ -358,7 +358,7 @@ export class InteractiveMemoryCreator {
358
358
  // Confirm save
359
359
  const { action } = await inquirer.prompt([
360
360
  {
361
- type: 'list',
361
+ type: 'select',
362
362
  name: 'action',
363
363
  message: 'Ready to save?',
364
364
  choices: [
@@ -459,7 +459,7 @@ export class InteractiveSearch {
459
459
  // Result actions
460
460
  const { action } = await inquirer.prompt([
461
461
  {
462
- type: 'list',
462
+ type: 'select',
463
463
  name: 'action',
464
464
  message: 'Actions:',
465
465
  choices: [
package/dist/index.js CHANGED
@@ -91,7 +91,14 @@ program
91
91
  actionCommand.parent?.name?.() === 'mcp-server';
92
92
  const isConfigFlow = actionCommand.name() === 'config' ||
93
93
  actionCommand.parent?.name?.() === 'config';
94
- if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
94
+ // Memory, topic, org, and key commands use the direct REST API — skip MCP auto-connect
95
+ const isDirectApiFlow = actionCommand.name() === 'memory' ||
96
+ actionCommand.parent?.name?.() === 'memory' ||
97
+ actionCommand.name() === 'topic' ||
98
+ actionCommand.parent?.name?.() === 'topic' ||
99
+ actionCommand.name() === 'org' ||
100
+ actionCommand.parent?.name?.() === 'org';
101
+ if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !isDirectApiFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
95
102
  try {
96
103
  const client = getMCPClient();
97
104
  if (!client.isConnectedToServer()) {