@lifestreamdynamics/vault-cli 1.3.7 → 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.
@@ -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) => {
@@ -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);
@@ -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);
@@ -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');
@@ -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);
@@ -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');
@@ -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
@@ -83,6 +84,13 @@ EXAMPLES
83
84
  lsvault config use prod
84
85
  lsvault config use dev`)
85
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
+ }
86
94
  setActiveProfile(name);
87
95
  process.stdout.write(chalk.green(`Active profile set to ${chalk.bold(name)}`) + '\n');
88
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')
@@ -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)}...`);