@lifestreamdynamics/vault-cli 1.3.5 → 1.3.7

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.
@@ -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 registerPublishCommands(program) {
6
7
  const publish = program.command('publish').description('Publish documents to public profile pages');
7
8
  addGlobalFlags(publish.command('list')
8
9
  .description('List your published documents')
9
- .argument('<vaultId>', 'Vault ID (required by route)'))
10
+ .argument('<vaultId>', 'Vault ID or slug (required by route)'))
10
11
  .action(async (vaultId, _opts) => {
11
12
  const flags = resolveFlags(_opts);
12
13
  const out = createOutput(flags);
13
14
  out.startSpinner('Fetching published documents...');
14
15
  try {
16
+ vaultId = await resolveVaultId(vaultId);
15
17
  const client = await getClientAsync();
16
18
  const docs = await client.publish.listMine(vaultId);
17
19
  out.stopSpinner();
@@ -49,7 +51,7 @@ export function registerPublishCommands(program) {
49
51
  });
50
52
  addGlobalFlags(publish.command('create')
51
53
  .description('Publish a document')
52
- .argument('<vaultId>', 'Vault ID')
54
+ .argument('<vaultId>', 'Vault ID or slug')
53
55
  .argument('<docPath>', 'Document path (e.g., blog/post.md)')
54
56
  .requiredOption('--slug <slug>', 'URL-friendly slug for the published page')
55
57
  .option('--title <title>', 'SEO title')
@@ -60,6 +62,7 @@ export function registerPublishCommands(program) {
60
62
  const out = createOutput(flags);
61
63
  out.startSpinner('Publishing document...');
62
64
  try {
65
+ vaultId = await resolveVaultId(vaultId);
63
66
  const client = await getClientAsync();
64
67
  const params = {
65
68
  slug: String(_opts.slug),
@@ -73,6 +76,7 @@ export function registerPublishCommands(program) {
73
76
  const pub = await client.publish.create(vaultId, docPath, params);
74
77
  out.success('Document published successfully!', {
75
78
  slug: pub.slug,
79
+ url: `/${pub.publishedBy}/${pub.slug}`,
76
80
  isPublished: pub.isPublished,
77
81
  seoTitle: pub.seoTitle || null,
78
82
  seoDescription: pub.seoDescription || null,
@@ -85,7 +89,7 @@ export function registerPublishCommands(program) {
85
89
  });
86
90
  addGlobalFlags(publish.command('update')
87
91
  .description('Update a published document')
88
- .argument('<vaultId>', 'Vault ID')
92
+ .argument('<vaultId>', 'Vault ID or slug')
89
93
  .argument('<docPath>', 'Document path (e.g., blog/post.md)')
90
94
  .requiredOption('--slug <slug>', 'URL-friendly slug (required for updates)')
91
95
  .option('--title <title>', 'SEO title')
@@ -96,6 +100,7 @@ export function registerPublishCommands(program) {
96
100
  const out = createOutput(flags);
97
101
  out.startSpinner('Updating published document...');
98
102
  try {
103
+ vaultId = await resolveVaultId(vaultId);
99
104
  const client = await getClientAsync();
100
105
  const params = {
101
106
  slug: String(_opts.slug),
@@ -120,7 +125,7 @@ export function registerPublishCommands(program) {
120
125
  });
121
126
  addGlobalFlags(publish.command('delete')
122
127
  .description('Unpublish a document')
123
- .argument('<vaultId>', 'Vault ID')
128
+ .argument('<vaultId>', 'Vault ID or slug')
124
129
  .argument('<docPath>', 'Document path (e.g., blog/post.md)')
125
130
  .option('-y, --yes', 'Skip confirmation prompt'))
126
131
  .action(async (vaultId, docPath, _opts) => {
@@ -132,6 +137,7 @@ export function registerPublishCommands(program) {
132
137
  }
133
138
  out.startSpinner('Unpublishing document...');
134
139
  try {
140
+ vaultId = await resolveVaultId(vaultId);
135
141
  const client = await getClientAsync();
136
142
  await client.publish.delete(vaultId, docPath);
137
143
  out.success('Document unpublished successfully', { path: docPath, unpublished: true });
@@ -143,16 +149,25 @@ export function registerPublishCommands(program) {
143
149
  const subdomain = publish.command('subdomain').description('Subdomain management for published vaults');
144
150
  addGlobalFlags(subdomain.command('get')
145
151
  .description('Get the subdomain for a published vault')
146
- .argument('<vaultId>', 'Vault ID'))
152
+ .argument('<vaultId>', 'Vault ID or slug'))
147
153
  .action(async (vaultId, _opts) => {
148
154
  const flags = resolveFlags(_opts);
149
155
  const out = createOutput(flags);
150
156
  out.startSpinner('Fetching subdomain...');
151
157
  try {
158
+ vaultId = await resolveVaultId(vaultId);
152
159
  const client = await getClientAsync();
153
160
  const result = await client.publish.getSubdomain(vaultId);
154
161
  out.stopSpinner();
155
- out.record({ subdomain: result.subdomain });
162
+ if (flags.output === 'json') {
163
+ out.record({ subdomain: result.subdomain });
164
+ }
165
+ else if (result.subdomain == null) {
166
+ out.status('No subdomain configured.');
167
+ }
168
+ else {
169
+ out.record({ subdomain: result.subdomain });
170
+ }
156
171
  }
157
172
  catch (err) {
158
173
  handleError(out, err, 'Failed to fetch subdomain');
@@ -160,13 +175,14 @@ export function registerPublishCommands(program) {
160
175
  });
161
176
  addGlobalFlags(subdomain.command('set')
162
177
  .description('Set a subdomain for a published vault')
163
- .argument('<vaultId>', 'Vault ID')
178
+ .argument('<vaultId>', 'Vault ID or slug')
164
179
  .argument('<subdomain>', 'Subdomain to assign'))
165
180
  .action(async (vaultId, subdomainArg, _opts) => {
166
181
  const flags = resolveFlags(_opts);
167
182
  const out = createOutput(flags);
168
183
  out.startSpinner('Setting subdomain...');
169
184
  try {
185
+ vaultId = await resolveVaultId(vaultId);
170
186
  const client = await getClientAsync();
171
187
  const result = await client.publish.setSubdomain(vaultId, subdomainArg);
172
188
  out.success(`Subdomain set: ${result.subdomain}`, { subdomain: result.subdomain });
@@ -177,7 +193,7 @@ export function registerPublishCommands(program) {
177
193
  });
178
194
  addGlobalFlags(subdomain.command('delete')
179
195
  .description('Remove the subdomain for a published vault')
180
- .argument('<vaultId>', 'Vault ID')
196
+ .argument('<vaultId>', 'Vault ID or slug')
181
197
  .option('-y, --yes', 'Skip confirmation prompt'))
182
198
  .action(async (vaultId, _opts) => {
183
199
  const flags = resolveFlags(_opts);
@@ -188,6 +204,7 @@ export function registerPublishCommands(program) {
188
204
  }
189
205
  out.startSpinner('Removing subdomain...');
190
206
  try {
207
+ vaultId = await resolveVaultId(vaultId);
191
208
  const client = await getClientAsync();
192
209
  const result = await client.publish.deleteSubdomain(vaultId);
193
210
  out.success(result.message, { message: result.message });
@@ -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 { resolveVaultId } from '../utils/resolve-vault.js';
5
6
  export function registerSearchCommands(program) {
6
7
  addGlobalFlags(program.command('search')
7
8
  .description('Full-text search across all documents')
@@ -21,6 +22,8 @@ EXAMPLES
21
22
  const out = createOutput(flags);
22
23
  out.startSpinner('Searching...');
23
24
  try {
25
+ if (_opts.vault)
26
+ _opts.vault = await resolveVaultId(String(_opts.vault));
24
27
  const client = await getClientAsync();
25
28
  const mode = _opts.mode;
26
29
  const response = await client.search.search({
@@ -31,6 +34,9 @@ EXAMPLES
31
34
  mode: mode,
32
35
  });
33
36
  out.stopSpinner();
37
+ if (response.results.length === 0 && mode === 'semantic') {
38
+ out.warn('Semantic search returned no results. Ensure document embeddings have been generated (the embedding worker must be running).');
39
+ }
34
40
  if (flags.output === 'text') {
35
41
  const modeInfo = mode && mode !== 'text' ? `[${mode}] ` : '';
36
42
  out.status(chalk.dim(`${modeInfo}${response.total} result(s) for "${response.query}":\n`));
@@ -39,6 +45,7 @@ EXAMPLES
39
45
  title: r.title || r.path,
40
46
  path: r.path,
41
47
  vaultName: r.vaultName,
48
+ rank: r.rank,
42
49
  tags: r.tags.join(', '),
43
50
  snippet: r.snippet ? r.snippet.replace(/<[^>]+>/g, '') : '',
44
51
  })), {
@@ -1,19 +1,22 @@
1
1
  import chalk from 'chalk';
2
2
  import { getClientAsync } from '../client.js';
3
+ import { loadConfigAsync } from '../config.js';
3
4
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
5
  import { createOutput, handleError } from '../utils/output.js';
5
6
  import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
7
+ import { resolveVaultId } from '../utils/resolve-vault.js';
6
8
  export function registerShareCommands(program) {
7
9
  const shares = program.command('shares').description('Create, list, and revoke document share links');
8
10
  addGlobalFlags(shares.command('list')
9
11
  .description('List share links for a document')
10
- .argument('<vaultId>', 'Vault ID')
12
+ .argument('<vaultId>', 'Vault ID or slug')
11
13
  .argument('<docPath>', 'Document path (e.g., notes/meeting.md)'))
12
14
  .action(async (vaultId, docPath, _opts) => {
13
15
  const flags = resolveFlags(_opts);
14
16
  const out = createOutput(flags);
15
17
  out.startSpinner('Fetching share links...');
16
18
  try {
19
+ vaultId = await resolveVaultId(vaultId);
17
20
  const client = await getClientAsync();
18
21
  const links = await client.shares.list(vaultId, docPath);
19
22
  out.stopSpinner();
@@ -53,7 +56,7 @@ export function registerShareCommands(program) {
53
56
  });
54
57
  addGlobalFlags(shares.command('create')
55
58
  .description('Create a share link for a document')
56
- .argument('<vaultId>', 'Vault ID')
59
+ .argument('<vaultId>', 'Vault ID or slug')
57
60
  .argument('<docPath>', 'Document path (e.g., notes/meeting.md)')
58
61
  .option('--permission <perm>', 'Permission level: view or edit', 'view')
59
62
  .option('--protect-with-password', 'Prompt for a password to protect the link (interactive TTY only)')
@@ -85,6 +88,7 @@ export function registerShareCommands(program) {
85
88
  }
86
89
  out.startSpinner('Creating share link...');
87
90
  try {
91
+ vaultId = await resolveVaultId(vaultId);
88
92
  const client = await getClientAsync();
89
93
  const params = {};
90
94
  if (_opts.permission)
@@ -97,9 +101,12 @@ export function registerShareCommands(program) {
97
101
  params.maxViews = parseInt(String(_opts.maxViews), 10);
98
102
  const result = await client.shares.create(vaultId, docPath, params);
99
103
  out.stopSpinner();
104
+ const config = await loadConfigAsync();
105
+ const shareUrl = `${config.apiUrl}/share/${result.fullToken}`;
100
106
  if (flags.output === 'json') {
101
107
  out.record({
102
108
  token: result.fullToken,
109
+ url: shareUrl,
103
110
  id: result.shareLink.id,
104
111
  permission: result.shareLink.permission,
105
112
  expiresAt: result.shareLink.expiresAt || null,
@@ -109,6 +116,7 @@ export function registerShareCommands(program) {
109
116
  else {
110
117
  out.warn('\nIMPORTANT: Save this token. It cannot be retrieved later.\n');
111
118
  process.stdout.write(chalk.green.bold(`Token: ${result.fullToken}\n`));
119
+ process.stdout.write(chalk.green(`URL: ${shareUrl}\n`));
112
120
  process.stdout.write(`\nID: ${result.shareLink.id}\n`);
113
121
  process.stdout.write(`Permission: ${result.shareLink.permission}\n`);
114
122
  if (result.shareLink.expiresAt) {
@@ -125,13 +133,14 @@ export function registerShareCommands(program) {
125
133
  });
126
134
  addGlobalFlags(shares.command('revoke')
127
135
  .description('Revoke a share link')
128
- .argument('<vaultId>', 'Vault ID')
136
+ .argument('<vaultId>', 'Vault ID or slug')
129
137
  .argument('<shareId>', 'Share link ID'))
130
138
  .action(async (vaultId, shareId, _opts) => {
131
139
  const flags = resolveFlags(_opts);
132
140
  const out = createOutput(flags);
133
141
  out.startSpinner('Revoking share link...');
134
142
  try {
143
+ vaultId = await resolveVaultId(vaultId);
135
144
  const client = await getClientAsync();
136
145
  await client.shares.revoke(vaultId, shareId);
137
146
  out.success('Share link revoked successfully', { id: shareId, revoked: true });
@@ -186,8 +186,6 @@ Sync modes:
186
186
  }
187
187
  out.stopSpinner();
188
188
  if (flags.dryRun) {
189
- out.status(chalk.yellow('Dry run — no changes will be made:'));
190
- out.status(formatDiff(diff));
191
189
  if (flags.output === 'json') {
192
190
  out.record({
193
191
  dryRun: true,
@@ -197,6 +195,10 @@ Sync modes:
197
195
  totalBytes: diff.totalBytes,
198
196
  });
199
197
  }
198
+ else {
199
+ out.status(chalk.yellow('Dry run — no changes will be made:'));
200
+ out.status(formatDiff(diff));
201
+ }
200
202
  return;
201
203
  }
202
204
  if (flags.verbose) {
@@ -409,9 +409,19 @@ export function registerUserCommands(program) {
409
409
  addGlobalFlags(consents.command('set')
410
410
  .description('Record a consent decision')
411
411
  .requiredOption('--type <t>', 'Consent type')
412
- .requiredOption('--version <v>', 'Policy version')
412
+ .requiredOption('--policy-version <v>', 'Policy version')
413
413
  .option('--granted', 'Grant consent')
414
- .option('--no-granted', 'Deny consent'))
414
+ .option('--no-granted', 'Deny consent')
415
+ .addHelpText('after', `
416
+ CONSENT TYPES
417
+ terms_of_service Terms of Service acceptance
418
+ privacy_policy Privacy Policy acceptance
419
+ ai_processing AI feature data processing consent
420
+ marketing Marketing communications consent
421
+
422
+ EXAMPLES
423
+ lsvault user consents set --type terms_of_service --policy-version 1.0 --granted
424
+ lsvault user consents set --type ai_processing --policy-version 1.0 --no-granted`))
415
425
  .action(async (_opts) => {
416
426
  const flags = resolveFlags(_opts);
417
427
  const out = createOutput(flags);
@@ -420,7 +430,7 @@ export function registerUserCommands(program) {
420
430
  const client = await getClientAsync();
421
431
  const result = await client.user.recordConsent({
422
432
  consentType: _opts.type,
423
- version: _opts.version,
433
+ version: _opts.policyVersion,
424
434
  granted: _opts.granted !== false,
425
435
  });
426
436
  out.success(result.message, { message: result.message });
@@ -4,6 +4,7 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
5
  import { generateVaultKey } from '@lifestreamdynamics/vault-sdk';
6
6
  import { getCredentialManager } from '../config.js';
7
+ import { resolveVaultId } from '../utils/resolve-vault.js';
7
8
  export function registerVaultCommands(program) {
8
9
  const vaults = program.command('vaults').description('Create, list, and inspect document vaults');
9
10
  addGlobalFlags(vaults.command('list')
@@ -16,7 +17,7 @@ export function registerVaultCommands(program) {
16
17
  const client = await getClientAsync();
17
18
  const vaultList = await client.vaults.list();
18
19
  out.stopSpinner();
19
- out.list(vaultList.map(v => ({ name: v.name, slug: v.slug, encrypted: v.encryptionEnabled ? 'yes' : 'no', description: v.description || 'No description', id: v.id })), {
20
+ out.list(vaultList.map(v => ({ name: v.name, slug: v.slug, encrypted: v.encryptionEnabled ? 'yes' : 'no', description: v.description ?? null, id: v.id })), {
20
21
  emptyMessage: 'No vaults found.',
21
22
  columns: [
22
23
  { key: 'name', header: 'Name' },
@@ -27,7 +28,8 @@ export function registerVaultCommands(program) {
27
28
  ],
28
29
  textFn: (v) => {
29
30
  const encIcon = v.encrypted === 'yes' ? chalk.green(' [encrypted]') : '';
30
- return `${chalk.cyan(String(v.name))} ${chalk.dim(`(${String(v.slug)})`)}${encIcon} -- ${String(v.description)}`;
31
+ const desc = v.description != null ? ` -- ${String(v.description)}` : '';
32
+ return `${chalk.cyan(String(v.name))} ${chalk.dim(`(${String(v.slug)})`)}${encIcon}${desc}`;
31
33
  },
32
34
  });
33
35
  }
@@ -160,12 +162,13 @@ EXAMPLES
160
162
  // vault tree
161
163
  addGlobalFlags(vaults.command('tree')
162
164
  .description('Show vault file tree')
163
- .argument('<vaultId>', 'Vault ID'))
165
+ .argument('<vaultId>', 'Vault ID or slug'))
164
166
  .action(async (vaultId, _opts) => {
165
167
  const flags = resolveFlags(_opts);
166
168
  const out = createOutput(flags);
167
169
  out.startSpinner('Fetching vault tree...');
168
170
  try {
171
+ vaultId = await resolveVaultId(vaultId);
169
172
  const client = await getClientAsync();
170
173
  const tree = await client.vaults.getTree(vaultId);
171
174
  out.stopSpinner();
@@ -2,11 +2,12 @@ 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 registerVersionCommands(program) {
6
7
  const versions = program.command('versions').description('View and manage document version history');
7
8
  addGlobalFlags(versions.command('list')
8
9
  .description('List version history for a document')
9
- .argument('<vaultId>', 'Vault ID')
10
+ .argument('<vaultId>', 'Vault ID or slug')
10
11
  .argument('<path>', 'Document path (e.g., notes/todo.md)')
11
12
  .addHelpText('after', `
12
13
  EXAMPLES
@@ -16,6 +17,7 @@ EXAMPLES
16
17
  const out = createOutput(flags);
17
18
  out.startSpinner('Fetching versions...');
18
19
  try {
20
+ vaultId = await resolveVaultId(vaultId);
19
21
  const client = await getClientAsync();
20
22
  const versionList = await client.documents.listVersions(vaultId, docPath);
21
23
  out.stopSpinner();
@@ -23,7 +25,7 @@ EXAMPLES
23
25
  version: v.versionNum,
24
26
  source: v.changeSource,
25
27
  sizeBytes: v.sizeBytes,
26
- pinned: v.isPinned ? 'yes' : 'no',
28
+ pinned: flags.output === 'json' ? v.isPinned : (v.isPinned ? 'yes' : 'no'),
27
29
  createdAt: v.createdAt,
28
30
  })), {
29
31
  emptyMessage: 'No versions found.',
@@ -49,7 +51,7 @@ EXAMPLES
49
51
  });
50
52
  addGlobalFlags(versions.command('view')
51
53
  .description('View content of a specific version')
52
- .argument('<vaultId>', 'Vault ID')
54
+ .argument('<vaultId>', 'Vault ID or slug')
53
55
  .argument('<path>', 'Document path')
54
56
  .argument('<version>', 'Version number')
55
57
  .addHelpText('after', `
@@ -65,6 +67,7 @@ EXAMPLES
65
67
  return;
66
68
  }
67
69
  try {
70
+ vaultId = await resolveVaultId(vaultId);
68
71
  const client = await getClientAsync();
69
72
  const version = await client.documents.getVersion(vaultId, docPath, versionNum);
70
73
  if (version.content === null) {
@@ -72,7 +75,12 @@ EXAMPLES
72
75
  process.exitCode = 1;
73
76
  return;
74
77
  }
75
- out.raw(version.content);
78
+ if (flags.output === 'json') {
79
+ out.raw(JSON.stringify({ version: version.versionNum, createdAt: version.createdAt, content: version.content }) + '\n');
80
+ }
81
+ else {
82
+ out.raw(version.content);
83
+ }
76
84
  }
77
85
  catch (err) {
78
86
  handleError(out, err, 'Failed to get version');
@@ -80,7 +88,7 @@ EXAMPLES
80
88
  });
81
89
  addGlobalFlags(versions.command('diff')
82
90
  .description('Show diff between two versions')
83
- .argument('<vaultId>', 'Vault ID')
91
+ .argument('<vaultId>', 'Vault ID or slug')
84
92
  .argument('<path>', 'Document path')
85
93
  .argument('<from>', 'Source version number')
86
94
  .argument('<to>', 'Target version number')
@@ -99,6 +107,7 @@ EXAMPLES
99
107
  }
100
108
  out.startSpinner('Computing diff...');
101
109
  try {
110
+ vaultId = await resolveVaultId(vaultId);
102
111
  const client = await getClientAsync();
103
112
  const diff = await client.documents.diffVersions(vaultId, docPath, from, to);
104
113
  out.stopSpinner();
@@ -121,12 +130,18 @@ EXAMPLES
121
130
  }
122
131
  }
123
132
  catch (err) {
124
- handleError(out, err, 'Failed to compute diff');
133
+ const message = err instanceof Error ? err.message : String(err);
134
+ if (message.includes('not found') || message.includes('Not found')) {
135
+ handleError(out, new Error(`Version ${from} or ${to} not found for document "${docPath}". Use 'versions list' to see available versions.`), 'Failed to compute diff');
136
+ }
137
+ else {
138
+ handleError(out, err, 'Failed to compute diff');
139
+ }
125
140
  }
126
141
  });
127
142
  addGlobalFlags(versions.command('restore')
128
143
  .description('Restore a document to a previous version')
129
- .argument('<vaultId>', 'Vault ID')
144
+ .argument('<vaultId>', 'Vault ID or slug')
130
145
  .argument('<path>', 'Document path')
131
146
  .argument('<version>', 'Version number to restore')
132
147
  .addHelpText('after', `
@@ -143,6 +158,7 @@ EXAMPLES
143
158
  }
144
159
  out.startSpinner(`Restoring to version ${versionNum}...`);
145
160
  try {
161
+ vaultId = await resolveVaultId(vaultId);
146
162
  const client = await getClientAsync();
147
163
  const doc = await client.documents.restoreVersion(vaultId, docPath, versionNum);
148
164
  out.success(`Restored ${chalk.cyan(docPath)} to version ${versionNum}`, {
@@ -156,7 +172,7 @@ EXAMPLES
156
172
  });
157
173
  addGlobalFlags(versions.command('pin')
158
174
  .description('Pin a version to prevent pruning')
159
- .argument('<vaultId>', 'Vault ID')
175
+ .argument('<vaultId>', 'Vault ID or slug')
160
176
  .argument('<path>', 'Document path')
161
177
  .argument('<version>', 'Version number to pin')
162
178
  .addHelpText('after', `
@@ -173,6 +189,7 @@ EXAMPLES
173
189
  }
174
190
  out.startSpinner(`Pinning version ${versionNum}...`);
175
191
  try {
192
+ vaultId = await resolveVaultId(vaultId);
176
193
  const client = await getClientAsync();
177
194
  await client.documents.pinVersion(vaultId, docPath, versionNum);
178
195
  out.success(`Pinned version ${versionNum} of ${chalk.cyan(docPath)}`, {
@@ -187,7 +204,7 @@ EXAMPLES
187
204
  });
188
205
  addGlobalFlags(versions.command('unpin')
189
206
  .description('Unpin a version, allowing it to be pruned')
190
- .argument('<vaultId>', 'Vault ID')
207
+ .argument('<vaultId>', 'Vault ID or slug')
191
208
  .argument('<path>', 'Document path')
192
209
  .argument('<version>', 'Version number to unpin')
193
210
  .addHelpText('after', `
@@ -204,6 +221,7 @@ EXAMPLES
204
221
  }
205
222
  out.startSpinner(`Unpinning version ${versionNum}...`);
206
223
  try {
224
+ vaultId = await resolveVaultId(vaultId);
207
225
  const client = await getClientAsync();
208
226
  await client.documents.unpinVersion(vaultId, docPath, versionNum);
209
227
  out.success(`Unpinned version ${versionNum} of ${chalk.cyan(docPath)}`, {
@@ -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 registerWebhookCommands(program) {
6
7
  const webhooks = program.command('webhooks').description('Manage vault webhooks');
7
8
  addGlobalFlags(webhooks.command('list')
8
9
  .description('List all webhooks 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 webhooks...');
14
15
  try {
16
+ vaultId = await resolveVaultId(vaultId);
15
17
  const client = await getClientAsync();
16
18
  const webhookList = await client.webhooks.list(vaultId);
17
19
  out.stopSpinner();
@@ -42,18 +44,45 @@ export function registerWebhookCommands(program) {
42
44
  });
43
45
  addGlobalFlags(webhooks.command('create')
44
46
  .description('Create a new webhook')
45
- .argument('<vaultId>', 'Vault ID')
47
+ .argument('<vaultId>', 'Vault ID or slug')
46
48
  .argument('<url>', 'Webhook endpoint URL')
47
- .option('--events <events>', 'Comma-separated events (e.g., create,update,delete)', 'create,update,delete'))
49
+ .option('--events <events>', 'Comma-separated events (document.created, document.updated, document.deleted, document.moved, document.copied, or * for all)', 'document.created,document.updated,document.deleted')
50
+ .addHelpText('after', `
51
+ VALID EVENT NAMES
52
+ document.created Document was created
53
+ document.updated Document content was updated
54
+ document.deleted Document was deleted
55
+ document.moved Document was moved or renamed
56
+ document.copied Document was copied
57
+ * All events
58
+
59
+ EXAMPLES
60
+ lsvault webhooks create <vaultId> https://example.com/hook
61
+ lsvault webhooks create <vaultId> https://example.com/hook --events "document.created,document.deleted"
62
+ lsvault webhooks create <vaultId> https://example.com/hook --events "*"`))
48
63
  .action(async (vaultId, url, _opts) => {
49
64
  const flags = resolveFlags(_opts);
50
65
  const out = createOutput(flags);
66
+ if (!/^https?:\/\//i.test(url)) {
67
+ out.error(`Invalid webhook URL "${url}". URL must start with http:// or https://`);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const VALID_EVENTS = ['document.created', 'document.updated', 'document.deleted', 'document.moved', 'document.copied', '*'];
72
+ const events = String(_opts.events || 'document.created,document.updated,document.deleted').split(',').map((e) => e.trim());
73
+ const invalid = events.filter(e => !VALID_EVENTS.includes(e));
74
+ if (invalid.length > 0) {
75
+ out.error(`Invalid event name(s): ${invalid.join(', ')}. Valid values: ${VALID_EVENTS.join(', ')}`);
76
+ process.exitCode = 1;
77
+ return;
78
+ }
51
79
  out.startSpinner('Creating webhook...');
52
80
  try {
81
+ vaultId = await resolveVaultId(vaultId);
53
82
  const client = await getClientAsync();
54
83
  const params = {
55
84
  url,
56
- events: String(_opts.events || 'create,update,delete').split(',').map((e) => e.trim()),
85
+ events,
57
86
  };
58
87
  const webhook = await client.webhooks.create(vaultId, params);
59
88
  out.stopSpinner();
@@ -79,7 +108,7 @@ export function registerWebhookCommands(program) {
79
108
  });
80
109
  addGlobalFlags(webhooks.command('update')
81
110
  .description('Update a webhook')
82
- .argument('<vaultId>', 'Vault ID')
111
+ .argument('<vaultId>', 'Vault ID or slug')
83
112
  .argument('<webhookId>', 'Webhook ID')
84
113
  .option('--url <url>', 'New webhook URL')
85
114
  .option('--events <events>', 'Comma-separated events')
@@ -95,6 +124,7 @@ export function registerWebhookCommands(program) {
95
124
  }
96
125
  out.startSpinner('Updating webhook...');
97
126
  try {
127
+ vaultId = await resolveVaultId(vaultId);
98
128
  const client = await getClientAsync();
99
129
  const params = {};
100
130
  if (_opts.url)
@@ -118,7 +148,7 @@ export function registerWebhookCommands(program) {
118
148
  });
119
149
  addGlobalFlags(webhooks.command('delete')
120
150
  .description('Delete a webhook')
121
- .argument('<vaultId>', 'Vault ID')
151
+ .argument('<vaultId>', 'Vault ID or slug')
122
152
  .argument('<webhookId>', 'Webhook ID')
123
153
  .option('-y, --yes', 'Skip confirmation prompt'))
124
154
  .action(async (vaultId, webhookId, _opts) => {
@@ -130,6 +160,7 @@ export function registerWebhookCommands(program) {
130
160
  }
131
161
  out.startSpinner('Deleting webhook...');
132
162
  try {
163
+ vaultId = await resolveVaultId(vaultId);
133
164
  const client = await getClientAsync();
134
165
  await client.webhooks.delete(vaultId, webhookId);
135
166
  out.success('Webhook deleted successfully', { id: webhookId, deleted: true });
@@ -140,13 +171,14 @@ export function registerWebhookCommands(program) {
140
171
  });
141
172
  addGlobalFlags(webhooks.command('deliveries')
142
173
  .description('List recent deliveries for a webhook')
143
- .argument('<vaultId>', 'Vault ID')
174
+ .argument('<vaultId>', 'Vault ID or slug')
144
175
  .argument('<webhookId>', 'Webhook ID'))
145
176
  .action(async (vaultId, webhookId, _opts) => {
146
177
  const flags = resolveFlags(_opts);
147
178
  const out = createOutput(flags);
148
179
  out.startSpinner('Fetching deliveries...');
149
180
  try {
181
+ vaultId = await resolveVaultId(vaultId);
150
182
  const client = await getClientAsync();
151
183
  const deliveries = await client.webhooks.listDeliveries(vaultId, webhookId);
152
184
  out.stopSpinner();
@@ -5,7 +5,7 @@ import chalk from 'chalk';
5
5
  */
6
6
  export function addGlobalFlags(cmd) {
7
7
  return cmd
8
- .option('-o, --output <format>', 'Output format: text, json, table (default: auto)')
8
+ .option('-o, --output <format>', 'Output format: text, json, table (default: auto — text for TTY, json for pipes)')
9
9
  .option('-v, --verbose', 'Verbose output (debug info)')
10
10
  .option('-q, --quiet', 'Minimal output (errors only)')
11
11
  .option('--no-color', 'Disable colored output')
@@ -1,4 +1,4 @@
1
- import type { GlobalFlags } from './flags.js';
1
+ import type { GlobalFlags, OutputFormat } from './flags.js';
2
2
  /**
3
3
  * Column definition for table output.
4
4
  */
@@ -41,8 +41,11 @@ export declare class Output {
41
41
  debug(message: string): void;
42
42
  /**
43
43
  * Print an error message to stderr.
44
+ * When output format is json, writes a JSON error envelope instead of colored text.
44
45
  */
45
46
  error(message: string): void;
47
+ /** Expose the current output format (for error handlers that need it). */
48
+ get format(): OutputFormat;
46
49
  /**
47
50
  * Print a warning message to stderr.
48
51
  */