@lanonasis/cli 3.9.7 → 3.9.8

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog - @lanonasis/cli
2
2
 
3
+ ## [3.9.8] - 2026-02-25
4
+
5
+ ### ✨ New Features
6
+
7
+ - **Issue #98 (CLI Memory UX Enhancements)**:
8
+ - Added `onasis memory create --json <json>` for direct JSON payload creation.
9
+ - Added `onasis memory create --content-file <path>` for file-based content ingestion.
10
+ - Added `onasis memory save-session` to persist branch/status/changed-files session context as memory.
11
+ - **Behavior methods via CLI commands**:
12
+ - Added `onasis memory intelligence` subcommands for health check, tag suggestions, related lookup, duplicate detection, insight extraction, and pattern analysis.
13
+ - Added `onasis memory behavior` subcommands for `record`, `recall`, and `suggest` workflow behavior operations.
14
+
15
+ ### 🐛 Bug Fixes
16
+
17
+ - Normalized memory response handling for create/get/update wrappers (`{ data: ... }`) so CLI output fields like ID/Title/Type are consistently resolved.
18
+ - Ensured token refresh is executed before memory command paths to reduce intermittent re-auth prompts during active OAuth sessions.
19
+ - Aligned default semantic search thresholds to `0.55` across memory and MCP search command paths for consistent result behavior.
20
+
3
21
  ## [3.9.7] - 2026-02-21
4
22
 
5
23
  ### ✨ New Features
@@ -132,6 +132,16 @@ export async function generateCompletionData() {
132
132
  description: 'Show memory statistics',
133
133
  options: []
134
134
  },
135
+ {
136
+ name: 'intelligence',
137
+ description: 'Memory intelligence operations (use: memory intelligence --help)',
138
+ options: []
139
+ },
140
+ {
141
+ name: 'behavior',
142
+ description: 'Behavior pattern operations (use: memory behavior --help)',
143
+ options: []
144
+ },
135
145
  {
136
146
  name: 'bulk-delete',
137
147
  description: 'Delete multiple memories',
@@ -462,7 +462,7 @@ export function mcpCommands(program) {
462
462
  .description('Search memories via MCP')
463
463
  .argument('<query>', 'Search query')
464
464
  .option('-l, --limit <number>', 'Maximum results', '10')
465
- .option('-t, --threshold <number>', 'Similarity threshold (0-1)', '0.7')
465
+ .option('-t, --threshold <number>', 'Similarity threshold (0-1)', '0.55')
466
466
  .action(async (query, options) => {
467
467
  const spinner = ora('Searching memories via MCP...').start();
468
468
  try {
@@ -488,7 +488,7 @@ export function mcpCommands(program) {
488
488
  console.log(`\n${chalk.bold(`${index + 1}. ${memory.title}`)}`);
489
489
  console.log(` ID: ${chalk.gray(memory.id)}`);
490
490
  console.log(` Type: ${chalk.blue(memory.memory_type)}`);
491
- console.log(` Score: ${chalk.green((memory.relevance_score * 100).toFixed(1) + '%')}`);
491
+ console.log(` Score: ${chalk.green((memory.similarity_score * 100).toFixed(1) + '%')}`);
492
492
  console.log(` Content: ${memory.content.substring(0, 100)}...`);
493
493
  });
494
494
  }
@@ -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,205 @@ 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
+ if (authToken) {
213
+ return {
214
+ mode: 'sdk',
215
+ client: new MemoryIntelligenceClient({
216
+ apiUrl,
217
+ authToken,
218
+ authType: 'bearer',
219
+ allowMissingAuth: false,
220
+ }),
221
+ };
222
+ }
223
+ if (apiKey) {
224
+ if (apiKey.startsWith('lano_')) {
225
+ return {
226
+ mode: 'sdk',
227
+ client: new MemoryIntelligenceClient({
228
+ apiUrl,
229
+ apiKey,
230
+ authType: 'apiKey',
231
+ allowMissingAuth: false,
232
+ }),
233
+ };
234
+ }
235
+ // Legacy non-lano key path: use CLI API client auth middleware directly.
236
+ return { mode: 'api' };
237
+ }
238
+ throw new Error('Authentication required. Run "lanonasis auth login" first.');
239
+ };
240
+ const printIntelligenceResult = (title, payload, options) => {
241
+ if (options.json) {
242
+ console.log(JSON.stringify(payload, null, 2));
243
+ return;
244
+ }
245
+ console.log(chalk.cyan.bold(`\n${title}`));
246
+ console.log(JSON.stringify(payload, null, 2));
247
+ };
248
+ const postIntelligenceEndpoint = async (transport, endpoint, payload) => {
249
+ if (transport.mode === 'sdk') {
250
+ if (!transport.client) {
251
+ throw new Error('SDK transport is not initialized');
252
+ }
253
+ const response = await transport.client.getHttpClient().postEnhanced(endpoint, payload);
254
+ if (response.error) {
255
+ throw new Error(response.error.message || `Request failed for ${endpoint}`);
256
+ }
257
+ return response.data;
258
+ }
259
+ return await apiClient.post(`/api/v1${endpoint}`, payload);
260
+ };
60
261
  export function memoryCommands(program) {
61
262
  // Create memory
62
263
  program
@@ -471,15 +672,16 @@ export function memoryCommands(program) {
471
672
  .description('Search memories using semantic search')
472
673
  .argument('<query>', 'search query')
473
674
  .option('-l, --limit <limit>', 'number of results', '20')
474
- .option('--threshold <threshold>', 'similarity threshold (0-1)', '0.7')
675
+ .option('--threshold <threshold>', 'similarity threshold (0-1)', '0.55')
475
676
  .option('--type <types>', 'filter by memory types (comma-separated)')
476
677
  .option('--tags <tags>', 'filter by tags (comma-separated)')
477
678
  .action(async (query, options) => {
478
679
  try {
479
680
  const spinner = ora(`Searching for "${query}"...`).start();
681
+ const requestedThreshold = clampThreshold(parseFloat(options.threshold || '0.55'));
480
682
  const searchOptions = {
481
683
  limit: parseInt(options.limit || '20'),
482
- threshold: parseFloat(options.threshold || '0.7')
684
+ threshold: requestedThreshold
483
685
  };
484
686
  if (options.type) {
485
687
  searchOptions.memory_types = options.type.split(',').map((t) => t.trim());
@@ -487,18 +689,52 @@ export function memoryCommands(program) {
487
689
  if (options.tags) {
488
690
  searchOptions.tags = options.tags.split(',').map((t) => t.trim());
489
691
  }
490
- const result = await apiClient.searchMemories(query, searchOptions);
692
+ const thresholdPlan = buildSearchThresholdPlan(requestedThreshold, Boolean(searchOptions.tags?.length));
693
+ let result = null;
694
+ let results = [];
695
+ let thresholdUsed = requestedThreshold;
696
+ let searchStrategy = 'semantic';
697
+ for (const threshold of thresholdPlan) {
698
+ const attempt = await apiClient.searchMemories(query, {
699
+ ...searchOptions,
700
+ threshold,
701
+ });
702
+ const attemptResults = (attempt.results || attempt.data || []);
703
+ result = attempt;
704
+ if (attemptResults.length > 0) {
705
+ results = attemptResults;
706
+ thresholdUsed = threshold;
707
+ const attemptStrategy = attempt.search_strategy;
708
+ searchStrategy = typeof attemptStrategy === 'string'
709
+ ? attemptStrategy
710
+ : 'semantic';
711
+ break;
712
+ }
713
+ }
714
+ if (results.length === 0) {
715
+ const lexicalResults = await lexicalFallbackSearch(query, searchOptions);
716
+ if (lexicalResults.length > 0) {
717
+ results = lexicalResults;
718
+ searchStrategy = 'cli_lexical_fallback';
719
+ }
720
+ }
491
721
  spinner.stop();
492
- const results = result.results || result.data || [];
493
722
  if (results.length === 0) {
494
723
  console.log(chalk.yellow('No memories found matching your search'));
724
+ console.log(chalk.gray(`Tried thresholds: ${thresholdPlan.map((t) => t.toFixed(2)).join(', ')}`));
495
725
  return;
496
726
  }
497
727
  console.log(chalk.blue.bold(`\n🔍 Search Results (${result.total_results || results.length} found)`));
498
728
  console.log(chalk.gray(`Query: "${query}" | Search time: ${result.search_time_ms || 0}ms`));
729
+ if (Math.abs(thresholdUsed - requestedThreshold) > 0.0001) {
730
+ console.log(chalk.gray(`No matches at ${requestedThreshold.toFixed(2)}; used adaptive threshold ${thresholdUsed.toFixed(2)}`));
731
+ }
732
+ if (searchStrategy) {
733
+ console.log(chalk.gray(`Search strategy: ${searchStrategy}`));
734
+ }
499
735
  console.log();
500
736
  results.forEach((memory, index) => {
501
- const score = (memory.relevance_score * 100).toFixed(1);
737
+ const score = (memory.similarity_score * 100).toFixed(1);
502
738
  console.log(chalk.green(`${index + 1}. ${memory.title}`) + chalk.gray(` (${score}% match)`));
503
739
  console.log(chalk.white(` ${truncateText(memory.content, 100)}`));
504
740
  console.log(chalk.cyan(` ID: ${memory.id}`) + chalk.gray(` | Type: ${memory.memory_type}`));
@@ -706,4 +942,278 @@ export function memoryCommands(program) {
706
942
  process.exit(1);
707
943
  }
708
944
  });
945
+ // Intelligence commands powered by @lanonasis/mem-intel-sdk
946
+ const intelligence = program
947
+ .command('intelligence')
948
+ .description('Memory intelligence operations');
949
+ intelligence
950
+ .command('health-check')
951
+ .description('Run memory intelligence health check')
952
+ .option('--json', 'Output raw JSON payload')
953
+ .action(async (options) => {
954
+ try {
955
+ const spinner = ora('Running intelligence health check...').start();
956
+ const transport = await createIntelligenceTransport();
957
+ const userId = await resolveCurrentUserId();
958
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/health-check', { user_id: userId, response_format: 'json' });
959
+ spinner.stop();
960
+ printIntelligenceResult('🩺 Intelligence Health Check', result, options);
961
+ }
962
+ catch (error) {
963
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
964
+ console.error(chalk.red('✖ Intelligence health check failed:'), errorMessage);
965
+ process.exit(1);
966
+ }
967
+ });
968
+ intelligence
969
+ .command('suggest-tags')
970
+ .description('Suggest tags for a memory')
971
+ .argument('<memory-id>', 'Memory ID')
972
+ .option('--max <number>', 'Maximum suggestions', '8')
973
+ .option('--json', 'Output raw JSON payload')
974
+ .action(async (memoryId, options) => {
975
+ try {
976
+ const spinner = ora('Generating tag suggestions...').start();
977
+ const transport = await createIntelligenceTransport();
978
+ const userId = await resolveCurrentUserId();
979
+ const maxSuggestions = Math.max(1, Math.min(20, parseInt(options.max || '8', 10)));
980
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/suggest-tags', {
981
+ memory_id: memoryId,
982
+ user_id: userId,
983
+ max_suggestions: maxSuggestions,
984
+ include_existing_tags: true,
985
+ response_format: 'json',
986
+ });
987
+ spinner.stop();
988
+ printIntelligenceResult('🏷️ Tag Suggestions', result, options);
989
+ }
990
+ catch (error) {
991
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
992
+ console.error(chalk.red('✖ Failed to suggest tags:'), errorMessage);
993
+ process.exit(1);
994
+ }
995
+ });
996
+ intelligence
997
+ .command('find-related')
998
+ .description('Find memories related to a source memory')
999
+ .argument('<memory-id>', 'Source memory ID')
1000
+ .option('--limit <number>', 'Maximum related memories', '5')
1001
+ .option('--threshold <number>', 'Similarity threshold (0-1)', '0.7')
1002
+ .option('--json', 'Output raw JSON payload')
1003
+ .action(async (memoryId, options) => {
1004
+ try {
1005
+ const spinner = ora('Finding related memories...').start();
1006
+ const transport = await createIntelligenceTransport();
1007
+ const userId = await resolveCurrentUserId();
1008
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/find-related', {
1009
+ memory_id: memoryId,
1010
+ user_id: userId,
1011
+ limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
1012
+ similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
1013
+ response_format: 'json',
1014
+ });
1015
+ spinner.stop();
1016
+ printIntelligenceResult('🔗 Related Memories', result, options);
1017
+ }
1018
+ catch (error) {
1019
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1020
+ console.error(chalk.red('✖ Failed to find related memories:'), errorMessage);
1021
+ process.exit(1);
1022
+ }
1023
+ });
1024
+ intelligence
1025
+ .command('detect-duplicates')
1026
+ .description('Detect duplicate memory entries')
1027
+ .option('--threshold <number>', 'Similarity threshold (0-1)', '0.88')
1028
+ .option('--max-pairs <number>', 'Maximum duplicate pairs to inspect', '100')
1029
+ .option('--json', 'Output raw JSON payload')
1030
+ .action(async (options) => {
1031
+ try {
1032
+ const spinner = ora('Detecting duplicates...').start();
1033
+ const transport = await createIntelligenceTransport();
1034
+ const userId = await resolveCurrentUserId();
1035
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/detect-duplicates', {
1036
+ user_id: userId,
1037
+ similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.88'))),
1038
+ max_pairs: Math.max(10, Math.min(500, parseInt(options.maxPairs || '100', 10))),
1039
+ response_format: 'json',
1040
+ });
1041
+ spinner.stop();
1042
+ printIntelligenceResult('🧬 Duplicate Detection', result, options);
1043
+ }
1044
+ catch (error) {
1045
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1046
+ console.error(chalk.red('✖ Failed to detect duplicates:'), errorMessage);
1047
+ process.exit(1);
1048
+ }
1049
+ });
1050
+ intelligence
1051
+ .command('extract-insights')
1052
+ .description('Extract insights from memory collection')
1053
+ .option('--topic <topic>', 'Optional topic filter')
1054
+ .option('--type <type>', `Optional memory type filter (${MEMORY_TYPE_CHOICES.join(', ')})`)
1055
+ .option('--max-memories <number>', 'Maximum memories to analyze', '50')
1056
+ .option('--json', 'Output raw JSON payload')
1057
+ .action(async (options) => {
1058
+ try {
1059
+ const spinner = ora('Extracting insights...').start();
1060
+ const transport = await createIntelligenceTransport();
1061
+ const userId = await resolveCurrentUserId();
1062
+ const memoryType = options.type ? coerceMemoryType(options.type) : undefined;
1063
+ if (options.type && !memoryType) {
1064
+ throw new Error(`Invalid type "${options.type}". Expected one of: ${MEMORY_TYPE_CHOICES.join(', ')}`);
1065
+ }
1066
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/extract-insights', {
1067
+ user_id: userId,
1068
+ topic: options.topic,
1069
+ memory_type: memoryType,
1070
+ max_memories: Math.max(5, Math.min(200, parseInt(options.maxMemories || '50', 10))),
1071
+ response_format: 'json',
1072
+ });
1073
+ spinner.stop();
1074
+ printIntelligenceResult('💡 Memory Insights', result, options);
1075
+ }
1076
+ catch (error) {
1077
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1078
+ console.error(chalk.red('✖ Failed to extract insights:'), errorMessage);
1079
+ process.exit(1);
1080
+ }
1081
+ });
1082
+ intelligence
1083
+ .command('analyze-patterns')
1084
+ .description('Analyze memory usage patterns')
1085
+ .option('--days <number>', 'Days to include in analysis', '30')
1086
+ .option('--json', 'Output raw JSON payload')
1087
+ .action(async (options) => {
1088
+ try {
1089
+ const spinner = ora('Analyzing memory patterns...').start();
1090
+ const transport = await createIntelligenceTransport();
1091
+ const userId = await resolveCurrentUserId();
1092
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/analyze-patterns', {
1093
+ user_id: userId,
1094
+ time_range_days: Math.max(1, Math.min(365, parseInt(options.days || '30', 10))),
1095
+ response_format: 'json',
1096
+ });
1097
+ spinner.stop();
1098
+ printIntelligenceResult('📈 Pattern Analysis', result, options);
1099
+ }
1100
+ catch (error) {
1101
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1102
+ console.error(chalk.red('✖ Failed to analyze patterns:'), errorMessage);
1103
+ process.exit(1);
1104
+ }
1105
+ });
1106
+ // Behavior commands powered by @lanonasis/mem-intel-sdk
1107
+ const behavior = program
1108
+ .command('behavior')
1109
+ .description('Behavior pattern intelligence operations');
1110
+ behavior
1111
+ .command('record')
1112
+ .description('Record a behavior pattern from successful workflow steps')
1113
+ .requiredOption('--trigger <text>', 'Behavior trigger description')
1114
+ .requiredOption('--final-outcome <result>', 'Final outcome: success | partial | failed')
1115
+ .requiredOption('--actions <json>', 'Actions JSON array. Example: [{"tool":"memory.search","params":{"query":"auth fix"}}]')
1116
+ .option('--context <json>', 'Context JSON object')
1117
+ .option('--confidence <number>', 'Confidence score (0-1)', '0.7')
1118
+ .option('--json', 'Output raw JSON payload')
1119
+ .action(async (options) => {
1120
+ try {
1121
+ const spinner = ora('Recording behavior pattern...').start();
1122
+ const transport = await createIntelligenceTransport();
1123
+ const userId = await resolveCurrentUserId();
1124
+ const parsedActions = parseJsonOption(options.actions, '--actions');
1125
+ const actions = ensureBehaviorActions(parsedActions, '--actions');
1126
+ const parsedContext = parseJsonOption(options.context, '--context');
1127
+ const context = ensureJsonObject(parsedContext, '--context');
1128
+ const finalOutcome = options.finalOutcome;
1129
+ if (!['success', 'partial', 'failed'].includes(finalOutcome)) {
1130
+ throw new Error('--final-outcome must be one of: success, partial, failed');
1131
+ }
1132
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-record', {
1133
+ user_id: userId,
1134
+ trigger: options.trigger,
1135
+ context: context || {},
1136
+ actions,
1137
+ final_outcome: finalOutcome,
1138
+ confidence: Math.max(0, Math.min(1, parseFloat(options.confidence || '0.7'))),
1139
+ });
1140
+ spinner.stop();
1141
+ printIntelligenceResult('🧠 Behavior Recorded', result, options);
1142
+ }
1143
+ catch (error) {
1144
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1145
+ console.error(chalk.red('✖ Failed to record behavior pattern:'), errorMessage);
1146
+ process.exit(1);
1147
+ }
1148
+ });
1149
+ behavior
1150
+ .command('recall')
1151
+ .description('Recall behavior patterns relevant to the current task')
1152
+ .requiredOption('--task <text>', 'Current task description')
1153
+ .option('--context <json>', 'Additional context JSON object')
1154
+ .option('--limit <number>', 'Maximum patterns to return', '5')
1155
+ .option('--threshold <number>', 'Similarity threshold (0-1)', '0.7')
1156
+ .option('--json', 'Output raw JSON payload')
1157
+ .action(async (options) => {
1158
+ try {
1159
+ const spinner = ora('Recalling behavior patterns...').start();
1160
+ const transport = await createIntelligenceTransport();
1161
+ const userId = await resolveCurrentUserId();
1162
+ const parsedContext = parseJsonOption(options.context, '--context');
1163
+ const context = ensureJsonObject(parsedContext, '--context') || {};
1164
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-recall', {
1165
+ user_id: userId,
1166
+ context: {
1167
+ ...context,
1168
+ current_task: options.task,
1169
+ },
1170
+ limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
1171
+ similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
1172
+ });
1173
+ spinner.stop();
1174
+ printIntelligenceResult('🔁 Behavior Recall', result, options);
1175
+ }
1176
+ catch (error) {
1177
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1178
+ console.error(chalk.red('✖ Failed to recall behavior patterns:'), errorMessage);
1179
+ process.exit(1);
1180
+ }
1181
+ });
1182
+ behavior
1183
+ .command('suggest')
1184
+ .description('Suggest next actions from learned behavior patterns')
1185
+ .requiredOption('--task <text>', 'Current task description')
1186
+ .option('--state <json>', 'Additional current state JSON object')
1187
+ .option('--completed-steps <json>', 'Completed steps JSON array')
1188
+ .option('--max-suggestions <number>', 'Maximum suggestions', '3')
1189
+ .option('--json', 'Output raw JSON payload')
1190
+ .action(async (options) => {
1191
+ try {
1192
+ const spinner = ora('Generating behavior suggestions...').start();
1193
+ const transport = await createIntelligenceTransport();
1194
+ const userId = await resolveCurrentUserId();
1195
+ const parsedState = parseJsonOption(options.state, '--state');
1196
+ const state = ensureJsonObject(parsedState, '--state') || {};
1197
+ const parsedCompletedSteps = parseJsonOption(options.completedSteps, '--completed-steps');
1198
+ const completedSteps = parsedCompletedSteps === undefined
1199
+ ? undefined
1200
+ : ensureBehaviorActions(parsedCompletedSteps, '--completed-steps', { allowEmpty: true });
1201
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-suggest', {
1202
+ user_id: userId,
1203
+ current_state: {
1204
+ ...state,
1205
+ task_description: options.task,
1206
+ completed_steps: completedSteps,
1207
+ },
1208
+ max_suggestions: Math.max(1, Math.min(10, parseInt(options.maxSuggestions || '3', 10))),
1209
+ });
1210
+ spinner.stop();
1211
+ printIntelligenceResult('🎯 Behavior Suggestions', result, options);
1212
+ }
1213
+ catch (error) {
1214
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1215
+ console.error(chalk.red('✖ Failed to suggest actions:'), errorMessage);
1216
+ process.exit(1);
1217
+ }
1218
+ });
709
1219
  }
@@ -39,7 +39,7 @@ export const MemorySearchSchema = z.object({
39
39
  threshold: z.number()
40
40
  .min(0)
41
41
  .max(1)
42
- .default(0.7)
42
+ .default(0.55)
43
43
  .describe("Similarity threshold (0-1)"),
44
44
  topic_id: z.string()
45
45
  .uuid()
@@ -134,7 +134,7 @@ export class LanonasisMCPServer {
134
134
  type: 'number',
135
135
  minimum: 0,
136
136
  maximum: 1,
137
- default: 0.7,
137
+ default: 0.55,
138
138
  description: 'Similarity threshold'
139
139
  }
140
140
  },
@@ -589,7 +589,7 @@ Tags: [Optional comma-separated tags]`
589
589
 
590
590
  Query: ${args?.query || '[Enter search terms]'}
591
591
  Limit: [Number of results, default 10]
592
- Threshold: [Similarity threshold 0-1, default 0.7]`
592
+ Threshold: [Similarity threshold 0-1, default 0.55]`
593
593
  }
594
594
  }
595
595
  ]
@@ -22,6 +22,7 @@ export interface LoginRequest {
22
22
  password: string;
23
23
  }
24
24
  export type MemoryType = 'context' | 'project' | 'knowledge' | 'reference' | 'personal' | 'workflow';
25
+ export type WriteIntent = 'new' | 'continue' | 'auto';
25
26
  export interface MemoryEntry {
26
27
  id: string;
27
28
  title: string;
@@ -44,6 +45,9 @@ export interface CreateMemoryRequest {
44
45
  tags?: string[];
45
46
  topic_id?: string;
46
47
  metadata?: Record<string, unknown>;
48
+ continuity_key?: string;
49
+ idempotency_key?: string;
50
+ write_intent?: WriteIntent;
47
51
  }
48
52
  export interface UpdateMemoryRequest {
49
53
  title?: string;
@@ -52,6 +56,9 @@ export interface UpdateMemoryRequest {
52
56
  tags?: string[];
53
57
  topic_id?: string | null;
54
58
  metadata?: Record<string, unknown>;
59
+ continuity_key?: string;
60
+ idempotency_key?: string;
61
+ write_intent?: WriteIntent;
55
62
  }
56
63
  export interface GetMemoriesParams {
57
64
  page?: number;
@@ -75,7 +82,7 @@ export interface SearchMemoryRequest {
75
82
  threshold?: number;
76
83
  }
77
84
  export interface MemorySearchResult extends MemoryEntry {
78
- relevance_score: number;
85
+ similarity_score: number;
79
86
  }
80
87
  export interface MemoryStats {
81
88
  total_memories: number;
@@ -172,6 +179,8 @@ export declare class APIClient {
172
179
  noExit: boolean;
173
180
  private normalizeMemoryEntry;
174
181
  private shouldUseLegacyMemoryRpcFallback;
182
+ private shouldRetryViaApiGateway;
183
+ private normalizeMcpPathToApi;
175
184
  constructor();
176
185
  login(email: string, password: string): Promise<AuthResponse>;
177
186
  register(email: string, password: string, organizationName?: string): Promise<AuthResponse>;
package/dist/utils/api.js CHANGED
@@ -42,6 +42,26 @@ export class APIClient {
42
42
  }
43
43
  return false;
44
44
  }
45
+ shouldRetryViaApiGateway(error) {
46
+ const baseURL = String(error?.config?.baseURL || '');
47
+ const code = String(error?.code || '');
48
+ const alreadyRetried = Boolean(error?.config?.__retriedViaApiGateway);
49
+ if (alreadyRetried)
50
+ return false;
51
+ if (!baseURL.includes('mcp.lanonasis.com'))
52
+ return false;
53
+ return code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED';
54
+ }
55
+ normalizeMcpPathToApi(url) {
56
+ // MCP HTTP compatibility path -> API gateway REST paths
57
+ if (url === '/memory') {
58
+ return '/api/v1/memories';
59
+ }
60
+ if (url.startsWith('/memory/')) {
61
+ return url.replace('/memory/', '/api/v1/memories/');
62
+ }
63
+ return url;
64
+ }
45
65
  constructor() {
46
66
  this.config = new CLIConfig();
47
67
  this.client = axios.create({
@@ -67,7 +87,9 @@ export class APIClient {
67
87
  const forceApiFromConfig = this.config.get('forceApi') === true
68
88
  || this.config.get('connectionTransport') === 'api';
69
89
  // Memory CRUD/search endpoints should always use the API gateway path.
70
- const forceDirectApi = forceApiFromEnv || forceApiFromConfig || isMemoryEndpoint;
90
+ const forceDirectApiRetry = config
91
+ .__forceDirectApiGatewayRetry === true;
92
+ const forceDirectApi = forceApiFromEnv || forceApiFromConfig || isMemoryEndpoint || forceDirectApiRetry;
71
93
  const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
72
94
  const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
73
95
  // Determine the correct API base URL:
@@ -147,6 +169,18 @@ export class APIClient {
147
169
  }
148
170
  return response;
149
171
  }, (error) => {
172
+ if (this.shouldRetryViaApiGateway(error)) {
173
+ const retryConfig = {
174
+ ...error.config,
175
+ __retriedViaApiGateway: true,
176
+ __forceDirectApiGatewayRetry: true
177
+ };
178
+ retryConfig.baseURL = this.config.getApiUrl();
179
+ if (typeof retryConfig.url === 'string') {
180
+ retryConfig.url = this.normalizeMcpPathToApi(retryConfig.url);
181
+ }
182
+ return this.client.request(retryConfig);
183
+ }
150
184
  if (error.response) {
151
185
  const { status, data } = error.response;
152
186
  if (status === 401) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.7",
3
+ "version": "3.9.8",
4
4
  "description": "Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security",
5
5
  "keywords": [
6
6
  "lanonasis",
@@ -52,6 +52,7 @@
52
52
  "CHANGELOG.md"
53
53
  ],
54
54
  "dependencies": {
55
+ "@lanonasis/mem-intel-sdk": "2.0.3",
55
56
  "@lanonasis/oauth-client": "2.0.0",
56
57
  "@lanonasis/security-sdk": "1.0.5",
57
58
  "@modelcontextprotocol/sdk": "^1.26.0",