@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 +18 -0
- package/dist/commands/completion.js +10 -0
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/memory.js +515 -5
- package/dist/mcp/schemas/tool-schemas.js +1 -1
- package/dist/mcp/server/lanonasis-server.js +2 -2
- package/dist/utils/api.d.ts +10 -1
- package/dist/utils/api.js +35 -1
- package/package.json +2 -1
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',
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
}
|
package/dist/commands/memory.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
|
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.
|
|
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
|
}
|
|
@@ -134,7 +134,7 @@ export class LanonasisMCPServer {
|
|
|
134
134
|
type: 'number',
|
|
135
135
|
minimum: 0,
|
|
136
136
|
maximum: 1,
|
|
137
|
-
default: 0.
|
|
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.
|
|
592
|
+
Threshold: [Similarity threshold 0-1, default 0.55]`
|
|
593
593
|
}
|
|
594
594
|
}
|
|
595
595
|
]
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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",
|