@lifestreamdynamics/vault-cli 1.3.8 → 1.3.9

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.
@@ -80,7 +80,11 @@ export function registerAiCommands(program) {
80
80
  addGlobalFlags(ai.command('chat')
81
81
  .description('Send a message in an AI chat session')
82
82
  .argument('<sessionId>', 'Session ID')
83
- .argument('<message>', 'Message to send'))
83
+ .argument('<message>', 'Message to send')
84
+ .addHelpText('after', `
85
+ EXAMPLES
86
+ lsvault ai chat <session-id> "What are the key points in my notes?"
87
+ lsvault ai chat <session-id> "Summarize recent changes" -o json`))
84
88
  .action(async (sessionId, message, _opts) => {
85
89
  const flags = resolveFlags(_opts);
86
90
  const out = createOutput(flags);
@@ -43,6 +43,7 @@ EXAMPLES
43
43
  const logPath = String(_opts.logPath || DEFAULT_LOG_PATH);
44
44
  if (!fs.existsSync(logPath)) {
45
45
  out.warn(`Audit log file not found at ${logPath}`);
46
+ process.exitCode = 1;
46
47
  if (flags.output === 'json') {
47
48
  process.stdout.write('[]\n');
48
49
  }
@@ -5,6 +5,8 @@ import { loadConfigAsync, getCredentialManager } from '../config.js';
5
5
  import { getClientAsync } from '../client.js';
6
6
  import { migrateCredentials, hasPlaintextCredentials, checkAndPromptMigration } from '../lib/migration.js';
7
7
  import { promptPassword, promptMfaCode } from '../utils/prompt.js';
8
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
9
+ import { createOutput, handleError } from '../utils/output.js';
8
10
  export function registerAuthCommands(program) {
9
11
  const auth = program.command('auth').description('Authentication and credential management');
10
12
  auth.command('login')
@@ -186,41 +188,47 @@ EXAMPLES
186
188
  spinner.info('Migration skipped.');
187
189
  }
188
190
  });
189
- auth.command('whoami')
190
- .description('Show the currently authenticated user, plan, and API URL')
191
- .action(async () => {
191
+ addGlobalFlags(auth.command('whoami')
192
+ .description('Show the currently authenticated user, plan, and API URL'))
193
+ .action(async (_opts) => {
194
+ const flags = resolveFlags(_opts);
195
+ const out = createOutput(flags);
192
196
  const config = await loadConfigAsync();
193
- console.log(`API URL: ${config.apiUrl}`);
194
- console.log(`API Key: ${config.apiKey ? config.apiKey.slice(0, 12) + '...' : chalk.yellow('not set')}`);
195
- if (config.accessToken) {
196
- console.log(`Auth: ${chalk.green('JWT (email/password)')}`);
197
- }
198
197
  // Warn about plaintext credentials
199
198
  await checkAndPromptMigration(getCredentialManager());
200
- if (config.apiKey || config.accessToken) {
201
- const spinner = ora('Fetching user info...').start();
202
- try {
203
- const client = await getClientAsync();
204
- const user = await client.user.me();
205
- spinner.stop();
206
- console.log(`User: ${chalk.cyan(user.email)}`);
207
- console.log(`Name: ${user.displayName || chalk.dim('not set')}`);
208
- console.log(`Role: ${user.role}`);
209
- let plan = user.subscriptionTier;
210
- if (!plan) {
211
- try {
212
- const sub = await client.subscription.get();
213
- plan = sub.subscription.tier;
214
- }
215
- catch { /* API key may not have scope */ }
199
+ if (!config.apiKey && !config.accessToken) {
200
+ out.record({
201
+ apiUrl: config.apiUrl,
202
+ apiKey: null,
203
+ auth: 'none',
204
+ });
205
+ return;
206
+ }
207
+ out.startSpinner('Fetching user info...');
208
+ try {
209
+ const client = await getClientAsync();
210
+ const user = await client.user.me();
211
+ out.stopSpinner();
212
+ let plan = user.subscriptionTier;
213
+ if (!plan) {
214
+ try {
215
+ const sub = await client.subscription.get();
216
+ plan = sub.subscription.tier;
216
217
  }
217
- console.log(`Plan: ${plan ? chalk.green(plan) : chalk.dim('unknown')}`);
218
- }
219
- catch (err) {
220
- spinner.fail('Could not fetch user info');
221
- console.error(err instanceof Error ? err.message : String(err));
222
- process.exitCode = 1;
218
+ catch { /* API key may not have scope */ }
223
219
  }
220
+ out.record({
221
+ apiUrl: config.apiUrl,
222
+ apiKey: config.apiKey ? config.apiKey.slice(0, 12) + '...' : null,
223
+ auth: config.accessToken ? 'JWT (email/password)' : 'API key',
224
+ email: user.email,
225
+ displayName: user.displayName || null,
226
+ role: user.role,
227
+ plan: plan || 'unknown',
228
+ });
229
+ }
230
+ catch (err) {
231
+ handleError(out, err, 'Could not fetch user info');
224
232
  }
225
233
  });
226
234
  }
@@ -170,7 +170,7 @@ export function registerBookingCommands(program) {
170
170
  const client = await getClientAsync();
171
171
  await client.booking.deleteSlot(vaultId, slotId);
172
172
  out.stopSpinner();
173
- out.status(chalk.green(`Slot ${slotId} deleted.`));
173
+ out.success(`Slot ${slotId} deleted.`, { id: slotId, deleted: true });
174
174
  }
175
175
  catch (err) {
176
176
  handleError(out, err, 'Delete slot failed');
@@ -31,6 +31,7 @@ export function registerCalendarCommands(program) {
31
31
  out.startSpinner('Loading calendar...');
32
32
  try {
33
33
  vaultId = await resolveVaultId(vaultId);
34
+ out.debug(`API: GET calendar events for vault ${vaultId}`);
34
35
  const client = await getClientAsync();
35
36
  const response = await client.calendar.getActivity(vaultId, {
36
37
  start: _opts.start ?? getDefaultStart(),
@@ -134,9 +135,7 @@ export function registerCalendarCommands(program) {
134
135
  recurrence: _opts.recurrence || null,
135
136
  });
136
137
  out.stopSpinner();
137
- out.status(dateStr === 'clear'
138
- ? chalk.green(`Due date cleared for ${path}`)
139
- : chalk.green(`Due date set to ${dateStr} for ${path}`));
138
+ out.success(dateStr === 'clear' ? `Due date cleared for ${path}` : `Due date set to ${dateStr} for ${path}`, { path, dueDate: dateStr === 'clear' ? null : dateStr });
140
139
  }
141
140
  catch (err) {
142
141
  handleError(out, err, 'Set due date failed');
@@ -290,7 +289,7 @@ export function registerCalendarCommands(program) {
290
289
  const client = await getClientAsync();
291
290
  await client.calendar.deleteEvent(vaultId, eventId);
292
291
  out.stopSpinner();
293
- out.status(chalk.green(`Event ${eventId} deleted.`));
292
+ out.success(`Event ${eventId} deleted.`, { id: eventId, deleted: true });
294
293
  }
295
294
  catch (err) {
296
295
  handleError(out, err, 'Delete event failed');
@@ -809,7 +808,7 @@ NOTE
809
808
  const client = await getClientAsync();
810
809
  await client.calendar.removeParticipant(vaultId, eventId, participantId);
811
810
  out.stopSpinner();
812
- out.status(chalk.green(`Participant ${participantId} removed.`));
811
+ out.success(`Participant ${participantId} removed.`, { id: participantId, eventId, removed: true });
813
812
  }
814
813
  catch (err) {
815
814
  handleError(out, err, 'Remove participant failed');
@@ -84,6 +84,13 @@ EXAMPLES
84
84
  lsvault config use prod
85
85
  lsvault config use dev`)
86
86
  .action((name) => {
87
+ const profiles = listProfiles();
88
+ if (!profiles.includes(name)) {
89
+ process.stderr.write(chalk.red(`Profile '${name}' does not exist.`) + '\n');
90
+ process.stderr.write(chalk.dim('Available profiles: ' + (profiles.length ? profiles.join(', ') : 'none')) + '\n');
91
+ process.exitCode = 1;
92
+ return;
93
+ }
87
94
  setActiveProfile(name);
88
95
  process.stdout.write(chalk.green(`Active profile set to ${chalk.bold(name)}`) + '\n');
89
96
  });
@@ -14,6 +14,7 @@ export function registerConnectorCommands(program) {
14
14
  const out = createOutput(flags);
15
15
  out.startSpinner('Fetching connectors...');
16
16
  try {
17
+ out.debug('API: GET connectors');
17
18
  if (_opts.vault)
18
19
  _opts.vault = await resolveVaultId(String(_opts.vault));
19
20
  const client = await getClientAsync();
@@ -205,12 +206,17 @@ VALID PROVIDERS
205
206
  const out = createOutput(flags);
206
207
  out.startSpinner('Triggering sync...');
207
208
  try {
209
+ out.debug(`API: POST connectors/${connectorId}/sync`);
208
210
  const client = await getClientAsync();
209
211
  const result = await client.connectors.sync(connectorId);
210
212
  out.success(result.message, { message: result.message });
211
213
  }
212
214
  catch (err) {
213
215
  handleError(out, err, 'Failed to trigger sync');
216
+ const msg = err instanceof Error ? err.message : String(err);
217
+ if (/oauth|token|not authorized|unauthorized/i.test(msg)) {
218
+ out.status(chalk.dim('Hint: Ensure the connector has valid OAuth credentials. Run "lsvault connectors create" to set up.'));
219
+ }
214
220
  }
215
221
  });
216
222
  addGlobalFlags(connectors.command('logs')
@@ -3,6 +3,7 @@ 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
  import { resolveVaultId } from '../utils/resolve-vault.js';
6
+ import { loadConfigAsync } from '../config.js';
6
7
  export function registerPublishCommands(program) {
7
8
  const publish = program.command('publish').description('Publish documents to public profile pages');
8
9
  addGlobalFlags(publish.command('list')
@@ -63,6 +64,7 @@ export function registerPublishCommands(program) {
63
64
  out.startSpinner('Publishing document...');
64
65
  try {
65
66
  vaultId = await resolveVaultId(vaultId);
67
+ out.debug(`API: POST publish ${vaultId}/${docPath}`);
66
68
  const client = await getClientAsync();
67
69
  const params = {
68
70
  slug: String(_opts.slug),
@@ -74,9 +76,11 @@ export function registerPublishCommands(program) {
74
76
  if (_opts.ogImage)
75
77
  params.ogImage = String(_opts.ogImage);
76
78
  const pub = await client.publish.create(vaultId, docPath, params);
79
+ const config = await loadConfigAsync();
80
+ const baseUrl = config.apiUrl.replace(/\/api\/v\d+\/?$/, '');
77
81
  out.success('Document published successfully!', {
78
82
  slug: pub.slug,
79
- url: `/${pub.publishedBy}/${pub.slug}`,
83
+ url: `${baseUrl}/${pub.publishedBy}/${pub.slug}`,
80
84
  isPublished: pub.isPublished,
81
85
  seoTitle: pub.seoTitle || null,
82
86
  seoDescription: pub.seoDescription || null,
@@ -156,12 +156,14 @@ export function registerSamlCommands(program) {
156
156
  addGlobalFlags(saml.command('delete-config')
157
157
  .description('Delete an SSO configuration')
158
158
  .argument('<id>', 'SSO config ID')
159
- .option('--force', 'Skip confirmation prompt'))
159
+ .option('--force', 'Skip confirmation prompt')
160
+ .option('-y, --yes', 'Alias for --force'))
160
161
  .action(async (id, _opts) => {
161
162
  const flags = resolveFlags(_opts);
162
163
  const out = createOutput(flags);
163
- if (!_opts.force) {
164
- out.raw(chalk.yellow(`Pass --force to delete SSO config ${id}.`) + '\n');
164
+ if (!_opts.force && !_opts.yes) {
165
+ out.warn(`Pass --force to delete SSO config ${id}.`);
166
+ process.exitCode = 1;
165
167
  return;
166
168
  }
167
169
  out.startSpinner('Deleting SSO config...');
@@ -205,6 +207,11 @@ export function registerSamlCommands(program) {
205
207
  .action(async (slug, _opts) => {
206
208
  const flags = resolveFlags(_opts);
207
209
  const out = createOutput(flags);
210
+ if (!slug || !slug.trim()) {
211
+ out.error('Slug cannot be empty.');
212
+ process.exitCode = 1;
213
+ return;
214
+ }
208
215
  try {
209
216
  const client = await getClientAsync();
210
217
  const url = client.saml.getLoginUrl(slug);
@@ -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 registerTeamCommands(program) {
6
7
  const teams = program.command('teams').description('Manage teams, members, invitations, and shared vaults');
7
8
  // ── Team CRUD ──────────────────────────────────────────────────────
@@ -12,8 +13,10 @@ export function registerTeamCommands(program) {
12
13
  const out = createOutput(flags);
13
14
  out.startSpinner('Fetching teams...');
14
15
  try {
16
+ out.debug('API: GET teams');
15
17
  const client = await getClientAsync();
16
18
  const teamList = await client.teams.list();
19
+ out.debug(`Response: ${teamList.length} teams`);
17
20
  out.stopSpinner();
18
21
  out.list(teamList.map(t => ({ name: t.name, id: t.id, description: t.description || 'No description' })), {
19
22
  emptyMessage: 'No teams found.',
@@ -152,7 +155,7 @@ EXAMPLES
152
155
  .description('Update a member role')
153
156
  .argument('<teamId>', 'Team ID')
154
157
  .argument('<userId>', 'User ID')
155
- .requiredOption('-r, --role <role>', 'New role (admin or member)'))
158
+ .requiredOption('-r, --role <role>', 'New role: admin, editor, or viewer'))
156
159
  .action(async (teamId, userId, _opts) => {
157
160
  const flags = resolveFlags(_opts);
158
161
  const out = createOutput(flags);
@@ -195,10 +198,16 @@ EXAMPLES
195
198
  });
196
199
  addGlobalFlags(teams.command('leave')
197
200
  .description('Leave a team')
198
- .argument('<teamId>', 'Team ID'))
201
+ .argument('<teamId>', 'Team ID')
202
+ .option('-y, --yes', 'Skip confirmation prompt'))
199
203
  .action(async (teamId, _opts) => {
200
204
  const flags = resolveFlags(_opts);
201
205
  const out = createOutput(flags);
206
+ const confirmed = await confirmAction(`Are you sure you want to leave team ${teamId}?`, { yes: !!_opts.yes });
207
+ if (!confirmed) {
208
+ out.status('Cancelled.');
209
+ return;
210
+ }
202
211
  out.startSpinner('Leaving team...');
203
212
  try {
204
213
  const client = await getClientAsync();
@@ -245,11 +254,11 @@ EXAMPLES
245
254
  .description('Invite a user to the team')
246
255
  .argument('<teamId>', 'Team ID')
247
256
  .argument('<email>', 'Email address')
248
- .requiredOption('-r, --role <role>', 'Role (admin or member)'))
257
+ .requiredOption('-r, --role <role>', 'Role to assign: admin, editor, or viewer (default: editor)'))
249
258
  .action(async (teamId, email, _opts) => {
250
259
  const flags = resolveFlags(_opts);
251
260
  const out = createOutput(flags);
252
- const role = String(_opts.role);
261
+ const role = (String(_opts.role) || 'editor');
253
262
  out.startSpinner('Sending invitation...');
254
263
  try {
255
264
  const client = await getClientAsync();
@@ -267,10 +276,16 @@ EXAMPLES
267
276
  addGlobalFlags(invitations.command('revoke')
268
277
  .description('Revoke a pending invitation')
269
278
  .argument('<teamId>', 'Team ID')
270
- .argument('<invitationId>', 'Invitation ID'))
279
+ .argument('<invitationId>', 'Invitation ID')
280
+ .option('-y, --yes', 'Skip confirmation prompt'))
271
281
  .action(async (teamId, invitationId, _opts) => {
272
282
  const flags = resolveFlags(_opts);
273
283
  const out = createOutput(flags);
284
+ const confirmed = await confirmAction(`Revoke invitation ${invitationId}?`, { yes: !!_opts.yes });
285
+ if (!confirmed) {
286
+ out.status('Cancelled.');
287
+ return;
288
+ }
274
289
  out.startSpinner('Revoking invitation...');
275
290
  try {
276
291
  const client = await getClientAsync();
@@ -16,11 +16,13 @@ export function registerVaultCommands(program) {
16
16
  const out = createOutput(flags);
17
17
  out.startSpinner('Fetching vaults...');
18
18
  try {
19
+ out.debug('API: GET vaults');
19
20
  const client = await getClientAsync();
20
21
  const vaultList = await client.vaults.list({
21
22
  includeArchived: _opts.includeArchived === true,
22
23
  });
23
24
  out.stopSpinner();
25
+ out.debug(`Response: ${vaultList.length} vaults`);
24
26
  out.list(vaultList.map(v => ({ name: v.name, slug: v.slug, encrypted: v.encryptionEnabled ? 'yes' : 'no', description: v.description ?? null, id: v.id })), {
25
27
  emptyMessage: 'No vaults found.',
26
28
  columns: [
@@ -118,9 +118,9 @@ export class Output {
118
118
  * - table: prints an ASCII table
119
119
  */
120
120
  list(data, options) {
121
+ if (this.flags.quiet)
122
+ return;
121
123
  if (data.length === 0) {
122
- if (this.flags.quiet)
123
- return;
124
124
  if (this.flags.output === 'json') {
125
125
  process.stdout.write('[]\n');
126
126
  return;
@@ -133,8 +133,6 @@ export class Output {
133
133
  }
134
134
  return;
135
135
  }
136
- if (this.flags.quiet)
137
- return;
138
136
  switch (this.flags.output) {
139
137
  case 'json':
140
138
  process.stdout.write(JSON.stringify(data) + '\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifestreamdynamics/vault-cli",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
4
4
  "description": "Command-line interface for Lifestream Vault",
5
5
  "engines": {
6
6
  "node": ">=22"