@lifestreamdynamics/vault-cli 1.3.6 → 1.3.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.
@@ -4,7 +4,9 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
5
  import { formatBytes, formatUptime } from '../utils/format.js';
6
6
  export function registerAdminCommands(program) {
7
- const admin = program.command('admin').description('System administration (requires admin role)');
7
+ const admin = program.command('admin')
8
+ .description('System administration (requires admin role)')
9
+ .addHelpText('after', '\nNOTE: Admin commands require JWT authentication with admin role.\nRun "lsvault auth login" to authenticate first.');
8
10
  // ── System Stats ────────────────────────────────────────────────────
9
11
  const stats = admin.command('stats').description('View system-wide statistics and metrics');
10
12
  const statsAction = async (_opts) => {
@@ -67,7 +67,7 @@ export function registerAnalyticsCommands(program) {
67
67
  .addHelpText('after', `
68
68
  NOTE
69
69
  The publishedDocId is a UUID, NOT a document path. Get it from:
70
- lsvault publish list <vaultId>`))
70
+ lsvault analytics published`))
71
71
  .action(async (vaultId, publishedDocId, _opts) => {
72
72
  const flags = resolveFlags(_opts);
73
73
  const out = createOutput(flags);
@@ -405,7 +405,8 @@ export function registerBookingCommands(program) {
405
405
  addGlobalFlags(booking.command('reschedule')
406
406
  .description('Reschedule a booking by guest reschedule token')
407
407
  .argument('<token>', 'Reschedule token (from guest email link)')
408
- .argument('<newStartAt>', 'New start time in ISO 8601 format (e.g. 2026-03-15T10:00:00Z)'))
408
+ .argument('<newStartAt>', 'New start time in ISO 8601 format (e.g. 2026-03-15T10:00:00Z)')
409
+ .addHelpText('after', '\n The <token> is the guest reschedule token from the booking confirmation email,\n not the booking ID.'))
409
410
  .action(async (token, newStartAt, _opts) => {
410
411
  const flags = resolveFlags(_opts);
411
412
  const out = createOutput(flags);
@@ -1,45 +1,217 @@
1
1
  const SAFE_COMMAND_NAME = /^[a-z][a-z0-9-]*$/;
2
- function collectCommandNames(cmd, prefix = '') {
3
- const names = [];
2
+ /**
3
+ * Recursively walk the Commander tree and produce a structured CommandNode tree.
4
+ * Only names matching SAFE_COMMAND_NAME are included (shell injection guard).
5
+ */
6
+ function collectCommandTree(cmd) {
7
+ const nodes = [];
4
8
  for (const sub of cmd.commands) {
5
9
  const name = sub.name();
6
- // Skip any name that doesn't match the safe pattern to prevent shell injection
7
10
  if (!SAFE_COMMAND_NAME.test(name))
8
11
  continue;
9
- const full = prefix ? `${prefix} ${name}` : name;
10
- names.push(name);
11
- names.push(...collectCommandNames(sub, full));
12
+ // Collect long-form option flags from this command's declared options.
13
+ const options = sub.options
14
+ .map((o) => o.long)
15
+ .filter((flag) => typeof flag === 'string');
16
+ nodes.push({
17
+ name,
18
+ description: sub.description() ?? '',
19
+ subcommands: collectCommandTree(sub),
20
+ options,
21
+ });
12
22
  }
13
- return names;
23
+ return nodes;
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Bash
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Emit the bash case arm for a top-level command.
30
+ * Handles COMP_CWORD == 2 (show subcommand names) and COMP_CWORD >= 3 (show
31
+ * a flat list of options for the matched sub/sub-subcommand).
32
+ */
33
+ function bashCaseArm(node, indent) {
34
+ const lines = [];
35
+ lines.push(`${indent}${node.name})`);
36
+ if (node.subcommands.length > 0) {
37
+ const subNames = node.subcommands.map(s => s.name).join(' ');
38
+ lines.push(`${indent} if [[ $COMP_CWORD -eq 2 ]]; then`);
39
+ lines.push(`${indent} COMPREPLY=($(compgen -W "${subNames}" -- "$cur"))`);
40
+ lines.push(`${indent} else`);
41
+ // Third-level: match sub-subcommand name in COMP_WORDS[2] and show its options
42
+ lines.push(`${indent} case "$cmd2" in`);
43
+ for (const sub of node.subcommands) {
44
+ const allOpts = sub.options.join(' ');
45
+ // Also include sub-subcommand names if this node has nested commands
46
+ const deepNames = sub.subcommands.map(s => s.name).join(' ');
47
+ const completions = [deepNames, allOpts].filter(Boolean).join(' ');
48
+ lines.push(`${indent} ${sub.name})`);
49
+ if (completions) {
50
+ lines.push(`${indent} COMPREPLY=($(compgen -W "${completions}" -- "$cur"))`);
51
+ }
52
+ lines.push(`${indent} ;;`);
53
+ }
54
+ lines.push(`${indent} esac`);
55
+ lines.push(`${indent} fi`);
56
+ }
57
+ else if (node.options.length > 0) {
58
+ // Leaf command with flags only
59
+ const opts = node.options.join(' ');
60
+ lines.push(`${indent} COMPREPLY=($(compgen -W "${opts}" -- "$cur"))`);
61
+ }
62
+ lines.push(`${indent} ;;`);
63
+ return lines.join('\n');
14
64
  }
15
65
  export function generateBashCompletion(program) {
16
- const commands = collectCommandNames(program);
66
+ const tree = collectCommandTree(program);
67
+ const topLevelNames = tree.map(n => n.name).join(' ');
68
+ const caseArms = tree.map(node => bashCaseArm(node, ' ')).join('\n');
17
69
  return `# Bash completion for lsvault
18
70
  # Add to ~/.bashrc: eval "$(lsvault completion bash)"
19
71
  _lsvault_completions() {
20
72
  local cur="\${COMP_WORDS[COMP_CWORD]}"
21
- local commands="${commands.join(' ')}"
22
- COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}"))
73
+ local cmd1="\${COMP_WORDS[1]:-}"
74
+ local cmd2="\${COMP_WORDS[2]:-}"
75
+
76
+ # Top-level completions
77
+ if [[ $COMP_CWORD -eq 1 ]]; then
78
+ COMPREPLY=($(compgen -W "${topLevelNames}" -- "$cur"))
79
+ return
80
+ fi
81
+
82
+ # Second-level and deeper
83
+ case "$cmd1" in
84
+ ${caseArms}
85
+ esac
23
86
  }
24
87
  complete -F _lsvault_completions lsvault
25
88
  `;
26
89
  }
90
+ // ---------------------------------------------------------------------------
91
+ // Zsh
92
+ // ---------------------------------------------------------------------------
93
+ function zshDescribeArray(nodes, varName, indent) {
94
+ const lines = [];
95
+ lines.push(`${indent}local -a ${varName}`);
96
+ lines.push(`${indent}${varName}=(`);
97
+ for (const n of nodes) {
98
+ // Escape single quotes in description
99
+ const desc = n.description.replace(/'/g, "'\\''");
100
+ lines.push(`${indent} '${n.name}:${desc}'`);
101
+ }
102
+ lines.push(`${indent})`);
103
+ return lines.join('\n');
104
+ }
105
+ function zshCaseArm(node, indent) {
106
+ const lines = [];
107
+ lines.push(`${indent}${node.name})`);
108
+ if (node.subcommands.length > 0) {
109
+ lines.push(zshDescribeArray(node.subcommands, 'subcmds', indent + ' '));
110
+ lines.push(`${indent} if (( CURRENT == 3 )); then`);
111
+ lines.push(`${indent} _describe 'subcommand' subcmds`);
112
+ lines.push(`${indent} else`);
113
+ // Third-level: show options for matched subcommand
114
+ lines.push(`${indent} case "$words[3]" in`);
115
+ for (const sub of node.subcommands) {
116
+ if (sub.options.length > 0 || sub.subcommands.length > 0) {
117
+ lines.push(`${indent} ${sub.name})`);
118
+ if (sub.subcommands.length > 0) {
119
+ lines.push(zshDescribeArray(sub.subcommands, 'deepcmds', indent + ' '));
120
+ lines.push(`${indent} if (( CURRENT == 4 )); then`);
121
+ lines.push(`${indent} _describe 'subcommand' deepcmds`);
122
+ if (sub.options.length > 0) {
123
+ const optSpecs = sub.options.map(o => `'${o}'`).join(' ');
124
+ lines.push(`${indent} else`);
125
+ lines.push(`${indent} _arguments ${optSpecs}`);
126
+ }
127
+ lines.push(`${indent} fi`);
128
+ }
129
+ else if (sub.options.length > 0) {
130
+ const optSpecs = sub.options.map(o => `'${o}'`).join(' ');
131
+ lines.push(`${indent} _arguments ${optSpecs}`);
132
+ }
133
+ lines.push(`${indent} ;;`);
134
+ }
135
+ }
136
+ lines.push(`${indent} esac`);
137
+ lines.push(`${indent} fi`);
138
+ }
139
+ else if (node.options.length > 0) {
140
+ const optSpecs = node.options.map(o => `'${o}'`).join(' ');
141
+ lines.push(`${indent} _arguments ${optSpecs}`);
142
+ }
143
+ lines.push(`${indent} ;;`);
144
+ return lines.join('\n');
145
+ }
27
146
  export function generateZshCompletion(program) {
28
- const commands = collectCommandNames(program);
147
+ const tree = collectCommandTree(program);
148
+ const topLevelArray = zshDescribeArray(tree, 'toplevel', ' ');
149
+ const caseArms = tree.map(node => zshCaseArm(node, ' ')).join('\n');
29
150
  return `# Zsh completion for lsvault
30
151
  # Add to ~/.zshrc: eval "$(lsvault completion zsh)"
31
152
  _lsvault() {
32
- local -a commands
33
- commands=(${commands.map(c => `'${c}'`).join(' ')})
34
- _describe 'lsvault commands' commands
153
+ ${topLevelArray}
154
+
155
+ if (( CURRENT == 2 )); then
156
+ _describe 'command' toplevel
157
+ return
158
+ fi
159
+
160
+ case "$words[2]" in
161
+ ${caseArms}
162
+ esac
35
163
  }
36
164
  compdef _lsvault lsvault
37
165
  `;
38
166
  }
167
+ // ---------------------------------------------------------------------------
168
+ // Fish
169
+ // ---------------------------------------------------------------------------
170
+ /**
171
+ * Escape a string for use inside a fish completion -d '...' description.
172
+ * Fish uses single quotes and doesn't support backslash escapes within them,
173
+ * so we just strip single quotes entirely.
174
+ */
175
+ function fishDesc(s) {
176
+ return s.replace(/'/g, '');
177
+ }
39
178
  export function generateFishCompletion(program) {
40
- const commands = collectCommandNames(program);
41
- return commands.map(c => `complete -c lsvault -n '__fish_use_subcommand' -a '${c}' -d '${c} command'`).join('\n') + '\n';
179
+ const tree = collectCommandTree(program);
180
+ const lines = [];
181
+ // Helper: the names of all top-level commands (used in __fish_seen_subcommand_from guards)
182
+ const topLevelNames = tree.map(n => n.name).join(' ');
183
+ for (const node of tree) {
184
+ // Top-level command — only show when no subcommand has been entered yet
185
+ lines.push(`complete -c lsvault -n '__fish_use_subcommand' -a '${node.name}' -d '${fishDesc(node.description)}'`);
186
+ if (node.subcommands.length > 0) {
187
+ // Subcommand names — show when this top-level command is active and no sub-subcommand yet
188
+ for (const sub of node.subcommands) {
189
+ lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and not __fish_seen_subcommand_from ${sub.subcommands.map(s => s.name).concat(node.subcommands.map(s => s.name)).join(' ')}' -a '${sub.name}' -d '${fishDesc(sub.description)}'`);
190
+ // Options for this subcommand — show when top-level AND subcommand are both seen
191
+ for (const opt of sub.options) {
192
+ lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and __fish_seen_subcommand_from ${sub.name}' -a '${opt}'`);
193
+ }
194
+ // Third-level sub-subcommands
195
+ for (const deep of sub.subcommands) {
196
+ lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and __fish_seen_subcommand_from ${sub.name}' -a '${deep.name}' -d '${fishDesc(deep.description)}'`);
197
+ for (const opt of deep.options) {
198
+ lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and __fish_seen_subcommand_from ${deep.name}' -a '${opt}'`);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ else {
204
+ // Leaf top-level command — offer its own options when active
205
+ for (const opt of node.options) {
206
+ lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${topLevelNames}' -f -a '${opt}'`);
207
+ }
208
+ }
209
+ }
210
+ return lines.join('\n') + '\n';
42
211
  }
212
+ // ---------------------------------------------------------------------------
213
+ // Registration
214
+ // ---------------------------------------------------------------------------
43
215
  export function registerCompletionCommands(program) {
44
216
  const completion = program.command('completion').description('Generate shell completion scripts');
45
217
  completion.command('bash')
@@ -46,6 +46,7 @@ EXAMPLES
46
46
  }
47
47
  else {
48
48
  process.stdout.write(chalk.yellow(`Key "${key}" not set in profile "${profile}"`) + '\n');
49
+ process.exitCode = 1;
49
50
  }
50
51
  });
51
52
  config
@@ -106,8 +106,9 @@ export function registerCustomDomainCommands(program) {
106
106
  }
107
107
  });
108
108
  addGlobalFlags(domains.command('verify')
109
- .description('Verify a custom domain via DNS')
110
- .argument('<domainId>', 'Domain ID'))
109
+ .description('Trigger DNS verification check for a custom domain')
110
+ .argument('<domainId>', 'Domain ID')
111
+ .addHelpText('after', '\n Submits a verification request to the server, which checks your DNS TXT record.\n Use "check" to read the current verification status without triggering a new check.'))
111
112
  .action(async (domainId, _opts) => {
112
113
  const flags = resolveFlags(_opts);
113
114
  const out = createOutput(flags);
@@ -122,8 +123,9 @@ export function registerCustomDomainCommands(program) {
122
123
  }
123
124
  });
124
125
  addGlobalFlags(domains.command('check')
125
- .description('Check DNS configuration for a custom domain')
126
- .argument('<domainId>', 'Domain ID'))
126
+ .description('Show current DNS verification status for a custom domain')
127
+ .argument('<domainId>', 'Domain ID')
128
+ .addHelpText('after', '\n Returns the current verification status without triggering a new check.\n Use "verify" to submit a fresh DNS verification request to the server.'))
127
129
  .action(async (domainId, _opts) => {
128
130
  const flags = resolveFlags(_opts);
129
131
  const out = createOutput(flags);
@@ -165,9 +165,11 @@ EXAMPLES
165
165
  else {
166
166
  doc = await client.documents.put(vaultId, docPath, content);
167
167
  }
168
- out.success(`Document saved: ${chalk.cyan(doc.path)} (${doc.sizeBytes ?? 0} bytes)`, {
169
- path: doc.path,
170
- sizeBytes: doc.sizeBytes ?? 0,
168
+ const docPath2 = doc.path ?? docPath;
169
+ const size = doc.sizeBytes ?? Buffer.byteLength(content, 'utf8');
170
+ out.success(`Document saved: ${chalk.cyan(docPath2)} (${size} bytes)`, {
171
+ path: docPath2,
172
+ sizeBytes: size,
171
173
  encrypted: doc.encrypted ?? false,
172
174
  });
173
175
  }
@@ -22,16 +22,16 @@ export function registerLinkCommands(program) {
22
22
  out.list(linkList.map(link => ({
23
23
  targetPath: link.targetPath,
24
24
  linkText: link.linkText,
25
- resolved: link.isResolved ? 'Yes' : 'No',
25
+ isResolved: link.isResolved,
26
26
  })), {
27
27
  emptyMessage: 'No forward links found.',
28
28
  columns: [
29
29
  { key: 'targetPath', header: 'Target' },
30
30
  { key: 'linkText', header: 'Link Text' },
31
- { key: 'resolved', header: 'Resolved' },
31
+ { key: 'isResolved', header: 'Resolved' },
32
32
  ],
33
33
  textFn: (link) => {
34
- const resolved = link.resolved === 'Yes' ? chalk.green('✓') : chalk.red('✗');
34
+ const resolved = link.isResolved ? chalk.green('✓') : chalk.red('✗');
35
35
  return ` ${resolved} [[${String(link.linkText)}]] → ${String(link.targetPath)}`;
36
36
  },
37
37
  });
@@ -55,18 +55,23 @@ export function registerLinkCommands(program) {
55
55
  const backlinks = await client.documents.getBacklinks(vaultId, docPath);
56
56
  out.stopSpinner();
57
57
  out.list(backlinks.map(bl => ({
58
- source: bl.sourceDocument.title || bl.sourceDocument.path,
58
+ sourcePath: bl.sourceDocument.path,
59
+ sourceTitle: bl.sourceDocument.title || null,
59
60
  linkText: bl.linkText,
60
61
  context: bl.contextSnippet || '',
61
62
  })), {
62
63
  emptyMessage: 'No backlinks found.',
63
64
  columns: [
64
- { key: 'source', header: 'Source' },
65
+ { key: 'sourcePath', header: 'Source Path' },
66
+ { key: 'sourceTitle', header: 'Source Title' },
65
67
  { key: 'linkText', header: 'Link Text' },
66
68
  { key: 'context', header: 'Context' },
67
69
  ],
68
70
  textFn: (bl) => {
69
- const lines = [chalk.cyan(` ${String(bl.source)}`)];
71
+ const displayName = bl.sourceTitle ? String(bl.sourceTitle) : String(bl.sourcePath);
72
+ const lines = [chalk.cyan(` ${displayName}`)];
73
+ if (bl.sourceTitle)
74
+ lines.push(` Path: ${String(bl.sourcePath)}`);
70
75
  lines.push(` Link: [[${String(bl.linkText)}]]`);
71
76
  if (bl.context)
72
77
  lines.push(` Context: ...${String(bl.context)}...`);
@@ -1,158 +1,208 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
3
2
  import { getClientAsync } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
4
5
  import { promptPassword, promptMfaCode } from '../utils/prompt.js';
5
6
  export function registerMfaCommands(program) {
6
7
  const mfa = program.command('mfa')
7
8
  .description('Multi-factor authentication management (requires JWT auth — use "lsvault auth login" first)')
8
9
  .addHelpText('after', '\nNOTE: MFA management requires JWT authentication. API key auth is not sufficient.\nRun "lsvault auth login" to authenticate with email/password first.');
9
- mfa.command('status')
10
- .description('Show MFA status and configured methods')
11
- .action(async () => {
12
- const spinner = ora('Fetching MFA status...').start();
10
+ addGlobalFlags(mfa.command('status')
11
+ .description('Show MFA status and configured methods'))
12
+ .action(async (_opts) => {
13
+ const flags = resolveFlags(_opts);
14
+ const out = createOutput(flags);
15
+ out.startSpinner('Fetching MFA status...');
13
16
  try {
14
17
  const client = await getClientAsync();
15
18
  const status = await client.mfa.getStatus();
16
- spinner.stop();
17
- console.log(chalk.bold('MFA Status'));
18
- console.log(` Enabled: ${status.mfaEnabled ? chalk.green('Yes') : chalk.dim('No')}`);
19
- console.log(` TOTP Configured: ${status.totpConfigured ? chalk.green('Yes') : chalk.dim('No')}`);
20
- console.log(` Passkeys Registered: ${status.passkeyCount > 0 ? chalk.cyan(status.passkeyCount) : chalk.dim('0')}`);
21
- console.log(` Backup Codes Left: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}`);
22
- if (status.passkeys.length > 0) {
23
- console.log('');
24
- console.log(chalk.bold('Registered Passkeys:'));
25
- for (const passkey of status.passkeys) {
26
- const lastUsed = passkey.lastUsedAt
27
- ? new Date(passkey.lastUsedAt).toLocaleDateString()
28
- : chalk.dim('never');
29
- console.log(` - ${chalk.cyan(passkey.name)} (last used: ${lastUsed})`);
19
+ out.stopSpinner();
20
+ if (flags.output === 'json') {
21
+ out.record({
22
+ mfaEnabled: status.mfaEnabled,
23
+ totpConfigured: status.totpConfigured,
24
+ passkeyCount: status.passkeyCount,
25
+ backupCodesRemaining: status.backupCodesRemaining,
26
+ passkeys: status.passkeys,
27
+ });
28
+ }
29
+ else {
30
+ out.raw(chalk.bold('MFA Status') + '\n');
31
+ out.raw(` Enabled: ${status.mfaEnabled ? chalk.green('Yes') : chalk.dim('No')}\n`);
32
+ out.raw(` TOTP Configured: ${status.totpConfigured ? chalk.green('Yes') : chalk.dim('No')}\n`);
33
+ out.raw(` Passkeys Registered: ${status.passkeyCount > 0 ? chalk.cyan(status.passkeyCount) : chalk.dim('0')}\n`);
34
+ out.raw(` Backup Codes Left: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}\n`);
35
+ if (status.passkeys.length > 0) {
36
+ out.raw('\n');
37
+ out.raw(chalk.bold('Registered Passkeys:') + '\n');
38
+ for (const passkey of status.passkeys) {
39
+ const lastUsed = passkey.lastUsedAt
40
+ ? new Date(passkey.lastUsedAt).toLocaleDateString()
41
+ : chalk.dim('never');
42
+ out.raw(` - ${chalk.cyan(passkey.name)} (last used: ${lastUsed})\n`);
43
+ }
30
44
  }
31
45
  }
32
46
  }
33
47
  catch (err) {
34
- spinner.fail('Failed to fetch MFA status');
35
- console.error(err instanceof Error ? err.message : String(err));
48
+ out.failSpinner('Failed to fetch MFA status');
49
+ handleError(out, err, 'MFA management requires JWT authentication');
36
50
  }
37
51
  });
38
- mfa.command('setup-totp')
39
- .description('Set up TOTP authenticator app (Google Authenticator, Authy, etc.)')
40
- .action(async () => {
41
- const spinner = ora('Generating TOTP secret...').start();
52
+ addGlobalFlags(mfa.command('setup-totp')
53
+ .description('Set up TOTP authenticator app (Google Authenticator, Authy, etc.)'))
54
+ .action(async (_opts) => {
55
+ const flags = resolveFlags(_opts);
56
+ const out = createOutput(flags);
57
+ out.startSpinner('Generating TOTP secret...');
42
58
  try {
43
59
  const client = await getClientAsync();
44
60
  const setup = await client.mfa.setupTotp();
45
- spinner.stop();
46
- console.log(chalk.bold('TOTP Setup'));
47
- console.log('');
48
- console.log(`Secret: ${chalk.cyan(setup.secret)}`);
49
- console.log('');
50
- console.log('Add this URI to your authenticator app:');
51
- console.log(chalk.dim(setup.otpauthUri));
52
- console.log('');
53
- console.log(chalk.yellow('Note: QR codes cannot be displayed in the terminal.'));
54
- console.log(chalk.yellow(' Copy the URI above to any authenticator app that supports otpauth:// URIs.'));
55
- console.log('');
56
- // Prompt for verification code
61
+ out.stopSpinner();
62
+ if (flags.output === 'json') {
63
+ out.record({ secret: setup.secret, otpauthUri: setup.otpauthUri });
64
+ }
65
+ else {
66
+ out.raw(chalk.bold('TOTP Setup') + '\n');
67
+ out.raw('\n');
68
+ out.raw(`Secret: ${chalk.cyan(setup.secret)}\n`);
69
+ out.raw('\n');
70
+ out.raw('Add this URI to your authenticator app:\n');
71
+ out.raw(chalk.dim(setup.otpauthUri) + '\n');
72
+ out.raw('\n');
73
+ out.raw(chalk.yellow('Note: QR codes cannot be displayed in the terminal.') + '\n');
74
+ out.raw(chalk.yellow(' Copy the URI above to any authenticator app that supports otpauth:// URIs.') + '\n');
75
+ out.raw('\n');
76
+ }
77
+ // Prompt for verification code — skip in quiet mode
78
+ if (flags.quiet)
79
+ return;
57
80
  const code = await promptMfaCode();
58
81
  if (!code) {
59
- console.log(chalk.yellow('Setup cancelled.'));
82
+ out.status(chalk.yellow('Setup cancelled.'));
60
83
  return;
61
84
  }
62
- const verifySpinner = ora('Verifying code and enabling TOTP...').start();
85
+ out.startSpinner('Verifying code and enabling TOTP...');
63
86
  const result = await client.mfa.verifyTotp(code);
64
- verifySpinner.succeed('TOTP enabled successfully!');
65
- console.log('');
66
- console.log(chalk.bold.yellow('IMPORTANT: Save these backup codes securely!'));
67
- console.log(chalk.dim('You can use them to access your account if you lose your authenticator device.'));
68
- console.log('');
69
- // Display backup codes in a grid (2 columns)
70
- const codes = result.backupCodes;
71
- for (let i = 0; i < codes.length; i += 2) {
72
- const left = codes[i] || '';
73
- const right = codes[i + 1] || '';
74
- console.log(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}`);
87
+ out.stopSpinner();
88
+ out.success('TOTP enabled successfully!');
89
+ if (flags.output === 'json') {
90
+ out.list(result.backupCodes.map((code) => ({ code })), { emptyMessage: 'No backup codes returned.' });
91
+ }
92
+ else {
93
+ out.raw('\n');
94
+ out.raw(chalk.bold.yellow('IMPORTANT: Save these backup codes securely!') + '\n');
95
+ out.raw(chalk.dim('You can use them to access your account if you lose your authenticator device.') + '\n');
96
+ out.raw('\n');
97
+ // Display backup codes in a grid (2 columns)
98
+ const codes = result.backupCodes;
99
+ for (let i = 0; i < codes.length; i += 2) {
100
+ const left = codes[i] || '';
101
+ const right = codes[i + 1] || '';
102
+ out.raw(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}\n`);
103
+ }
104
+ out.raw('\n');
75
105
  }
76
- console.log('');
77
106
  }
78
107
  catch (err) {
79
- spinner.fail('TOTP setup failed');
80
- console.error(err instanceof Error ? err.message : String(err));
108
+ handleError(out, err, 'TOTP setup failed');
81
109
  }
82
110
  });
83
- mfa.command('disable-totp')
84
- .description('Disable TOTP authentication (requires password)')
85
- .action(async () => {
111
+ addGlobalFlags(mfa.command('disable-totp')
112
+ .description('Disable TOTP authentication (requires password)'))
113
+ .action(async (_opts) => {
114
+ const flags = resolveFlags(_opts);
115
+ const out = createOutput(flags);
116
+ // Skip interactive prompt in quiet mode
117
+ if (flags.quiet) {
118
+ out.error('Password prompt required — cannot run in quiet mode without --password-stdin.');
119
+ process.exitCode = 1;
120
+ return;
121
+ }
86
122
  const password = await promptPassword();
87
123
  if (!password) {
88
- console.log(chalk.yellow('Operation cancelled.'));
124
+ out.status(chalk.yellow('Operation cancelled.'));
89
125
  return;
90
126
  }
91
- const spinner = ora('Disabling TOTP...').start();
127
+ out.startSpinner('Disabling TOTP...');
92
128
  try {
93
129
  const client = await getClientAsync();
94
130
  const result = await client.mfa.disableTotp(password);
95
- spinner.succeed(result.message);
131
+ out.stopSpinner();
132
+ out.success(result.message, { message: result.message });
96
133
  }
97
134
  catch (err) {
98
- spinner.fail('Failed to disable TOTP');
99
- console.error(err instanceof Error ? err.message : String(err));
135
+ handleError(out, err, 'Failed to disable TOTP');
100
136
  }
101
137
  });
102
- mfa.command('backup-codes')
138
+ addGlobalFlags(mfa.command('backup-codes')
103
139
  .description('Show remaining backup code count or regenerate codes')
104
- .option('--regenerate', 'Generate new backup codes (requires password, invalidates old codes)')
105
- .action(async (opts) => {
106
- if (opts.regenerate) {
140
+ .option('--regenerate', 'Generate new backup codes (requires password, invalidates old codes)'))
141
+ .action(async (_opts) => {
142
+ const flags = resolveFlags(_opts);
143
+ const out = createOutput(flags);
144
+ if (_opts.regenerate) {
107
145
  // Regenerate backup codes
146
+ if (flags.quiet) {
147
+ out.error('Password prompt required — cannot run in quiet mode without --password-stdin.');
148
+ process.exitCode = 1;
149
+ return;
150
+ }
108
151
  const password = await promptPassword();
109
152
  if (!password) {
110
- console.log(chalk.yellow('Operation cancelled.'));
153
+ out.status(chalk.yellow('Operation cancelled.'));
111
154
  return;
112
155
  }
113
- const spinner = ora('Regenerating backup codes...').start();
156
+ out.startSpinner('Regenerating backup codes...');
114
157
  try {
115
158
  const client = await getClientAsync();
116
159
  const result = await client.mfa.regenerateBackupCodes(password);
117
- spinner.succeed('Backup codes regenerated!');
118
- console.log('');
119
- console.log(chalk.bold.yellow('IMPORTANT: Save these new backup codes securely!'));
120
- console.log(chalk.dim('All previous backup codes have been invalidated.'));
121
- console.log('');
122
- // Display backup codes in a grid (2 columns)
123
- const codes = result.backupCodes;
124
- for (let i = 0; i < codes.length; i += 2) {
125
- const left = codes[i] || '';
126
- const right = codes[i + 1] || '';
127
- console.log(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}`);
160
+ out.stopSpinner();
161
+ out.success('Backup codes regenerated!');
162
+ if (flags.output === 'json') {
163
+ out.list(result.backupCodes.map((code) => ({ code })), { emptyMessage: 'No backup codes returned.' });
164
+ }
165
+ else {
166
+ out.raw('\n');
167
+ out.raw(chalk.bold.yellow('IMPORTANT: Save these new backup codes securely!') + '\n');
168
+ out.raw(chalk.dim('All previous backup codes have been invalidated.') + '\n');
169
+ out.raw('\n');
170
+ // Display backup codes in a grid (2 columns)
171
+ const codes = result.backupCodes;
172
+ for (let i = 0; i < codes.length; i += 2) {
173
+ const left = codes[i] || '';
174
+ const right = codes[i + 1] || '';
175
+ out.raw(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}\n`);
176
+ }
177
+ out.raw('\n');
128
178
  }
129
- console.log('');
130
179
  }
131
180
  catch (err) {
132
- spinner.fail('Failed to regenerate backup codes');
133
- console.error(err instanceof Error ? err.message : String(err));
134
- process.exitCode = 1;
181
+ handleError(out, err, 'Failed to regenerate backup codes');
135
182
  }
136
183
  }
137
184
  else {
138
185
  // Show backup code count
139
- const spinner = ora('Fetching backup code count...').start();
186
+ out.startSpinner('Fetching backup code count...');
140
187
  try {
141
188
  const client = await getClientAsync();
142
189
  const status = await client.mfa.getStatus();
143
- spinner.stop();
144
- console.log(chalk.bold('Backup Codes'));
145
- console.log(` Remaining: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}`);
146
- if (status.backupCodesRemaining === 0) {
147
- console.log('');
148
- console.log(chalk.yellow('You have no backup codes remaining.'));
149
- console.log(chalk.yellow('Run `lsvault mfa backup-codes --regenerate` to generate new codes.'));
190
+ out.stopSpinner();
191
+ if (flags.output === 'json') {
192
+ out.record({ backupCodesRemaining: status.backupCodesRemaining });
193
+ }
194
+ else {
195
+ out.raw(chalk.bold('Backup Codes') + '\n');
196
+ out.raw(` Remaining: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}\n`);
197
+ if (status.backupCodesRemaining === 0) {
198
+ out.raw('\n');
199
+ out.raw(chalk.yellow('You have no backup codes remaining.') + '\n');
200
+ out.raw(chalk.yellow('Run `lsvault mfa backup-codes --regenerate` to generate new codes.') + '\n');
201
+ }
150
202
  }
151
203
  }
152
204
  catch (err) {
153
- spinner.fail('Failed to fetch backup code count');
154
- console.error(err instanceof Error ? err.message : String(err));
155
- process.exitCode = 1;
205
+ handleError(out, err, 'Failed to fetch backup code count');
156
206
  }
157
207
  }
158
208
  });
@@ -44,7 +44,7 @@ export function registerPluginCommands(program) {
44
44
  addGlobalFlags(plugins.command('install')
45
45
  .description('Install a plugin from the marketplace')
46
46
  .requiredOption('--plugin-id <pluginId>', 'Plugin marketplace identifier (e.g. org/plugin-name)')
47
- .requiredOption('--version <version>', 'Version to install'))
47
+ .requiredOption('--plugin-version <version>', 'Version to install'))
48
48
  .action(async (_opts) => {
49
49
  const flags = resolveFlags(_opts);
50
50
  const out = createOutput(flags);
@@ -53,7 +53,7 @@ export function registerPluginCommands(program) {
53
53
  const client = await getClientAsync();
54
54
  const installed = await client.plugins.install({
55
55
  pluginId: _opts.pluginId,
56
- version: _opts.version,
56
+ version: _opts.pluginVersion,
57
57
  });
58
58
  out.stopSpinner();
59
59
  if (flags.output === 'json') {
@@ -2,6 +2,7 @@ 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 { confirmAction } from '../utils/confirm.js';
5
6
  export function registerPublishVaultCommands(program) {
6
7
  const pv = program.command('publish-vault').description('Manage whole-vault publishing (public sites)');
7
8
  addGlobalFlags(pv.command('list')
@@ -37,7 +38,7 @@ export function registerPublishVaultCommands(program) {
37
38
  .option('--description <desc>', 'Site description')
38
39
  .option('--show-sidebar', 'Show sidebar navigation')
39
40
  .option('--enable-search', 'Enable search on the site')
40
- .option('--theme <theme>', 'Site theme')
41
+ .option('--theme <theme>', 'Site theme (e.g. default, minimal, blog, docs)')
41
42
  .option('--domain <domainId>', 'Custom domain ID'))
42
43
  .action(async (vaultId, _opts) => {
43
44
  const flags = resolveFlags(_opts);
@@ -68,7 +69,7 @@ export function registerPublishVaultCommands(program) {
68
69
  .option('--description <desc>', 'Site description')
69
70
  .option('--show-sidebar', 'Show sidebar')
70
71
  .option('--enable-search', 'Enable search')
71
- .option('--theme <theme>', 'Site theme')
72
+ .option('--theme <theme>', 'Site theme (e.g. default, minimal, blog, docs)')
72
73
  .option('--domain <domainId>', 'Custom domain ID'))
73
74
  .action(async (vaultId, _opts) => {
74
75
  const flags = resolveFlags(_opts);
@@ -100,12 +101,18 @@ export function registerPublishVaultCommands(program) {
100
101
  });
101
102
  addGlobalFlags(pv.command('unpublish')
102
103
  .description('Unpublish a vault site')
103
- .argument('<vaultId>', 'Vault ID'))
104
+ .argument('<vaultId>', 'Vault ID')
105
+ .option('-y, --yes', 'Skip confirmation prompt'))
104
106
  .action(async (vaultId, _opts) => {
105
107
  const flags = resolveFlags(_opts);
106
108
  const out = createOutput(flags);
107
- out.startSpinner('Unpublishing vault...');
108
109
  try {
110
+ const confirmed = await confirmAction(`Unpublish vault site for vault ${vaultId}?`, { yes: _opts.yes });
111
+ if (!confirmed) {
112
+ out.status('Unpublish cancelled.');
113
+ return;
114
+ }
115
+ out.startSpinner('Unpublishing vault...');
109
116
  const client = await getClientAsync();
110
117
  await client.publishVault.unpublish(vaultId);
111
118
  out.success('Vault unpublished', { vaultId });
@@ -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 registerSamlCommands(program) {
6
- const saml = program.command('saml').description('SAML SSO configuration management (requires admin role)');
6
+ const saml = program.command('saml')
7
+ .description('SAML SSO configuration management (requires admin role)')
8
+ .addHelpText('after', '\nNOTE: SAML commands require JWT authentication with admin role.\nRun "lsvault auth login" to authenticate first.');
7
9
  // ── list-configs ─────────────────────────────────────────────────────────
8
10
  addGlobalFlags(saml.command('list-configs')
9
11
  .description('List all SSO configurations'))
@@ -16,7 +16,11 @@ EXAMPLES
16
16
  lsvault search "meeting notes"
17
17
  lsvault search "project plan" --vault abc123
18
18
  lsvault search "typescript" --tags dev,code --limit 5
19
- lsvault search "machine learning" --mode semantic`))
19
+ lsvault search "machine learning" --mode semantic
20
+
21
+ NOTE
22
+ Semantic search (--mode semantic) requires the embedding worker to have
23
+ processed documents. If results are empty, ensure the worker is running.`))
20
24
  .action(async (query, _opts) => {
21
25
  const flags = resolveFlags(_opts);
22
26
  const out = createOutput(flags);
@@ -5,6 +5,7 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
5
5
  import { createOutput, handleError } from '../utils/output.js';
6
6
  import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
7
7
  import { resolveVaultId } from '../utils/resolve-vault.js';
8
+ import { confirmAction } from '../utils/confirm.js';
8
9
  export function registerShareCommands(program) {
9
10
  const shares = program.command('shares').description('Create, list, and revoke document share links');
10
11
  addGlobalFlags(shares.command('list')
@@ -134,12 +135,18 @@ export function registerShareCommands(program) {
134
135
  addGlobalFlags(shares.command('revoke')
135
136
  .description('Revoke a share link')
136
137
  .argument('<vaultId>', 'Vault ID or slug')
137
- .argument('<shareId>', 'Share link ID'))
138
+ .argument('<shareId>', 'Share link ID')
139
+ .option('-y, --yes', 'Skip confirmation prompt'))
138
140
  .action(async (vaultId, shareId, _opts) => {
139
141
  const flags = resolveFlags(_opts);
140
142
  const out = createOutput(flags);
141
- out.startSpinner('Revoking share link...');
142
143
  try {
144
+ const confirmed = await confirmAction(`Revoke share link ${shareId}?`, { yes: _opts.yes });
145
+ if (!confirmed) {
146
+ out.status('Revoke cancelled.');
147
+ return;
148
+ }
149
+ out.startSpinner('Revoking share link...');
143
150
  vaultId = await resolveVaultId(vaultId);
144
151
  const client = await getClientAsync();
145
152
  await client.shares.revoke(vaultId, shareId);
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { getClientAsync } from '../client.js';
5
5
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
6
6
  import { createOutput, handleError } from '../utils/output.js';
7
+ import { confirmAction } from '../utils/confirm.js';
7
8
  import { formatUptime } from '../utils/format.js';
8
9
  import { loadSyncConfigs, createSyncConfig, deleteSyncConfig, getSyncConfig, } from '../sync/config.js';
9
10
  import { deleteSyncState, loadSyncState, saveSyncState, hashFileContent, buildRemoteFileState } from '../sync/state.js';
@@ -124,12 +125,18 @@ Sync modes:
124
125
  // sync delete <syncId>
125
126
  addGlobalFlags(sync.command('delete')
126
127
  .description('Delete a sync configuration')
127
- .argument('<syncId>', 'Sync configuration ID'))
128
+ .argument('<syncId>', 'Sync configuration ID')
129
+ .option('-y, --yes', 'Skip confirmation prompt'))
128
130
  .action(async (syncId, _opts) => {
129
131
  const flags = resolveFlags(_opts);
130
132
  const out = createOutput(flags);
131
- out.startSpinner('Deleting sync configuration...');
132
133
  try {
134
+ const confirmed = await confirmAction(`Delete sync configuration ${syncId}?`, { yes: _opts.yes });
135
+ if (!confirmed) {
136
+ out.status('Delete cancelled.');
137
+ return;
138
+ }
139
+ out.startSpinner('Deleting sync configuration...');
133
140
  const deleted = deleteSyncConfig(syncId);
134
141
  if (!deleted) {
135
142
  out.failSpinner('Sync configuration not found');
@@ -274,8 +281,6 @@ Sync modes:
274
281
  }
275
282
  out.stopSpinner();
276
283
  if (flags.dryRun) {
277
- out.status(chalk.yellow('Dry run — no changes will be made:'));
278
- out.status(formatDiff(diff));
279
284
  if (flags.output === 'json') {
280
285
  out.record({
281
286
  dryRun: true,
@@ -285,6 +290,10 @@ Sync modes:
285
290
  totalBytes: diff.totalBytes,
286
291
  });
287
292
  }
293
+ else {
294
+ out.status(chalk.yellow('Dry run — no changes will be made:'));
295
+ out.status(formatDiff(diff));
296
+ }
288
297
  return;
289
298
  }
290
299
  if (flags.verbose) {
@@ -5,6 +5,7 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
5
5
  import { createOutput, handleError } from '../utils/output.js';
6
6
  import { formatBytes } from '../utils/format.js';
7
7
  import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
8
+ import { confirmAction } from '../utils/confirm.js';
8
9
  export function registerUserCommands(program) {
9
10
  const user = program.command('user').description('View account details and storage usage');
10
11
  addGlobalFlags(user.command('storage')
@@ -176,10 +177,16 @@ export function registerUserCommands(program) {
176
177
  .description('Request account deletion')
177
178
  .option('--password-stdin', 'Read password from stdin for CI usage')
178
179
  .option('--reason <reason>', 'Reason for deletion')
179
- .option('--export-data', 'Request data export before deletion'))
180
+ .option('--export-data', 'Request data export before deletion')
181
+ .option('-y, --yes', 'Skip confirmation prompt'))
180
182
  .action(async (_opts) => {
181
183
  const flags = resolveFlags(_opts);
182
184
  const out = createOutput(flags);
185
+ const confirmed = await confirmAction('Are you sure you want to delete your account? This cannot be undone.', { yes: _opts.yes });
186
+ if (!confirmed) {
187
+ out.status('Account deletion cancelled.');
188
+ return;
189
+ }
183
190
  let password;
184
191
  if (_opts.passwordStdin) {
185
192
  password = await readPasswordFromStdin();
@@ -5,17 +5,21 @@ import { createOutput, handleError } from '../utils/output.js';
5
5
  import { generateVaultKey } from '@lifestreamdynamics/vault-sdk';
6
6
  import { getCredentialManager } from '../config.js';
7
7
  import { resolveVaultId } from '../utils/resolve-vault.js';
8
+ import { confirmAction } from '../utils/confirm.js';
8
9
  export function registerVaultCommands(program) {
9
10
  const vaults = program.command('vaults').description('Create, list, and inspect document vaults');
10
11
  addGlobalFlags(vaults.command('list')
11
- .description('List all vaults accessible to the current user'))
12
+ .description('List all vaults accessible to the current user')
13
+ .option('--include-archived', 'Include archived vaults in the list'))
12
14
  .action(async (_opts) => {
13
15
  const flags = resolveFlags(_opts);
14
16
  const out = createOutput(flags);
15
17
  out.startSpinner('Fetching vaults...');
16
18
  try {
17
19
  const client = await getClientAsync();
18
- const vaultList = await client.vaults.list();
20
+ const vaultList = await client.vaults.list({
21
+ includeArchived: _opts.includeArchived === true,
22
+ });
19
23
  out.stopSpinner();
20
24
  out.list(vaultList.map(v => ({ name: v.name, slug: v.slug, encrypted: v.encryptionEnabled ? 'yes' : 'no', description: v.description ?? null, id: v.id })), {
21
25
  emptyMessage: 'No vaults found.',
@@ -45,6 +49,7 @@ export function registerVaultCommands(program) {
45
49
  const out = createOutput(flags);
46
50
  out.startSpinner('Fetching vault...');
47
51
  try {
52
+ vaultId = await resolveVaultId(vaultId);
48
53
  const client = await getClientAsync();
49
54
  const vault = await client.vaults.get(vaultId);
50
55
  out.stopSpinner();
@@ -196,12 +201,18 @@ EXAMPLES
196
201
  // vault archive
197
202
  addGlobalFlags(vaults.command('archive')
198
203
  .description('Archive a vault')
199
- .argument('<vaultId>', 'Vault ID'))
204
+ .argument('<vaultId>', 'Vault ID')
205
+ .option('-y, --yes', 'Skip confirmation prompt'))
200
206
  .action(async (vaultId, _opts) => {
201
207
  const flags = resolveFlags(_opts);
202
208
  const out = createOutput(flags);
203
- out.startSpinner('Archiving vault...');
204
209
  try {
210
+ const confirmed = await confirmAction(`Archive vault ${vaultId}?`, { yes: _opts.yes });
211
+ if (!confirmed) {
212
+ out.status('Archive cancelled.');
213
+ return;
214
+ }
215
+ out.startSpinner('Archiving vault...');
205
216
  const client = await getClientAsync();
206
217
  const vault = await client.vaults.archive(vaultId);
207
218
  out.success(`Vault archived: ${vault.name}`, { id: vault.id, name: vault.name, isArchived: vault.isArchived });
@@ -96,6 +96,8 @@ export class Output {
96
96
  * - table: prints a single-row table
97
97
  */
98
98
  record(data, columns) {
99
+ if (this.flags.quiet)
100
+ return;
99
101
  switch (this.flags.output) {
100
102
  case 'json':
101
103
  process.stdout.write(JSON.stringify(data) + '\n');
@@ -131,11 +133,11 @@ export class Output {
131
133
  }
132
134
  return;
133
135
  }
136
+ if (this.flags.quiet)
137
+ return;
134
138
  switch (this.flags.output) {
135
139
  case 'json':
136
- for (const item of data) {
137
- process.stdout.write(JSON.stringify(item) + '\n');
138
- }
140
+ process.stdout.write(JSON.stringify(data) + '\n');
139
141
  break;
140
142
  case 'table':
141
143
  this.table(data, options?.columns);
@@ -166,8 +168,8 @@ export class Output {
166
168
  * Print a success result (used for create/update/delete confirmations).
167
169
  */
168
170
  success(message, data) {
169
- if (this.flags.output === 'json' && data) {
170
- process.stdout.write(JSON.stringify(data) + '\n');
171
+ if (this.flags.output === 'json') {
172
+ process.stdout.write(JSON.stringify(data ?? { success: true, message }) + '\n');
171
173
  }
172
174
  else if (!this.flags.quiet) {
173
175
  this.succeedSpinner(message);
@@ -11,7 +11,7 @@ export async function resolveVaultId(idOrSlug) {
11
11
  if (UUID_RE.test(idOrSlug))
12
12
  return idOrSlug;
13
13
  const client = await getClientAsync();
14
- const vaults = await client.vaults.list();
14
+ const vaults = await client.vaults.list({ includeArchived: true });
15
15
  const match = vaults.find(v => v.slug === idOrSlug);
16
16
  if (!match)
17
17
  throw new Error(`Vault not found: "${idOrSlug}"`);
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@lifestreamdynamics/vault-cli",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Command-line interface for Lifestream Vault",
5
5
  "engines": {
6
- "node": ">=20"
6
+ "node": ">=22"
7
7
  },
8
8
  "type": "module",
9
9
  "bin": {