@lanonasis/cli 3.9.4 → 3.9.5
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/README.md +36 -1
- package/dist/commands/config.js +11 -0
- package/dist/commands/memory.js +276 -6
- package/dist/index-simple.js +16 -3
- package/dist/index.js +19 -3
- package/dist/mcp/schemas/tool-schemas.d.ts +8 -8
- package/dist/utils/api.d.ts +5 -0
- package/dist/utils/api.js +128 -11
- package/dist/utils/config.js +87 -0
- package/dist/ux/implementations/TextInputHandlerImpl.d.ts +1 -0
- package/dist/ux/implementations/TextInputHandlerImpl.js +8 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -242,6 +242,12 @@ onasis memory list --type context --limit 20
|
|
|
242
242
|
onasis memory create -t "Project Notes" -c "Important information"
|
|
243
243
|
onasis memory create -t "Reference" --type reference --tags "docs,api"
|
|
244
244
|
|
|
245
|
+
# Create memory via JSON payload
|
|
246
|
+
onasis memory create --json '{"title":"Design decisions","type":"project","content":"Summary...","tags":["architecture","design"]}'
|
|
247
|
+
|
|
248
|
+
# Create memory from a file
|
|
249
|
+
onasis memory create -t "Session notes" --content-file ./notes.md
|
|
250
|
+
|
|
245
251
|
# Create memories (interactive)
|
|
246
252
|
onasis memory create -i # Interactive mode with inline editor
|
|
247
253
|
onasis memory create # Prompts for missing fields
|
|
@@ -258,6 +264,34 @@ onasis memory delete <id> # Delete memory
|
|
|
258
264
|
onasis memory stats # Memory statistics
|
|
259
265
|
```
|
|
260
266
|
|
|
267
|
+
#### `onasis memory save-session`
|
|
268
|
+
|
|
269
|
+
Save the current CLI session context (CWD + git branch/status + changed files) as a memory entry so you can persist what you worked on and pick it up later.
|
|
270
|
+
|
|
271
|
+
- `--test-summary`: Stores a human-readable test result summary (e.g., `Vitest: 53 passed, 1 skipped`) in the saved session memory.
|
|
272
|
+
- `--title`: Sets the memory title (default: `Session summary`).
|
|
273
|
+
- `--type`: Sets the memory type (default: `project`).
|
|
274
|
+
- `--tags`: Comma-separated tags for session metadata (default: `session,cli`).
|
|
275
|
+
|
|
276
|
+
**Examples**
|
|
277
|
+
```bash
|
|
278
|
+
onasis memory save-session --test-summary "Vitest: 53 passed, 1 skipped"
|
|
279
|
+
onasis memory save-session --title "API client fixes" --type project --tags "session,cli,testing"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
See **Session management** below for related commands.
|
|
283
|
+
|
|
284
|
+
#### Session management
|
|
285
|
+
|
|
286
|
+
Sessions are stored as memory entries (tagged `session,cli` by default). Related commands:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
onasis memory save-session
|
|
290
|
+
onasis memory list-sessions
|
|
291
|
+
onasis memory load-session <id>
|
|
292
|
+
onasis memory delete-session <id>
|
|
293
|
+
```
|
|
294
|
+
|
|
261
295
|
**Create/Update Options:**
|
|
262
296
|
| Short | Long | Description |
|
|
263
297
|
|-------|------|-------------|
|
|
@@ -266,6 +300,8 @@ onasis memory stats # Memory statistics
|
|
|
266
300
|
| `-i` | `--interactive` | Interactive mode |
|
|
267
301
|
| | `--type` | Memory type (context, project, knowledge, etc.) |
|
|
268
302
|
| | `--tags` | Comma-separated tags |
|
|
303
|
+
| | `--json` | JSON payload (title, content, type/memory_type, tags, topic_id) |
|
|
304
|
+
| | `--content-file` | Read content from a file |
|
|
269
305
|
|
|
270
306
|
### Topic Management
|
|
271
307
|
|
|
@@ -637,4 +673,3 @@ The CLI will show the authorization URL - copy and paste it into your browser ma
|
|
|
637
673
|
|
|
638
674
|
**Token refresh failed:**
|
|
639
675
|
Run `onasis auth login` to re-authenticate.
|
|
640
|
-
|
package/dist/commands/config.js
CHANGED
|
@@ -11,11 +11,13 @@ export function configCommands(program) {
|
|
|
11
11
|
.action(async (key, value) => {
|
|
12
12
|
const config = new CLIConfig();
|
|
13
13
|
await config.init();
|
|
14
|
+
let shouldSave = true;
|
|
14
15
|
// Handle special cases
|
|
15
16
|
switch (key) {
|
|
16
17
|
case 'api-url':
|
|
17
18
|
await config.setApiUrl(value);
|
|
18
19
|
console.log(chalk.green('✓ API URL updated:'), value);
|
|
20
|
+
shouldSave = false; // setApiUrl already persists
|
|
19
21
|
break;
|
|
20
22
|
case 'ai-integration':
|
|
21
23
|
if (value === 'claude-mcp') {
|
|
@@ -42,11 +44,19 @@ export function configCommands(program) {
|
|
|
42
44
|
config.set('mcpServerUrl', value);
|
|
43
45
|
console.log(chalk.green('✓ MCP server URL updated:'), value);
|
|
44
46
|
break;
|
|
47
|
+
case 'force-api':
|
|
48
|
+
config.set('forceApi', value === 'true');
|
|
49
|
+
config.set('connectionTransport', value === 'true' ? 'api' : 'auto');
|
|
50
|
+
console.log(chalk.green('✓ Force direct API mode:'), value === 'true' ? 'enabled' : 'disabled');
|
|
51
|
+
break;
|
|
45
52
|
default:
|
|
46
53
|
// Generic config set
|
|
47
54
|
config.set(key, value);
|
|
48
55
|
console.log(chalk.green(`✓ ${key} set to:`), value);
|
|
49
56
|
}
|
|
57
|
+
if (shouldSave) {
|
|
58
|
+
await config.save();
|
|
59
|
+
}
|
|
50
60
|
});
|
|
51
61
|
// Generic config get command
|
|
52
62
|
program
|
|
@@ -101,6 +111,7 @@ export function configCommands(program) {
|
|
|
101
111
|
{ key: 'mcp-use-remote', description: 'Use remote MCP server', current: config.get('mcpUseRemote') || false },
|
|
102
112
|
{ key: 'mcp-server-path', description: 'Local MCP server path', current: config.get('mcpServerPath') || 'default' },
|
|
103
113
|
{ key: 'mcp-server-url', description: 'Remote MCP server URL', current: config.get('mcpServerUrl') || 'https://mcp.lanonasis.com' },
|
|
114
|
+
{ key: 'force-api', description: 'Force direct API transport', current: config.get('forceApi') || false },
|
|
104
115
|
{ key: 'mcpEnabled', description: 'MCP integration enabled', current: config.get('mcpEnabled') || false }
|
|
105
116
|
];
|
|
106
117
|
configOptions.forEach(opt => {
|
package/dist/commands/memory.js
CHANGED
|
@@ -8,6 +8,30 @@ import { apiClient } from '../utils/api.js';
|
|
|
8
8
|
import { formatBytes, truncateText } from '../utils/formatting.js';
|
|
9
9
|
import { CLIConfig } from '../utils/config.js';
|
|
10
10
|
import { createTextInputHandler } from '../ux/index.js';
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
import { exec as execCb } from 'node:child_process';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
const exec = promisify(execCb);
|
|
15
|
+
const MEMORY_TYPE_CHOICES = [
|
|
16
|
+
'context',
|
|
17
|
+
'project',
|
|
18
|
+
'knowledge',
|
|
19
|
+
'reference',
|
|
20
|
+
'personal',
|
|
21
|
+
'workflow',
|
|
22
|
+
];
|
|
23
|
+
const coerceMemoryType = (value) => {
|
|
24
|
+
if (typeof value !== 'string')
|
|
25
|
+
return undefined;
|
|
26
|
+
const normalized = value.trim().toLowerCase();
|
|
27
|
+
// Backward compatibility for older docs/examples.
|
|
28
|
+
if (normalized === 'conversation')
|
|
29
|
+
return 'context';
|
|
30
|
+
if (MEMORY_TYPE_CHOICES.includes(normalized)) {
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
};
|
|
11
35
|
const resolveInputMode = async () => {
|
|
12
36
|
const config = new CLIConfig();
|
|
13
37
|
await config.init();
|
|
@@ -41,13 +65,53 @@ export function memoryCommands(program) {
|
|
|
41
65
|
.description('Create a new memory entry')
|
|
42
66
|
.option('-t, --title <title>', 'memory title')
|
|
43
67
|
.option('-c, --content <content>', 'memory content')
|
|
44
|
-
.option('--type <type>',
|
|
68
|
+
.option('--type <type>', `memory type (${MEMORY_TYPE_CHOICES.join(', ')})`)
|
|
45
69
|
.option('--tags <tags>', 'comma-separated tags')
|
|
46
70
|
.option('--topic-id <id>', 'topic ID')
|
|
47
71
|
.option('-i, --interactive', 'interactive mode')
|
|
72
|
+
.option('--json <json>', 'JSON payload (title, content, type/memory_type, tags[], topic_id)')
|
|
73
|
+
.option('--content-file <path>', 'Read memory content from a file (overrides --content)')
|
|
48
74
|
.action(async (options) => {
|
|
49
75
|
try {
|
|
50
|
-
let { title, content, type, tags, topicId, interactive } = options;
|
|
76
|
+
let { title, content, type, tags, topicId, interactive, json, contentFile } = options;
|
|
77
|
+
// 1) JSON payload (optional)
|
|
78
|
+
if (json) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(json);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const msg = err instanceof Error ? err.message : 'Invalid JSON';
|
|
85
|
+
throw new Error(`Invalid --json payload: ${msg}`);
|
|
86
|
+
}
|
|
87
|
+
if (!title && typeof parsed.title === 'string')
|
|
88
|
+
title = parsed.title;
|
|
89
|
+
if (!content && typeof parsed.content === 'string')
|
|
90
|
+
content = parsed.content;
|
|
91
|
+
const parsedType = parsed.memory_type ?? parsed.type;
|
|
92
|
+
if (!type && parsedType !== undefined) {
|
|
93
|
+
const coerced = coerceMemoryType(parsedType);
|
|
94
|
+
if (!coerced) {
|
|
95
|
+
throw new Error(`Invalid memory type in --json payload. Expected one of: ${MEMORY_TYPE_CHOICES.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
type = coerced;
|
|
98
|
+
}
|
|
99
|
+
if (!tags) {
|
|
100
|
+
if (Array.isArray(parsed.tags)) {
|
|
101
|
+
tags = parsed.tags.map((t) => String(t)).join(',');
|
|
102
|
+
}
|
|
103
|
+
else if (typeof parsed.tags === 'string') {
|
|
104
|
+
tags = parsed.tags;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const parsedTopic = parsed.topic_id ?? parsed.topicId;
|
|
108
|
+
if (!topicId && typeof parsedTopic === 'string')
|
|
109
|
+
topicId = parsedTopic;
|
|
110
|
+
}
|
|
111
|
+
// 2) Content file (optional)
|
|
112
|
+
if (contentFile) {
|
|
113
|
+
content = await fs.readFile(contentFile, 'utf-8');
|
|
114
|
+
}
|
|
51
115
|
if (interactive || (!title || !content)) {
|
|
52
116
|
const inputMode = await resolveInputMode();
|
|
53
117
|
const answers = await inquirer.prompt([
|
|
@@ -62,7 +126,7 @@ export function memoryCommands(program) {
|
|
|
62
126
|
type: 'list',
|
|
63
127
|
name: 'type',
|
|
64
128
|
message: 'Memory type:',
|
|
65
|
-
choices: [
|
|
129
|
+
choices: [...MEMORY_TYPE_CHOICES],
|
|
66
130
|
default: type || 'context',
|
|
67
131
|
},
|
|
68
132
|
{
|
|
@@ -86,11 +150,12 @@ export function memoryCommands(program) {
|
|
|
86
150
|
if (!content || content.trim().length === 0) {
|
|
87
151
|
throw new Error('Content is required');
|
|
88
152
|
}
|
|
153
|
+
const resolvedType = type ?? 'context';
|
|
89
154
|
const spinner = ora('Creating memory...').start();
|
|
90
155
|
const memoryData = {
|
|
91
156
|
title,
|
|
92
157
|
content,
|
|
93
|
-
memory_type:
|
|
158
|
+
memory_type: resolvedType
|
|
94
159
|
};
|
|
95
160
|
if (tags) {
|
|
96
161
|
memoryData.tags = tags.split(',').map((tag) => tag.trim()).filter(Boolean);
|
|
@@ -115,6 +180,211 @@ export function memoryCommands(program) {
|
|
|
115
180
|
process.exit(1);
|
|
116
181
|
}
|
|
117
182
|
});
|
|
183
|
+
// Save current working session context as a memory
|
|
184
|
+
program
|
|
185
|
+
.command('save-session')
|
|
186
|
+
.description('Save current session context (git branch/status + optional test summary) as a memory')
|
|
187
|
+
.option('-t, --title <title>', 'memory title', 'Session summary')
|
|
188
|
+
.option('--type <type>', `memory type (${MEMORY_TYPE_CHOICES.join(', ')})`, 'project')
|
|
189
|
+
.option('--tags <tags>', 'comma-separated tags', 'session,cli')
|
|
190
|
+
.option('--test-summary <text>', 'Optional test summary to include')
|
|
191
|
+
.action(async (options) => {
|
|
192
|
+
try {
|
|
193
|
+
const spinner = ora('Collecting session info...').start();
|
|
194
|
+
const cwd = process.cwd();
|
|
195
|
+
const runGit = async (cmd) => {
|
|
196
|
+
try {
|
|
197
|
+
const { stdout } = await exec(cmd, { cwd });
|
|
198
|
+
return String(stdout || '').trim();
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const branch = await runGit('git rev-parse --abbrev-ref HEAD');
|
|
205
|
+
const status = await runGit('git status --porcelain');
|
|
206
|
+
const changedFiles = status
|
|
207
|
+
? status
|
|
208
|
+
.split('\n')
|
|
209
|
+
.map((line) => line.trim())
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.map((line) => line.replace(/^.. /, ''))
|
|
212
|
+
: [];
|
|
213
|
+
const lines = [];
|
|
214
|
+
lines.push(`# Session Summary`);
|
|
215
|
+
lines.push('');
|
|
216
|
+
lines.push(`- Date: ${new Date().toISOString()}`);
|
|
217
|
+
lines.push(`- CWD: ${cwd}`);
|
|
218
|
+
if (branch)
|
|
219
|
+
lines.push(`- Git branch: ${branch}`);
|
|
220
|
+
lines.push('');
|
|
221
|
+
lines.push('## Changes');
|
|
222
|
+
if (changedFiles.length === 0) {
|
|
223
|
+
lines.push('No uncommitted changes detected (or git not available).');
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
lines.push(changedFiles.map((f) => `- ${f}`).join('\n'));
|
|
227
|
+
}
|
|
228
|
+
lines.push('');
|
|
229
|
+
if (options.testSummary) {
|
|
230
|
+
lines.push('## Test Summary');
|
|
231
|
+
lines.push(options.testSummary);
|
|
232
|
+
lines.push('');
|
|
233
|
+
}
|
|
234
|
+
spinner.text = 'Saving session memory...';
|
|
235
|
+
const resolvedType = coerceMemoryType(options.type) ?? 'project';
|
|
236
|
+
const memoryData = {
|
|
237
|
+
title: options.title || 'Session summary',
|
|
238
|
+
content: lines.join('\n'),
|
|
239
|
+
memory_type: resolvedType
|
|
240
|
+
};
|
|
241
|
+
if (options.tags) {
|
|
242
|
+
memoryData.tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
243
|
+
}
|
|
244
|
+
const memory = await apiClient.createMemory(memoryData);
|
|
245
|
+
spinner.succeed('Session saved');
|
|
246
|
+
console.log();
|
|
247
|
+
console.log(chalk.green('✓ Memory created:'));
|
|
248
|
+
console.log(` ID: ${chalk.cyan(memory.id)}`);
|
|
249
|
+
console.log(` Title: ${memory.title}`);
|
|
250
|
+
console.log(` Type: ${memory.memory_type}`);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
254
|
+
console.error(chalk.red('✖ Failed to save session:'), errorMessage);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// Session management helpers (sessions are stored as memory entries tagged `session,cli` by default)
|
|
259
|
+
program
|
|
260
|
+
.command('list-sessions')
|
|
261
|
+
.description('List saved CLI sessions (memories tagged session,cli by default)')
|
|
262
|
+
.option('-p, --page <page>', 'page number', '1')
|
|
263
|
+
.option('-l, --limit <limit>', 'number of entries per page', '20')
|
|
264
|
+
.option('--type <type>', 'filter by memory type', 'project')
|
|
265
|
+
.option('--tags <tags>', 'filter by tags (comma-separated)', 'session,cli')
|
|
266
|
+
.option('--sort <field>', 'sort by field (created_at, updated_at, title, last_accessed)', 'created_at')
|
|
267
|
+
.option('--order <order>', 'sort order (asc, desc)', 'desc')
|
|
268
|
+
.action(async (options) => {
|
|
269
|
+
try {
|
|
270
|
+
const spinner = ora('Fetching sessions...').start();
|
|
271
|
+
const params = {
|
|
272
|
+
page: parseInt(options.page || '1'),
|
|
273
|
+
limit: parseInt(options.limit || '20'),
|
|
274
|
+
sort: options.sort || 'created_at',
|
|
275
|
+
order: options.order || 'desc'
|
|
276
|
+
};
|
|
277
|
+
if (options.type)
|
|
278
|
+
params.memory_type = options.type;
|
|
279
|
+
if (options.tags)
|
|
280
|
+
params.tags = options.tags;
|
|
281
|
+
const result = await apiClient.getMemories(params);
|
|
282
|
+
spinner.stop();
|
|
283
|
+
const memories = result.memories || result.data || [];
|
|
284
|
+
if (memories.length === 0) {
|
|
285
|
+
console.log(chalk.yellow('No sessions found'));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk.blue.bold(`\n📚 Sessions (${result.pagination.total} total)`));
|
|
289
|
+
console.log(chalk.gray(`Page ${result.pagination.page || 1} of ${result.pagination.pages || Math.ceil(result.pagination.total / result.pagination.limit)}`));
|
|
290
|
+
console.log();
|
|
291
|
+
const outputFormat = process.env.CLI_OUTPUT_FORMAT || 'table';
|
|
292
|
+
if (outputFormat === 'json') {
|
|
293
|
+
console.log(JSON.stringify(result, null, 2));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const tableData = memories.map((memory) => [
|
|
297
|
+
truncateText(memory.title, 30),
|
|
298
|
+
memory.memory_type,
|
|
299
|
+
memory.tags.slice(0, 3).join(', '),
|
|
300
|
+
format(new Date(memory.created_at), 'MMM dd, yyyy'),
|
|
301
|
+
memory.access_count
|
|
302
|
+
]);
|
|
303
|
+
const tableConfig = {
|
|
304
|
+
header: ['Title', 'Type', 'Tags', 'Created', 'Access'],
|
|
305
|
+
columnDefault: {
|
|
306
|
+
width: 20,
|
|
307
|
+
wrapWord: true
|
|
308
|
+
},
|
|
309
|
+
columns: [
|
|
310
|
+
{ width: 30 },
|
|
311
|
+
{ width: 12 },
|
|
312
|
+
{ width: 20 },
|
|
313
|
+
{ width: 12 },
|
|
314
|
+
{ width: 8 }
|
|
315
|
+
]
|
|
316
|
+
};
|
|
317
|
+
console.log(table([tableConfig.header, ...tableData], {
|
|
318
|
+
columnDefault: tableConfig.columnDefault,
|
|
319
|
+
columns: tableConfig.columns
|
|
320
|
+
}));
|
|
321
|
+
if (result.pagination.pages > 1) {
|
|
322
|
+
console.log(chalk.gray(`\nUse --page ${result.pagination.page + 1} for next page`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
327
|
+
console.error(chalk.red('✖ Failed to list sessions:'), errorMessage);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
program
|
|
332
|
+
.command('load-session')
|
|
333
|
+
.description('Load a saved session by memory ID (prints the saved session context)')
|
|
334
|
+
.argument('<id>', 'session memory ID')
|
|
335
|
+
.action(async (id) => {
|
|
336
|
+
try {
|
|
337
|
+
const spinner = ora('Loading session...').start();
|
|
338
|
+
const memory = await apiClient.getMemory(id);
|
|
339
|
+
spinner.stop();
|
|
340
|
+
const outputFormat = process.env.CLI_OUTPUT_FORMAT || 'text';
|
|
341
|
+
if (outputFormat === 'json') {
|
|
342
|
+
console.log(JSON.stringify(memory, null, 2));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
console.log(chalk.blue.bold('\n📌 Session'));
|
|
346
|
+
console.log(chalk.gray(`${memory.title} (${memory.id})`));
|
|
347
|
+
console.log();
|
|
348
|
+
console.log(memory.content);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
352
|
+
console.error(chalk.red('✖ Failed to load session:'), errorMessage);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
program
|
|
357
|
+
.command('delete-session')
|
|
358
|
+
.description('Delete a saved session by memory ID')
|
|
359
|
+
.argument('<id>', 'session memory ID')
|
|
360
|
+
.option('-f, --force', 'skip confirmation')
|
|
361
|
+
.action(async (id, options) => {
|
|
362
|
+
try {
|
|
363
|
+
if (!options.force) {
|
|
364
|
+
const memory = await apiClient.getMemory(id);
|
|
365
|
+
const answer = await inquirer.prompt([
|
|
366
|
+
{
|
|
367
|
+
type: 'confirm',
|
|
368
|
+
name: 'confirm',
|
|
369
|
+
message: `Are you sure you want to delete session "${memory.title}"?`,
|
|
370
|
+
default: false
|
|
371
|
+
}
|
|
372
|
+
]);
|
|
373
|
+
if (!answer.confirm) {
|
|
374
|
+
console.log(chalk.yellow('Deletion cancelled'));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const spinner = ora('Deleting session...').start();
|
|
379
|
+
await apiClient.deleteMemory(id);
|
|
380
|
+
spinner.succeed('Session deleted successfully');
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
384
|
+
console.error(chalk.red('✖ Failed to delete session:'), errorMessage);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
118
388
|
// List memories
|
|
119
389
|
program
|
|
120
390
|
.command('list')
|
|
@@ -294,7 +564,7 @@ export function memoryCommands(program) {
|
|
|
294
564
|
.argument('<id>', 'memory ID')
|
|
295
565
|
.option('-t, --title <title>', 'new title')
|
|
296
566
|
.option('-c, --content <content>', 'new content')
|
|
297
|
-
.option('--type <type>',
|
|
567
|
+
.option('--type <type>', `new memory type (${MEMORY_TYPE_CHOICES.join(', ')})`)
|
|
298
568
|
.option('--tags <tags>', 'new tags (comma-separated)')
|
|
299
569
|
.option('-i, --interactive', 'interactive mode')
|
|
300
570
|
.action(async (id, options) => {
|
|
@@ -317,7 +587,7 @@ export function memoryCommands(program) {
|
|
|
317
587
|
type: 'list',
|
|
318
588
|
name: 'type',
|
|
319
589
|
message: 'Memory type:',
|
|
320
|
-
choices: [
|
|
590
|
+
choices: [...MEMORY_TYPE_CHOICES],
|
|
321
591
|
default: currentMemory.memory_type,
|
|
322
592
|
},
|
|
323
593
|
{
|
package/dist/index-simple.js
CHANGED
|
@@ -64,12 +64,23 @@ program
|
|
|
64
64
|
process.env.MEMORY_API_URL = opts.apiUrl;
|
|
65
65
|
}
|
|
66
66
|
process.env.CLI_OUTPUT_FORMAT = opts.output;
|
|
67
|
+
const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true' ||
|
|
68
|
+
process.env.CLI_FORCE_API === 'true' ||
|
|
69
|
+
process.env.ONASIS_FORCE_API === 'true';
|
|
70
|
+
const forceApiFromConfig = cliConfig.get('forceApi') === true ||
|
|
71
|
+
cliConfig.get('connectionTransport') === 'api';
|
|
72
|
+
const forceDirectApi = forceApiFromEnv || forceApiFromConfig || opts.mcp === false;
|
|
73
|
+
if (forceDirectApi) {
|
|
74
|
+
process.env.LANONASIS_FORCE_API = 'true';
|
|
75
|
+
}
|
|
67
76
|
// Auto-initialize MCP unless disabled
|
|
68
77
|
const isMcpFlow = actionCommand.name() === 'mcp' ||
|
|
69
78
|
actionCommand.parent?.name?.() === 'mcp' ||
|
|
70
79
|
actionCommand.name() === 'mcp-server' ||
|
|
71
80
|
actionCommand.parent?.name?.() === 'mcp-server';
|
|
72
|
-
|
|
81
|
+
const isConfigFlow = actionCommand.name() === 'config' ||
|
|
82
|
+
actionCommand.parent?.name?.() === 'config';
|
|
83
|
+
if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
|
|
73
84
|
try {
|
|
74
85
|
const client = getMCPClient();
|
|
75
86
|
if (!client.isConnectedToServer()) {
|
|
@@ -87,6 +98,9 @@ program
|
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
}
|
|
101
|
+
else if (forceDirectApi && process.env.CLI_VERBOSE === 'true') {
|
|
102
|
+
console.log(colors.muted('MCP auto-connect skipped (force direct API enabled)'));
|
|
103
|
+
}
|
|
90
104
|
});
|
|
91
105
|
// Enhanced global error handler
|
|
92
106
|
process.on('uncaughtException', (error) => {
|
|
@@ -324,11 +338,10 @@ const topicCmd = program
|
|
|
324
338
|
.description('Topic management commands');
|
|
325
339
|
requireAuth(topicCmd);
|
|
326
340
|
topicCommands(topicCmd);
|
|
327
|
-
// Configuration commands (
|
|
341
|
+
// Configuration commands (no auth required)
|
|
328
342
|
const configCmd = program
|
|
329
343
|
.command('config')
|
|
330
344
|
.description('Configuration management');
|
|
331
|
-
requireAuth(configCmd);
|
|
332
345
|
configCommands(configCmd);
|
|
333
346
|
// Organization commands (require auth)
|
|
334
347
|
const orgCmd = program
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,18 @@ program
|
|
|
53
53
|
process.env.MEMORY_API_URL = opts.apiUrl;
|
|
54
54
|
}
|
|
55
55
|
process.env.CLI_OUTPUT_FORMAT = opts.output;
|
|
56
|
+
const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true' ||
|
|
57
|
+
process.env.CLI_FORCE_API === 'true' ||
|
|
58
|
+
process.env.ONASIS_FORCE_API === 'true';
|
|
59
|
+
const forceApiFromConfig = cliConfig.get('forceApi') === true ||
|
|
60
|
+
cliConfig.get('connectionTransport') === 'api';
|
|
61
|
+
const forceDirectApi = forceApiFromEnv || forceApiFromConfig || opts.mcp === false;
|
|
62
|
+
if (process.env.CLI_VERBOSE === 'true') {
|
|
63
|
+
console.log(colors.muted(`transport flags: env=${forceApiFromEnv} config=${forceApiFromConfig} no_mcp=${opts.mcp === false}`));
|
|
64
|
+
}
|
|
65
|
+
if (forceDirectApi) {
|
|
66
|
+
process.env.LANONASIS_FORCE_API = 'true';
|
|
67
|
+
}
|
|
56
68
|
const skipOnboarding = actionCommand.name() === 'init' ||
|
|
57
69
|
actionCommand.name() === 'auth' ||
|
|
58
70
|
actionCommand.parent?.name?.() === 'auth';
|
|
@@ -76,7 +88,9 @@ program
|
|
|
76
88
|
actionCommand.parent?.name?.() === 'mcp' ||
|
|
77
89
|
actionCommand.name() === 'mcp-server' ||
|
|
78
90
|
actionCommand.parent?.name?.() === 'mcp-server';
|
|
79
|
-
|
|
91
|
+
const isConfigFlow = actionCommand.name() === 'config' ||
|
|
92
|
+
actionCommand.parent?.name?.() === 'config';
|
|
93
|
+
if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
|
|
80
94
|
try {
|
|
81
95
|
const client = getMCPClient();
|
|
82
96
|
if (!client.isConnectedToServer()) {
|
|
@@ -94,6 +108,9 @@ program
|
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
110
|
}
|
|
111
|
+
else if (forceDirectApi && process.env.CLI_VERBOSE === 'true') {
|
|
112
|
+
console.log(colors.muted('MCP auto-connect skipped (force direct API enabled)'));
|
|
113
|
+
}
|
|
97
114
|
});
|
|
98
115
|
// Enhanced global error handler
|
|
99
116
|
process.on('uncaughtException', (error) => {
|
|
@@ -399,11 +416,10 @@ const topicCmd = program
|
|
|
399
416
|
.description('Topic management commands');
|
|
400
417
|
requireAuth(topicCmd);
|
|
401
418
|
topicCommands(topicCmd);
|
|
402
|
-
// Configuration commands (
|
|
419
|
+
// Configuration commands (no auth required)
|
|
403
420
|
const configCmd = program
|
|
404
421
|
.command('config')
|
|
405
422
|
.description('Configuration management');
|
|
406
|
-
requireAuth(configCmd);
|
|
407
423
|
configCommands(configCmd);
|
|
408
424
|
// Organization commands (require auth)
|
|
409
425
|
const orgCmd = program
|
|
@@ -91,18 +91,18 @@ export declare const MemoryListSchema: z.ZodObject<{
|
|
|
91
91
|
tags?: string[];
|
|
92
92
|
limit?: number;
|
|
93
93
|
topic_id?: string;
|
|
94
|
+
order?: "desc" | "asc";
|
|
94
95
|
memory_type?: "context" | "reference" | "note";
|
|
95
96
|
offset?: number;
|
|
96
97
|
sort_by?: "title" | "created_at" | "updated_at";
|
|
97
|
-
order?: "desc" | "asc";
|
|
98
98
|
}, {
|
|
99
99
|
tags?: string[];
|
|
100
100
|
limit?: number;
|
|
101
101
|
topic_id?: string;
|
|
102
|
+
order?: "desc" | "asc";
|
|
102
103
|
memory_type?: "context" | "reference" | "note";
|
|
103
104
|
offset?: number;
|
|
104
105
|
sort_by?: "title" | "created_at" | "updated_at";
|
|
105
|
-
order?: "desc" | "asc";
|
|
106
106
|
}>;
|
|
107
107
|
export declare const TopicCreateSchema: z.ZodObject<{
|
|
108
108
|
name: z.ZodString;
|
|
@@ -201,14 +201,14 @@ export declare const SystemConfigSchema: z.ZodObject<{
|
|
|
201
201
|
scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
|
|
202
202
|
}, "strip", z.ZodTypeAny, {
|
|
203
203
|
value?: any;
|
|
204
|
-
key?: string;
|
|
205
204
|
action?: "get" | "set" | "reset";
|
|
206
205
|
scope?: "user" | "global";
|
|
206
|
+
key?: string;
|
|
207
207
|
}, {
|
|
208
208
|
value?: any;
|
|
209
|
-
key?: string;
|
|
210
209
|
action?: "get" | "set" | "reset";
|
|
211
210
|
scope?: "user" | "global";
|
|
211
|
+
key?: string;
|
|
212
212
|
}>;
|
|
213
213
|
export declare const BulkOperationSchema: z.ZodObject<{
|
|
214
214
|
operation: z.ZodEnum<["create", "update", "delete"]>;
|
|
@@ -463,18 +463,18 @@ export declare const MCPSchemas: {
|
|
|
463
463
|
tags?: string[];
|
|
464
464
|
limit?: number;
|
|
465
465
|
topic_id?: string;
|
|
466
|
+
order?: "desc" | "asc";
|
|
466
467
|
memory_type?: "context" | "reference" | "note";
|
|
467
468
|
offset?: number;
|
|
468
469
|
sort_by?: "title" | "created_at" | "updated_at";
|
|
469
|
-
order?: "desc" | "asc";
|
|
470
470
|
}, {
|
|
471
471
|
tags?: string[];
|
|
472
472
|
limit?: number;
|
|
473
473
|
topic_id?: string;
|
|
474
|
+
order?: "desc" | "asc";
|
|
474
475
|
memory_type?: "context" | "reference" | "note";
|
|
475
476
|
offset?: number;
|
|
476
477
|
sort_by?: "title" | "created_at" | "updated_at";
|
|
477
|
-
order?: "desc" | "asc";
|
|
478
478
|
}>;
|
|
479
479
|
};
|
|
480
480
|
topic: {
|
|
@@ -579,14 +579,14 @@ export declare const MCPSchemas: {
|
|
|
579
579
|
scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
|
|
580
580
|
}, "strip", z.ZodTypeAny, {
|
|
581
581
|
value?: any;
|
|
582
|
-
key?: string;
|
|
583
582
|
action?: "get" | "set" | "reset";
|
|
584
583
|
scope?: "user" | "global";
|
|
584
|
+
key?: string;
|
|
585
585
|
}, {
|
|
586
586
|
value?: any;
|
|
587
|
-
key?: string;
|
|
588
587
|
action?: "get" | "set" | "reset";
|
|
589
588
|
scope?: "user" | "global";
|
|
589
|
+
key?: string;
|
|
590
590
|
}>;
|
|
591
591
|
};
|
|
592
592
|
operations: {
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -54,11 +54,15 @@ export interface UpdateMemoryRequest {
|
|
|
54
54
|
metadata?: Record<string, unknown>;
|
|
55
55
|
}
|
|
56
56
|
export interface GetMemoriesParams {
|
|
57
|
+
page?: number;
|
|
57
58
|
limit?: number;
|
|
58
59
|
offset?: number;
|
|
59
60
|
memory_type?: MemoryType;
|
|
60
61
|
tags?: string[] | string;
|
|
61
62
|
topic_id?: string;
|
|
63
|
+
user_id?: string;
|
|
64
|
+
sort?: 'created_at' | 'updated_at' | 'last_accessed' | 'access_count' | 'title';
|
|
65
|
+
order?: 'asc' | 'desc';
|
|
62
66
|
sort_by?: 'created_at' | 'updated_at' | 'last_accessed' | 'access_count';
|
|
63
67
|
sort_order?: 'asc' | 'desc';
|
|
64
68
|
}
|
|
@@ -148,6 +152,7 @@ export interface ApiErrorResponse {
|
|
|
148
152
|
export declare class APIClient {
|
|
149
153
|
private client;
|
|
150
154
|
private config;
|
|
155
|
+
private normalizeMemoryEntry;
|
|
151
156
|
constructor();
|
|
152
157
|
login(email: string, password: string): Promise<AuthResponse>;
|
|
153
158
|
register(email: string, password: string, organizationName?: string): Promise<AuthResponse>;
|
package/dist/utils/api.js
CHANGED
|
@@ -5,6 +5,26 @@ import { CLIConfig } from './config.js';
|
|
|
5
5
|
export class APIClient {
|
|
6
6
|
client;
|
|
7
7
|
config;
|
|
8
|
+
normalizeMemoryEntry(payload) {
|
|
9
|
+
// API responses are inconsistent across gateways:
|
|
10
|
+
// - Some return the memory entry directly
|
|
11
|
+
// - Some wrap it in `{ data: <memory>, message?: string }`
|
|
12
|
+
if (payload && typeof payload === 'object') {
|
|
13
|
+
const obj = payload;
|
|
14
|
+
const directId = obj.id;
|
|
15
|
+
if (typeof directId === 'string' && directId.length > 0) {
|
|
16
|
+
return payload;
|
|
17
|
+
}
|
|
18
|
+
const data = obj.data;
|
|
19
|
+
if (data && typeof data === 'object') {
|
|
20
|
+
const dataObj = data;
|
|
21
|
+
if (typeof dataObj.id === 'string' && dataObj.id.length > 0) {
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return payload;
|
|
27
|
+
}
|
|
8
28
|
constructor() {
|
|
9
29
|
this.config = new CLIConfig();
|
|
10
30
|
this.client = axios.create({
|
|
@@ -13,6 +33,8 @@ export class APIClient {
|
|
|
13
33
|
// Setup request interceptor to add auth token and headers
|
|
14
34
|
this.client.interceptors.request.use(async (config) => {
|
|
15
35
|
await this.config.init();
|
|
36
|
+
// Keep OAuth sessions alive automatically (prevents intermittent "auth required" cutouts).
|
|
37
|
+
await this.config.refreshTokenIfNeeded();
|
|
16
38
|
// Service Discovery
|
|
17
39
|
await this.config.discoverServices();
|
|
18
40
|
// Use appropriate base URL based on endpoint and auth method
|
|
@@ -20,17 +42,26 @@ export class APIClient {
|
|
|
20
42
|
const discoveredServices = this.config.get('discoveredServices');
|
|
21
43
|
const authMethod = this.config.get('authMethod');
|
|
22
44
|
const vendorKey = await this.config.getVendorKeyAsync();
|
|
45
|
+
const token = this.config.getToken();
|
|
46
|
+
const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true'
|
|
47
|
+
|| process.env.CLI_FORCE_API === 'true'
|
|
48
|
+
|| process.env.ONASIS_FORCE_API === 'true';
|
|
49
|
+
const forceApiFromConfig = this.config.get('forceApi') === true
|
|
50
|
+
|| this.config.get('connectionTransport') === 'api';
|
|
51
|
+
const forceDirectApi = forceApiFromEnv || forceApiFromConfig;
|
|
52
|
+
const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
|
|
53
|
+
const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
|
|
23
54
|
// Determine the correct API base URL:
|
|
24
55
|
// - Auth endpoints -> auth.lanonasis.com
|
|
25
56
|
// - JWT auth (no vendor key) -> mcp.lanonasis.com (supports JWT tokens)
|
|
26
57
|
// - Vendor key auth -> api.lanonasis.com (requires vendor key)
|
|
27
58
|
let apiBaseUrl;
|
|
28
|
-
const useMcpServer = !
|
|
59
|
+
const useMcpServer = !forceDirectApi && prefersTokenAuth && !useVendorKeyAuth;
|
|
29
60
|
if (isAuthEndpoint) {
|
|
30
61
|
apiBaseUrl = discoveredServices?.auth_base || 'https://auth.lanonasis.com';
|
|
31
62
|
}
|
|
32
|
-
else if (
|
|
33
|
-
//
|
|
63
|
+
else if (forceDirectApi) {
|
|
64
|
+
// Force direct REST API mode to bypass MCP routing for troubleshooting.
|
|
34
65
|
apiBaseUrl = this.config.getApiUrl();
|
|
35
66
|
}
|
|
36
67
|
else if (useMcpServer) {
|
|
@@ -51,8 +82,22 @@ export class APIClient {
|
|
|
51
82
|
config.headers['X-Project-Scope'] = 'lanonasis-maas';
|
|
52
83
|
}
|
|
53
84
|
// Enhanced Authentication Support
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
// Even in forced direct-API mode, prefer bearer token auth when available.
|
|
86
|
+
// This avoids accidentally sending an OAuth access token as X-API-Key (we store it
|
|
87
|
+
// in secure storage for MCP/WebSocket usage), which can cause 401s.
|
|
88
|
+
const preferVendorKeyInDirectApiMode = forceDirectApi && Boolean(vendorKey) && !prefersTokenAuth;
|
|
89
|
+
if (preferVendorKeyInDirectApiMode) {
|
|
90
|
+
// Vendor key authentication (validated server-side)
|
|
91
|
+
// Send raw key - server handles hashing for comparison
|
|
92
|
+
config.headers['X-API-Key'] = vendorKey;
|
|
93
|
+
config.headers['X-Auth-Method'] = 'vendor_key';
|
|
94
|
+
}
|
|
95
|
+
else if (prefersTokenAuth) {
|
|
96
|
+
// JWT/OAuth token authentication takes precedence when both are present.
|
|
97
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
98
|
+
config.headers['X-Auth-Method'] = 'jwt';
|
|
99
|
+
}
|
|
100
|
+
else if (vendorKey) {
|
|
56
101
|
// Vendor key authentication (validated server-side)
|
|
57
102
|
// Send raw key - server handles hashing for comparison
|
|
58
103
|
config.headers['X-API-Key'] = vendorKey;
|
|
@@ -69,7 +114,10 @@ export class APIClient {
|
|
|
69
114
|
// Add project scope for Golden Contract compliance
|
|
70
115
|
config.headers['X-Project-Scope'] = 'lanonasis-maas';
|
|
71
116
|
if (process.env.CLI_VERBOSE === 'true') {
|
|
117
|
+
const transportMode = forceDirectApi ? 'api-forced' : (useMcpServer ? 'mcp-http' : 'api');
|
|
118
|
+
config.headers['X-Transport-Mode'] = transportMode;
|
|
72
119
|
console.log(chalk.dim(`→ ${config.method?.toUpperCase()} ${config.url} [${requestId}]`));
|
|
120
|
+
console.log(chalk.dim(` transport=${transportMode} baseURL=${config.baseURL}`));
|
|
73
121
|
}
|
|
74
122
|
return config;
|
|
75
123
|
});
|
|
@@ -84,7 +132,7 @@ export class APIClient {
|
|
|
84
132
|
const { status, data } = error.response;
|
|
85
133
|
if (status === 401) {
|
|
86
134
|
console.error(chalk.red('✖ Authentication failed'));
|
|
87
|
-
console.log(chalk.yellow('Please run:'), chalk.white('
|
|
135
|
+
console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
|
|
88
136
|
process.exit(1);
|
|
89
137
|
}
|
|
90
138
|
if (status === 403) {
|
|
@@ -127,19 +175,88 @@ export class APIClient {
|
|
|
127
175
|
// All memory endpoints use /api/v1/memories path (plural, per REST conventions)
|
|
128
176
|
async createMemory(data) {
|
|
129
177
|
const response = await this.client.post('/api/v1/memories', data);
|
|
130
|
-
return response.data;
|
|
178
|
+
return this.normalizeMemoryEntry(response.data);
|
|
131
179
|
}
|
|
132
180
|
async getMemories(params = {}) {
|
|
133
|
-
|
|
134
|
-
|
|
181
|
+
try {
|
|
182
|
+
const response = await this.client.get('/api/v1/memories', { params });
|
|
183
|
+
return response.data;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
// Backward-compatible fallback: newer API contracts may reject GET list and prefer search-only.
|
|
187
|
+
if (error?.response?.status === 405) {
|
|
188
|
+
const limit = Number(params.limit || 20);
|
|
189
|
+
const page = Number(params.page || 1);
|
|
190
|
+
const offset = Number(params.offset ?? Math.max(0, (page - 1) * limit));
|
|
191
|
+
const searchPayload = {
|
|
192
|
+
query: '*',
|
|
193
|
+
limit,
|
|
194
|
+
threshold: 0
|
|
195
|
+
};
|
|
196
|
+
if (params.memory_type) {
|
|
197
|
+
searchPayload.memory_types = [params.memory_type];
|
|
198
|
+
}
|
|
199
|
+
if (params.tags) {
|
|
200
|
+
searchPayload.tags = Array.isArray(params.tags)
|
|
201
|
+
? params.tags
|
|
202
|
+
: String(params.tags).split(',').map((tag) => tag.trim()).filter(Boolean);
|
|
203
|
+
}
|
|
204
|
+
if (params.topic_id) {
|
|
205
|
+
searchPayload.topic_id = params.topic_id;
|
|
206
|
+
}
|
|
207
|
+
if (offset > 0) {
|
|
208
|
+
searchPayload.offset = offset;
|
|
209
|
+
}
|
|
210
|
+
const fallback = await this.client.post('/api/v1/memories/search', searchPayload);
|
|
211
|
+
const payload = fallback.data || {};
|
|
212
|
+
const resultsArray = Array.isArray(payload.data)
|
|
213
|
+
? payload.data
|
|
214
|
+
: Array.isArray(payload.results)
|
|
215
|
+
? payload.results
|
|
216
|
+
: [];
|
|
217
|
+
const memories = resultsArray
|
|
218
|
+
.map((entry) => {
|
|
219
|
+
// Some gateways/search endpoints wrap results as `{ data: <memory> }`.
|
|
220
|
+
if (entry && typeof entry === 'object') {
|
|
221
|
+
const obj = entry;
|
|
222
|
+
const data = obj.data;
|
|
223
|
+
if (data && typeof data === 'object') {
|
|
224
|
+
const dataObj = data;
|
|
225
|
+
if (typeof dataObj.id === 'string' && dataObj.id.length > 0) {
|
|
226
|
+
return data;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return entry;
|
|
231
|
+
})
|
|
232
|
+
.map((entry) => this.normalizeMemoryEntry(entry));
|
|
233
|
+
const total = Number.isFinite(payload.total) ? Number(payload.total) : memories.length;
|
|
234
|
+
const pages = Math.max(1, Math.ceil(total / limit));
|
|
235
|
+
const currentPage = Math.max(1, Math.floor(offset / limit) + 1);
|
|
236
|
+
return {
|
|
237
|
+
...payload,
|
|
238
|
+
data: memories,
|
|
239
|
+
memories,
|
|
240
|
+
pagination: {
|
|
241
|
+
total,
|
|
242
|
+
limit,
|
|
243
|
+
offset,
|
|
244
|
+
has_more: (offset + memories.length) < total,
|
|
245
|
+
page: currentPage,
|
|
246
|
+
pages
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
135
252
|
}
|
|
136
253
|
async getMemory(id) {
|
|
137
254
|
const response = await this.client.get(`/api/v1/memories/${id}`);
|
|
138
|
-
return response.data;
|
|
255
|
+
return this.normalizeMemoryEntry(response.data);
|
|
139
256
|
}
|
|
140
257
|
async updateMemory(id, data) {
|
|
141
258
|
const response = await this.client.put(`/api/v1/memories/${id}`, data);
|
|
142
|
-
return response.data;
|
|
259
|
+
return this.normalizeMemoryEntry(response.data);
|
|
143
260
|
}
|
|
144
261
|
async deleteMemory(id) {
|
|
145
262
|
await this.client.delete(`/api/v1/memories/${id}`);
|
package/dist/utils/config.js
CHANGED
|
@@ -678,6 +678,9 @@ export class CLIConfig {
|
|
|
678
678
|
return this.config.user;
|
|
679
679
|
}
|
|
680
680
|
async isAuthenticated() {
|
|
681
|
+
// Attempt refresh for OAuth sessions before checks (prevents intermittent auth dropouts).
|
|
682
|
+
// This is safe to call even when not using OAuth; it will no-op.
|
|
683
|
+
await this.refreshTokenIfNeeded();
|
|
681
684
|
// Check if using vendor key authentication
|
|
682
685
|
if (this.config.authMethod === 'vendor_key') {
|
|
683
686
|
// Use async method to read from encrypted ApiKeyStorage
|
|
@@ -736,6 +739,16 @@ export class CLIConfig {
|
|
|
736
739
|
const token = this.getToken();
|
|
737
740
|
if (!token)
|
|
738
741
|
return false;
|
|
742
|
+
// OAuth tokens are often opaque (not JWT). Prefer local expiry metadata when present.
|
|
743
|
+
if (this.config.authMethod === 'oauth') {
|
|
744
|
+
const tokenExpiresAt = this.get('token_expires_at');
|
|
745
|
+
if (typeof tokenExpiresAt === 'number') {
|
|
746
|
+
const isValid = Date.now() < tokenExpiresAt;
|
|
747
|
+
this.authCheckCache = { isValid, timestamp: Date.now() };
|
|
748
|
+
return isValid;
|
|
749
|
+
}
|
|
750
|
+
// Fall through to legacy validation when we don't have expiry metadata.
|
|
751
|
+
}
|
|
739
752
|
// Check cache first
|
|
740
753
|
if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
|
|
741
754
|
return this.authCheckCache.isValid;
|
|
@@ -968,11 +981,85 @@ export class CLIConfig {
|
|
|
968
981
|
return;
|
|
969
982
|
}
|
|
970
983
|
try {
|
|
984
|
+
// OAuth token refresh (opaque tokens + refresh_token + token_expires_at)
|
|
985
|
+
if (this.config.authMethod === 'oauth') {
|
|
986
|
+
const refreshToken = this.get('refresh_token');
|
|
987
|
+
if (!refreshToken) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const tokenExpiresAtRaw = this.get('token_expires_at');
|
|
991
|
+
const tokenExpiresAt = (() => {
|
|
992
|
+
const n = typeof tokenExpiresAtRaw === 'number'
|
|
993
|
+
? tokenExpiresAtRaw
|
|
994
|
+
: typeof tokenExpiresAtRaw === 'string'
|
|
995
|
+
? Number(tokenExpiresAtRaw)
|
|
996
|
+
: undefined;
|
|
997
|
+
if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) {
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
// Support both seconds and milliseconds since epoch.
|
|
1001
|
+
// Seconds are ~1.7e9; ms are ~1.7e12.
|
|
1002
|
+
return n < 1e11 ? n * 1000 : n;
|
|
1003
|
+
})();
|
|
1004
|
+
const nowMs = Date.now();
|
|
1005
|
+
const refreshWindowMs = 5 * 60 * 1000; // 5 minutes
|
|
1006
|
+
// If we don't know expiry, don't force a refresh.
|
|
1007
|
+
if (typeof tokenExpiresAt !== 'number' || nowMs < (tokenExpiresAt - refreshWindowMs)) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
await this.discoverServices();
|
|
1011
|
+
const authBase = this.getDiscoveredApiUrl();
|
|
1012
|
+
const resp = await axios.post(`${authBase}/oauth/token`, {
|
|
1013
|
+
grant_type: 'refresh_token',
|
|
1014
|
+
refresh_token: refreshToken,
|
|
1015
|
+
client_id: 'lanonasis-cli'
|
|
1016
|
+
}, {
|
|
1017
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1018
|
+
timeout: 10000,
|
|
1019
|
+
proxy: false
|
|
1020
|
+
});
|
|
1021
|
+
// Some gateways wrap responses as `{ data: { ... } }`.
|
|
1022
|
+
const raw = resp?.data;
|
|
1023
|
+
const payload = raw && typeof raw === 'object' && raw.data && typeof raw.data === 'object'
|
|
1024
|
+
? raw.data
|
|
1025
|
+
: raw;
|
|
1026
|
+
const accessToken = payload?.access_token ?? payload?.token;
|
|
1027
|
+
const refreshedRefreshToken = payload?.refresh_token;
|
|
1028
|
+
const expiresIn = payload?.expires_in;
|
|
1029
|
+
if (typeof accessToken !== 'string' || accessToken.length === 0) {
|
|
1030
|
+
throw new Error('Token refresh response missing access_token');
|
|
1031
|
+
}
|
|
1032
|
+
// setToken() assumes JWT by default; ensure authMethod stays oauth after storing.
|
|
1033
|
+
await this.setToken(accessToken);
|
|
1034
|
+
this.config.authMethod = 'oauth';
|
|
1035
|
+
if (typeof refreshedRefreshToken === 'string' && refreshedRefreshToken.length > 0) {
|
|
1036
|
+
this.config.refresh_token = refreshedRefreshToken;
|
|
1037
|
+
}
|
|
1038
|
+
if (typeof expiresIn === 'number' && Number.isFinite(expiresIn)) {
|
|
1039
|
+
this.config.token_expires_at = Date.now() + (expiresIn * 1000);
|
|
1040
|
+
}
|
|
1041
|
+
// Keep the encrypted "vendor key" in sync for MCP/WebSocket clients that use X-API-Key.
|
|
1042
|
+
// This does not change authMethod away from oauth (setVendorKey guards against that).
|
|
1043
|
+
try {
|
|
1044
|
+
await this.setVendorKey(accessToken);
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
// Non-fatal: bearer token refresh still helps API calls.
|
|
1048
|
+
}
|
|
1049
|
+
await this.save().catch(() => { });
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
971
1052
|
// Check if token is JWT and if it's close to expiry
|
|
972
1053
|
if (token.startsWith('cli_')) {
|
|
973
1054
|
// CLI tokens don't need refresh, they're long-lived
|
|
974
1055
|
return;
|
|
975
1056
|
}
|
|
1057
|
+
// Only attempt JWT refresh for tokens that look like JWTs.
|
|
1058
|
+
// OAuth access tokens in this system can be opaque strings; treating them as JWTs
|
|
1059
|
+
// creates noisy failures and can cause unwanted state writes.
|
|
1060
|
+
if (token.split('.').length !== 3) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
976
1063
|
const decoded = jwtDecode(token);
|
|
977
1064
|
const now = Date.now() / 1000;
|
|
978
1065
|
const exp = typeof decoded.exp === 'number' ? decoded.exp : 0;
|
|
@@ -152,7 +152,7 @@ export class TextInputHandlerImpl {
|
|
|
152
152
|
};
|
|
153
153
|
// Ensure stdin is flowing before adding listener
|
|
154
154
|
// This is critical after inquirer prompts which may pause stdin
|
|
155
|
-
|
|
155
|
+
this.resumeStdinIfSupported();
|
|
156
156
|
process.stdin.on('data', handleKeypress);
|
|
157
157
|
}
|
|
158
158
|
catch (error) {
|
|
@@ -168,13 +168,13 @@ export class TextInputHandlerImpl {
|
|
|
168
168
|
if (!this.isRawModeEnabled && process.stdin.isTTY) {
|
|
169
169
|
this.originalStdinMode = process.stdin.isRaw;
|
|
170
170
|
process.stdin.setRawMode(true);
|
|
171
|
-
|
|
171
|
+
this.resumeStdinIfSupported(); // Ensure stdin is flowing to receive data events
|
|
172
172
|
this.isRawModeEnabled = true;
|
|
173
173
|
}
|
|
174
174
|
else if (!process.stdin.isTTY) {
|
|
175
175
|
// Non-TTY mode - can't use raw mode, fall back to line mode
|
|
176
176
|
console.error('Warning: Not a TTY, inline text input may not work correctly');
|
|
177
|
-
|
|
177
|
+
this.resumeStdinIfSupported();
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
/**
|
|
@@ -302,6 +302,11 @@ export class TextInputHandlerImpl {
|
|
|
302
302
|
}
|
|
303
303
|
return key;
|
|
304
304
|
}
|
|
305
|
+
resumeStdinIfSupported() {
|
|
306
|
+
if (typeof process.stdin.resume === 'function') {
|
|
307
|
+
process.stdin.resume();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
305
310
|
/**
|
|
306
311
|
* Check if a key event matches a key pattern
|
|
307
312
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lanonasis/cli",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.5",
|
|
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",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"build": "rimraf dist && tsc -p tsconfig.json",
|
|
89
89
|
"prepublishOnly": "npm run build",
|
|
90
90
|
"postinstall": "node scripts/postinstall.js",
|
|
91
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
91
|
+
"test": "[ -f node_modules/jest/bin/jest.js ] || bun install --no-save; node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
92
92
|
"test:watch": "npm test -- --watch",
|
|
93
93
|
"test:coverage": "npm test -- --coverage"
|
|
94
94
|
}
|