@lifestreamdynamics/vault-cli 1.3.4 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,32 +4,45 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
5
  import { getCredentialManager } from '../config.js';
6
6
  import { confirmAction } from '../utils/confirm.js';
7
+ import { resolveVaultId } from '../utils/resolve-vault.js';
7
8
  export function registerDocCommands(program) {
8
9
  const docs = program.command('docs').description('Read, write, move, and delete documents in a vault');
9
10
  addGlobalFlags(docs.command('list')
10
11
  .description('List documents in a vault, optionally filtered by directory')
11
- .argument('<vaultId>', 'Vault ID')
12
+ .argument('<vaultId>', 'Vault ID or slug')
12
13
  .option('--dir <path>', 'Filter by directory path')
14
+ .option('--limit <n>', 'Maximum number of documents to return')
15
+ .option('--offset <n>', 'Number of documents to skip')
16
+ .option('--tag <tags>', 'Filter by tags (comma-separated)')
13
17
  .addHelpText('after', `
14
18
  EXAMPLES
15
19
  lsvault docs list abc123
16
- lsvault docs list abc123 --dir notes/meetings`))
20
+ lsvault docs list abc123 --dir notes/meetings
21
+ lsvault docs list abc123 --tag work,project --limit 20
22
+ lsvault docs list abc123 --limit 50 --offset 100`))
17
23
  .action(async (vaultId, _opts) => {
18
24
  const flags = resolveFlags(_opts);
19
25
  const out = createOutput(flags);
20
26
  out.startSpinner('Fetching documents...');
21
27
  try {
28
+ vaultId = await resolveVaultId(vaultId);
22
29
  const client = await getClientAsync();
23
- const documents = await client.documents.list(vaultId, _opts.dir);
30
+ const dirPath = _opts.dir ? String(_opts.dir).replace(/\/+$/, '') : undefined;
31
+ const limit = _opts.limit !== undefined ? Number(_opts.limit) : undefined;
32
+ const offset = _opts.offset !== undefined ? Number(_opts.offset) : undefined;
33
+ const tags = _opts.tag ? String(_opts.tag).split(',').map((t) => t.trim()).filter(Boolean) : undefined;
34
+ const documents = await client.documents.list(vaultId, dirPath, { limit, offset, tags });
24
35
  out.stopSpinner();
25
36
  out.list(documents.map(doc => ({
37
+ id: doc.id,
26
38
  path: doc.path,
27
- title: doc.title || '',
39
+ title: doc.title ?? null,
28
40
  tags: Array.isArray(doc.tags) ? doc.tags.join(', ') : '',
29
41
  sizeBytes: doc.sizeBytes,
30
42
  })), {
31
43
  emptyMessage: 'No documents found.',
32
44
  columns: [
45
+ { key: 'id', header: 'ID' },
33
46
  { key: 'path', header: 'Path' },
34
47
  { key: 'title', header: 'Title' },
35
48
  { key: 'tags', header: 'Tags' },
@@ -38,7 +51,7 @@ EXAMPLES
38
51
  textFn: (doc) => {
39
52
  const title = doc.title ? chalk.dim(` -- ${String(doc.title)}`) : '';
40
53
  const tags = doc.tags ? chalk.blue(` [${String(doc.tags)}]`) : '';
41
- return `${chalk.cyan(String(doc.path))}${title}${tags}`;
54
+ return `${chalk.cyan(String(doc.path))}${title}${tags} ${chalk.dim(String(doc.id))}`;
42
55
  },
43
56
  });
44
57
  if (flags.output === 'text' && documents.length > 0) {
@@ -51,7 +64,7 @@ EXAMPLES
51
64
  });
52
65
  addGlobalFlags(docs.command('get')
53
66
  .description('Print document content to stdout, or show metadata with --meta')
54
- .argument('<vaultId>', 'Vault ID')
67
+ .argument('<vaultId>', 'Vault ID or slug')
55
68
  .argument('<path>', 'Document path (e.g., notes/todo.md)')
56
69
  .option('--meta', 'Show metadata instead of content')
57
70
  .addHelpText('after', `
@@ -63,6 +76,7 @@ EXAMPLES
63
76
  const flags = resolveFlags(_opts);
64
77
  const out = createOutput(flags);
65
78
  try {
79
+ vaultId = await resolveVaultId(vaultId);
66
80
  const client = await getClientAsync();
67
81
  const result = await client.documents.get(vaultId, docPath);
68
82
  // Auto-decrypt if the document is encrypted
@@ -82,6 +96,7 @@ EXAMPLES
82
96
  if (_opts.meta) {
83
97
  const doc = result.document;
84
98
  out.record({
99
+ id: doc.id,
85
100
  path: doc.path,
86
101
  title: doc.title,
87
102
  sizeBytes: doc.sizeBytes,
@@ -93,6 +108,9 @@ EXAMPLES
93
108
  updatedAt: doc.updatedAt,
94
109
  });
95
110
  }
111
+ else if (flags.output === 'json') {
112
+ out.raw(JSON.stringify({ path: result.document.path, content: result.content }) + '\n');
113
+ }
96
114
  else {
97
115
  out.raw(result.content);
98
116
  }
@@ -103,7 +121,7 @@ EXAMPLES
103
121
  });
104
122
  addGlobalFlags(docs.command('put')
105
123
  .description('Create or update a document by reading content from stdin')
106
- .argument('<vaultId>', 'Vault ID')
124
+ .argument('<vaultId>', 'Vault ID or slug')
107
125
  .argument('<path>', 'Document path (must end with .md)')
108
126
  .addHelpText('after', `
109
127
  EXAMPLES
@@ -114,11 +132,19 @@ EXAMPLES
114
132
  const out = createOutput(flags);
115
133
  out.startSpinner('Reading stdin...');
116
134
  try {
135
+ vaultId = await resolveVaultId(vaultId);
117
136
  const content = await new Promise((resolve) => {
118
137
  let data = '';
119
138
  process.stdin.on('data', (chunk) => data += chunk);
120
139
  process.stdin.on('end', () => resolve(data));
121
140
  });
141
+ if (!content.trim()) {
142
+ out.failSpinner('No content received');
143
+ out.error('No content received on stdin. Pipe content to this command:');
144
+ out.status(chalk.dim(` echo "# Hello" | lsvault docs put ${vaultId} ${docPath}`));
145
+ process.exitCode = 1;
146
+ return;
147
+ }
122
148
  out.startSpinner('Uploading document...');
123
149
  const client = await getClientAsync();
124
150
  // Check if vault is encrypted and auto-encrypt
@@ -139,10 +165,10 @@ EXAMPLES
139
165
  else {
140
166
  doc = await client.documents.put(vaultId, docPath, content);
141
167
  }
142
- out.success(`Document saved: ${chalk.cyan(doc.path)} (${doc.sizeBytes} bytes)`, {
168
+ out.success(`Document saved: ${chalk.cyan(doc.path)} (${doc.sizeBytes ?? 0} bytes)`, {
143
169
  path: doc.path,
144
- sizeBytes: doc.sizeBytes,
145
- encrypted: doc.encrypted,
170
+ sizeBytes: doc.sizeBytes ?? 0,
171
+ encrypted: doc.encrypted ?? false,
146
172
  });
147
173
  }
148
174
  catch (err) {
@@ -151,13 +177,14 @@ EXAMPLES
151
177
  });
152
178
  addGlobalFlags(docs.command('delete')
153
179
  .description('Permanently delete a document from a vault')
154
- .argument('<vaultId>', 'Vault ID')
180
+ .argument('<vaultId>', 'Vault ID or slug')
155
181
  .argument('<path>', 'Document path to delete')
156
182
  .option('-y, --yes', 'Skip confirmation prompt'))
157
183
  .action(async (vaultId, docPath, _opts) => {
158
184
  const flags = resolveFlags(_opts);
159
185
  const out = createOutput(flags);
160
186
  try {
187
+ vaultId = await resolveVaultId(vaultId);
161
188
  const confirmed = await confirmAction(`Permanently delete "${docPath}" from vault ${vaultId}?`, { yes: _opts.yes });
162
189
  if (!confirmed) {
163
190
  out.status('Deletion cancelled.');
@@ -174,7 +201,7 @@ EXAMPLES
174
201
  });
175
202
  addGlobalFlags(docs.command('move')
176
203
  .description('Move or rename a document within a vault')
177
- .argument('<vaultId>', 'Vault ID')
204
+ .argument('<vaultId>', 'Vault ID or slug')
178
205
  .argument('<source>', 'Current document path')
179
206
  .argument('<dest>', 'New document path')
180
207
  .option('--overwrite', 'Overwrite if destination already exists')
@@ -185,8 +212,17 @@ EXAMPLES
185
212
  .action(async (vaultId, source, dest, _opts) => {
186
213
  const flags = resolveFlags(_opts);
187
214
  const out = createOutput(flags);
215
+ if (flags.dryRun) {
216
+ out.success(`[dry-run] Would move: ${source} → ${dest}`, {
217
+ dryRun: true,
218
+ source,
219
+ destination: dest,
220
+ });
221
+ return;
222
+ }
188
223
  out.startSpinner('Moving document...');
189
224
  try {
225
+ vaultId = await resolveVaultId(vaultId);
190
226
  const client = await getClientAsync();
191
227
  const result = await client.documents.move(vaultId, source, dest, _opts.overwrite);
192
228
  out.success(`Moved: ${chalk.cyan(result.source)} -> ${chalk.cyan(result.destination)}`, {
@@ -200,7 +236,7 @@ EXAMPLES
200
236
  });
201
237
  addGlobalFlags(docs.command('bulk-move')
202
238
  .description('Move multiple documents to a target directory')
203
- .argument('<vaultId>', 'Vault ID')
239
+ .argument('<vaultId>', 'Vault ID or slug')
204
240
  .requiredOption('--paths <csv>', 'Comma-separated list of document paths')
205
241
  .requiredOption('--target <dir>', 'Target directory'))
206
242
  .action(async (vaultId, _opts) => {
@@ -208,9 +244,10 @@ EXAMPLES
208
244
  const out = createOutput(flags);
209
245
  out.startSpinner('Moving documents...');
210
246
  try {
247
+ vaultId = await resolveVaultId(vaultId);
211
248
  const client = await getClientAsync();
212
249
  const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
213
- const result = await client.documents.bulkMove(vaultId, { paths, targetDirectory: _opts.target });
250
+ const result = await client.documents.bulkMove(vaultId, { items: paths, destination: _opts.target });
214
251
  out.stopSpinner();
215
252
  if (flags.output === 'json') {
216
253
  out.raw(JSON.stringify(result, null, 2) + '\n');
@@ -229,7 +266,7 @@ EXAMPLES
229
266
  });
230
267
  addGlobalFlags(docs.command('bulk-copy')
231
268
  .description('Copy multiple documents to a target directory')
232
- .argument('<vaultId>', 'Vault ID')
269
+ .argument('<vaultId>', 'Vault ID or slug')
233
270
  .requiredOption('--paths <csv>', 'Comma-separated list of document paths')
234
271
  .requiredOption('--target <dir>', 'Target directory'))
235
272
  .action(async (vaultId, _opts) => {
@@ -237,9 +274,10 @@ EXAMPLES
237
274
  const out = createOutput(flags);
238
275
  out.startSpinner('Copying documents...');
239
276
  try {
277
+ vaultId = await resolveVaultId(vaultId);
240
278
  const client = await getClientAsync();
241
279
  const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
242
- const result = await client.documents.bulkCopy(vaultId, { paths, targetDirectory: _opts.target });
280
+ const result = await client.documents.bulkCopy(vaultId, { items: paths, destination: _opts.target });
243
281
  out.stopSpinner();
244
282
  if (flags.output === 'json') {
245
283
  out.raw(JSON.stringify(result, null, 2) + '\n');
@@ -258,16 +296,17 @@ EXAMPLES
258
296
  });
259
297
  addGlobalFlags(docs.command('bulk-delete')
260
298
  .description('Delete multiple documents')
261
- .argument('<vaultId>', 'Vault ID')
299
+ .argument('<vaultId>', 'Vault ID or slug')
262
300
  .requiredOption('--paths <csv>', 'Comma-separated list of document paths'))
263
301
  .action(async (vaultId, _opts) => {
264
302
  const flags = resolveFlags(_opts);
265
303
  const out = createOutput(flags);
266
304
  out.startSpinner('Deleting documents...');
267
305
  try {
306
+ vaultId = await resolveVaultId(vaultId);
268
307
  const client = await getClientAsync();
269
308
  const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
270
- const result = await client.documents.bulkDelete(vaultId, { paths });
309
+ const result = await client.documents.bulkDelete(vaultId, { items: paths });
271
310
  out.stopSpinner();
272
311
  if (flags.output === 'json') {
273
312
  out.raw(JSON.stringify(result, null, 2) + '\n');
@@ -286,7 +325,7 @@ EXAMPLES
286
325
  });
287
326
  addGlobalFlags(docs.command('bulk-tag')
288
327
  .description('Add or remove tags from multiple documents')
289
- .argument('<vaultId>', 'Vault ID')
328
+ .argument('<vaultId>', 'Vault ID or slug')
290
329
  .requiredOption('--paths <csv>', 'Comma-separated list of document paths')
291
330
  .option('--add <csv>', 'Tags to add (comma-separated)')
292
331
  .option('--remove <csv>', 'Tags to remove (comma-separated)'))
@@ -300,11 +339,12 @@ EXAMPLES
300
339
  }
301
340
  out.startSpinner('Tagging documents...');
302
341
  try {
342
+ vaultId = await resolveVaultId(vaultId);
303
343
  const client = await getClientAsync();
304
344
  const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
305
345
  const addTags = _opts.add ? (String(_opts.add)).split(',').map(t => t.trim()).filter(Boolean) : undefined;
306
346
  const removeTags = _opts.remove ? (String(_opts.remove)).split(',').map(t => t.trim()).filter(Boolean) : undefined;
307
- const result = await client.documents.bulkTag(vaultId, { paths, addTags, removeTags });
347
+ const result = await client.documents.bulkTag(vaultId, { items: paths, addTags, removeTags });
308
348
  out.stopSpinner();
309
349
  if (flags.output === 'json') {
310
350
  out.raw(JSON.stringify(result, null, 2) + '\n');
@@ -323,16 +363,18 @@ EXAMPLES
323
363
  });
324
364
  addGlobalFlags(docs.command('mkdir')
325
365
  .description('Create a directory in a vault')
326
- .argument('<vaultId>', 'Vault ID')
366
+ .argument('<vaultId>', 'Vault ID or slug')
327
367
  .argument('<path>', 'Directory path to create'))
328
368
  .action(async (vaultId, path, _opts) => {
329
369
  const flags = resolveFlags(_opts);
330
370
  const out = createOutput(flags);
331
371
  out.startSpinner('Creating directory...');
332
372
  try {
373
+ vaultId = await resolveVaultId(vaultId);
333
374
  const client = await getClientAsync();
334
- const result = await client.documents.createDirectory(vaultId, path);
335
- out.success(`Directory ${result.created ? 'created' : 'already exists'}: ${result.path}`, { path: result.path, created: result.created });
375
+ const normalizedPath = path.replace(/\/+$/, '');
376
+ const result = await client.documents.createDirectory(vaultId, normalizedPath);
377
+ out.success(`Directory ${result.created !== false ? 'created' : 'already exists'}: ${result.path}`, { path: result.path, created: result.created ?? true });
336
378
  }
337
379
  catch (err) {
338
380
  handleError(out, err, 'Failed to create directory');
@@ -2,16 +2,18 @@ import chalk from 'chalk';
2
2
  import { getClientAsync } from '../client.js';
3
3
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
+ import { resolveVaultId } from '../utils/resolve-vault.js';
5
6
  export function registerHookCommands(program) {
6
- const hooks = program.command('hooks').description('Manage vault event hooks (auto-tag, template, etc.)');
7
+ const hooks = program.command('hooks').description('Manage vault event hooks');
7
8
  addGlobalFlags(hooks.command('list')
8
9
  .description('List all hooks for a vault')
9
- .argument('<vaultId>', 'Vault ID'))
10
+ .argument('<vaultId>', 'Vault ID or slug'))
10
11
  .action(async (vaultId, _opts) => {
11
12
  const flags = resolveFlags(_opts);
12
13
  const out = createOutput(flags);
13
14
  out.startSpinner('Fetching hooks...');
14
15
  try {
16
+ vaultId = await resolveVaultId(vaultId);
15
17
  const client = await getClientAsync();
16
18
  const hookList = await client.hooks.list(vaultId);
17
19
  out.stopSpinner();
@@ -45,12 +47,31 @@ export function registerHookCommands(program) {
45
47
  });
46
48
  addGlobalFlags(hooks.command('create')
47
49
  .description('Create a new hook')
48
- .argument('<vaultId>', 'Vault ID')
50
+ .argument('<vaultId>', 'Vault ID or slug')
49
51
  .argument('<name>', 'Hook name')
50
- .requiredOption('--trigger <event>', 'Trigger event (e.g., document.create)')
51
- .requiredOption('--action <type>', 'Action type (e.g., auto-tag, template)')
52
+ .requiredOption('--trigger <event>', 'Trigger event (document.created, document.updated, document.deleted, document.moved, document.copied)')
53
+ .requiredOption('--action <type>', 'Action type (webhook, ai_prompt, document_operation, auto_calendar_event, auto_booking_process)')
52
54
  .requiredOption('--config <json>', 'Action configuration as JSON')
53
- .option('--filter <json>', 'Trigger filter as JSON'))
55
+ .option('--filter <json>', 'Trigger filter as JSON')
56
+ .addHelpText('after', `
57
+ VALID TRIGGER EVENTS
58
+ document.created Document was created
59
+ document.updated Document content was updated
60
+ document.deleted Document was deleted
61
+ document.moved Document was moved or renamed
62
+ document.copied Document was copied
63
+
64
+ VALID ACTION TYPES
65
+ webhook Send an HTTP notification to a URL
66
+ ai_prompt Run an AI prompt on the document
67
+ document_operation Perform a document operation (move, copy, tag)
68
+ auto_calendar_event Automatically create a calendar event
69
+ auto_booking_process Automatically process a booking
70
+
71
+ EXAMPLES
72
+ lsvault hooks create <vaultId> my-hook --trigger document.created --action webhook --config '{"url":"https://example.com/hook"}'
73
+ lsvault hooks create <vaultId> ai-tag --trigger document.created --action ai_prompt --config '{"prompt":"Suggest tags"}'
74
+ lsvault hooks create <vaultId> move-docs --trigger document.created --action document_operation --config '{"operation":"move","targetPath":"inbox/"}'`))
54
75
  .action(async (vaultId, name, _opts) => {
55
76
  const flags = resolveFlags(_opts);
56
77
  const out = createOutput(flags);
@@ -74,8 +95,23 @@ export function registerHookCommands(program) {
74
95
  return;
75
96
  }
76
97
  }
98
+ const VALID_TRIGGERS = ['document.created', 'document.updated', 'document.deleted', 'document.moved', 'document.copied'];
99
+ const VALID_ACTIONS = ['webhook', 'ai_prompt', 'document_operation', 'auto_calendar_event', 'auto_booking_process'];
100
+ const trigger = String(_opts.trigger);
101
+ const action = String(_opts.action);
102
+ if (!VALID_TRIGGERS.includes(trigger)) {
103
+ out.error(`Invalid trigger "${trigger}". Valid values: document.created, document.updated, document.deleted, document.moved, document.copied`);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ if (!VALID_ACTIONS.includes(action)) {
108
+ out.error(`Invalid action "${action}". Valid values: webhook, ai_prompt, document_operation, auto_calendar_event, auto_booking_process`);
109
+ process.exitCode = 1;
110
+ return;
111
+ }
77
112
  out.startSpinner('Creating hook...');
78
113
  try {
114
+ vaultId = await resolveVaultId(vaultId);
79
115
  const client = await getClientAsync();
80
116
  const params = {
81
117
  name,
@@ -99,7 +135,7 @@ export function registerHookCommands(program) {
99
135
  });
100
136
  addGlobalFlags(hooks.command('delete')
101
137
  .description('Delete a hook')
102
- .argument('<vaultId>', 'Vault ID')
138
+ .argument('<vaultId>', 'Vault ID or slug')
103
139
  .argument('<hookId>', 'Hook ID')
104
140
  .option('-y, --yes', 'Skip confirmation prompt'))
105
141
  .action(async (vaultId, hookId, _opts) => {
@@ -111,6 +147,7 @@ export function registerHookCommands(program) {
111
147
  }
112
148
  out.startSpinner('Deleting hook...');
113
149
  try {
150
+ vaultId = await resolveVaultId(vaultId);
114
151
  const client = await getClientAsync();
115
152
  await client.hooks.delete(vaultId, hookId);
116
153
  out.success('Hook deleted successfully', { id: hookId, deleted: true });
@@ -121,13 +158,14 @@ export function registerHookCommands(program) {
121
158
  });
122
159
  addGlobalFlags(hooks.command('executions')
123
160
  .description('List recent executions for a hook')
124
- .argument('<vaultId>', 'Vault ID')
161
+ .argument('<vaultId>', 'Vault ID or slug')
125
162
  .argument('<hookId>', 'Hook ID'))
126
163
  .action(async (vaultId, hookId, _opts) => {
127
164
  const flags = resolveFlags(_opts);
128
165
  const out = createOutput(flags);
129
166
  out.startSpinner('Fetching executions...');
130
167
  try {
168
+ vaultId = await resolveVaultId(vaultId);
131
169
  const client = await getClientAsync();
132
170
  const executions = await client.hooks.listExecutions(vaultId, hookId);
133
171
  out.stopSpinner();
@@ -3,7 +3,9 @@ import { getClientAsync } from '../client.js';
3
3
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
5
  export function registerKeyCommands(program) {
6
- const keys = program.command('keys').description('Create, list, update, and revoke API keys');
6
+ const keys = program.command('keys')
7
+ .description('Create, list, update, and revoke API keys (requires JWT auth — use "lsvault auth login" first)')
8
+ .addHelpText('after', '\nNOTE: API key management requires JWT authentication. API key auth is not sufficient.\nRun "lsvault auth login" to authenticate with email/password first.');
7
9
  addGlobalFlags(keys.command('list')
8
10
  .description('List all API keys for the current user'))
9
11
  .action(async (_opts) => {
@@ -148,10 +150,15 @@ EXAMPLES
148
150
  });
149
151
  addGlobalFlags(keys.command('revoke')
150
152
  .description('Permanently revoke and delete an API key')
151
- .argument('<keyId>', 'API key ID'))
153
+ .argument('<keyId>', 'API key ID')
154
+ .option('-y, --yes', 'Skip confirmation prompt'))
152
155
  .action(async (keyId, _opts) => {
153
156
  const flags = resolveFlags(_opts);
154
157
  const out = createOutput(flags);
158
+ if (!_opts.yes) {
159
+ out.status(chalk.yellow(`Pass -y/--yes to permanently revoke key ${keyId}.`));
160
+ return;
161
+ }
155
162
  out.startSpinner('Revoking API key...');
156
163
  try {
157
164
  const client = await getClientAsync();
@@ -2,18 +2,20 @@ import chalk from 'chalk';
2
2
  import { getClientAsync } from '../client.js';
3
3
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
+ import { resolveVaultId } from '../utils/resolve-vault.js';
5
6
  export function registerLinkCommands(program) {
6
7
  const links = program.command('links').description('Manage document links and backlinks');
7
8
  // lsvault links list <vaultId> <path> — forward links
8
9
  addGlobalFlags(links.command('list')
9
10
  .description('List forward links from a document')
10
- .argument('<vaultId>', 'Vault ID')
11
+ .argument('<vaultId>', 'Vault ID or slug')
11
12
  .argument('<path>', 'Document path'))
12
13
  .action(async (vaultId, docPath, _opts) => {
13
14
  const flags = resolveFlags(_opts);
14
15
  const out = createOutput(flags);
15
16
  out.startSpinner('Fetching links...');
16
17
  try {
18
+ vaultId = await resolveVaultId(vaultId);
17
19
  const client = await getClientAsync();
18
20
  const linkList = await client.documents.getLinks(vaultId, docPath);
19
21
  out.stopSpinner();
@@ -41,13 +43,14 @@ export function registerLinkCommands(program) {
41
43
  // lsvault links backlinks <vaultId> <path>
42
44
  addGlobalFlags(links.command('backlinks')
43
45
  .description('List backlinks pointing to a document')
44
- .argument('<vaultId>', 'Vault ID')
46
+ .argument('<vaultId>', 'Vault ID or slug')
45
47
  .argument('<path>', 'Document path'))
46
48
  .action(async (vaultId, docPath, _opts) => {
47
49
  const flags = resolveFlags(_opts);
48
50
  const out = createOutput(flags);
49
51
  out.startSpinner('Fetching backlinks...');
50
52
  try {
53
+ vaultId = await resolveVaultId(vaultId);
51
54
  const client = await getClientAsync();
52
55
  const backlinks = await client.documents.getBacklinks(vaultId, docPath);
53
56
  out.stopSpinner();
@@ -78,12 +81,13 @@ export function registerLinkCommands(program) {
78
81
  // lsvault links graph <vaultId>
79
82
  addGlobalFlags(links.command('graph')
80
83
  .description('Get the link graph for a vault')
81
- .argument('<vaultId>', 'Vault ID'))
84
+ .argument('<vaultId>', 'Vault ID or slug'))
82
85
  .action(async (vaultId, _opts) => {
83
86
  const flags = resolveFlags(_opts);
84
87
  const out = createOutput(flags);
85
88
  out.startSpinner('Fetching link graph...');
86
89
  try {
90
+ vaultId = await resolveVaultId(vaultId);
87
91
  const client = await getClientAsync();
88
92
  const graph = await client.vaults.getGraph(vaultId);
89
93
  out.stopSpinner();
@@ -104,15 +108,20 @@ export function registerLinkCommands(program) {
104
108
  // lsvault links broken <vaultId>
105
109
  addGlobalFlags(links.command('broken')
106
110
  .description('List unresolved (broken) links in a vault')
107
- .argument('<vaultId>', 'Vault ID'))
111
+ .argument('<vaultId>', 'Vault ID or slug'))
108
112
  .action(async (vaultId, _opts) => {
109
113
  const flags = resolveFlags(_opts);
110
114
  const out = createOutput(flags);
111
115
  out.startSpinner('Fetching unresolved links...');
112
116
  try {
117
+ vaultId = await resolveVaultId(vaultId);
113
118
  const client = await getClientAsync();
114
119
  const unresolved = await client.vaults.getUnresolvedLinks(vaultId);
115
120
  out.stopSpinner();
121
+ if (flags.output === 'json') {
122
+ process.stdout.write(JSON.stringify(unresolved) + '\n');
123
+ return;
124
+ }
116
125
  if (unresolved.length === 0) {
117
126
  out.success('No broken links found!');
118
127
  return;
@@ -3,7 +3,9 @@ import ora from 'ora';
3
3
  import { getClientAsync } from '../client.js';
4
4
  import { promptPassword, promptMfaCode } from '../utils/prompt.js';
5
5
  export function registerMfaCommands(program) {
6
- const mfa = program.command('mfa').description('Multi-factor authentication management');
6
+ const mfa = program.command('mfa')
7
+ .description('Multi-factor authentication management (requires JWT auth — use "lsvault auth login" first)')
8
+ .addHelpText('after', '\nNOTE: MFA management requires JWT authentication. API key auth is not sufficient.\nRun "lsvault auth login" to authenticate with email/password first.');
7
9
  mfa.command('status')
8
10
  .description('Show MFA status and configured methods')
9
11
  .action(async () => {
@@ -129,6 +131,7 @@ export function registerMfaCommands(program) {
129
131
  catch (err) {
130
132
  spinner.fail('Failed to regenerate backup codes');
131
133
  console.error(err instanceof Error ? err.message : String(err));
134
+ process.exitCode = 1;
132
135
  }
133
136
  }
134
137
  else {
@@ -149,6 +152,7 @@ export function registerMfaCommands(program) {
149
152
  catch (err) {
150
153
  spinner.fail('Failed to fetch backup code count');
151
154
  console.error(err instanceof Error ? err.message : String(err));
155
+ process.exitCode = 1;
152
156
  }
153
157
  }
154
158
  });
@@ -71,12 +71,13 @@ export function registerPluginCommands(program) {
71
71
  addGlobalFlags(plugins.command('uninstall')
72
72
  .description('Uninstall a plugin')
73
73
  .argument('<pluginId>', 'Plugin marketplace identifier')
74
- .option('--confirm', 'Skip confirmation prompt'))
74
+ .option('-y, --yes', 'Skip confirmation prompt')
75
+ .option('--confirm', 'Alias for --yes (deprecated)'))
75
76
  .action(async (pluginId, _opts) => {
76
77
  const flags = resolveFlags(_opts);
77
78
  const out = createOutput(flags);
78
- if (!_opts.confirm) {
79
- out.raw(chalk.yellow(`Pass --confirm to uninstall plugin ${pluginId}.`) + '\n');
79
+ if (!_opts.yes && !_opts.confirm) {
80
+ out.raw(chalk.yellow(`Pass -y/--yes or --confirm to uninstall plugin ${pluginId}.`) + '\n');
80
81
  return;
81
82
  }
82
83
  out.startSpinner('Uninstalling plugin...');