@lifestreamdynamics/vault-cli 1.0.1 → 1.2.0

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.
package/README.md CHANGED
@@ -20,6 +20,7 @@ A powerful command-line interface for Lifestream Vault - the multi-user Markdown
20
20
  - [Team Commands](#team-commands)
21
21
  - [Sharing & Publishing](#sharing--publishing)
22
22
  - [Hooks & Webhooks](#hooks--webhooks)
23
+ - [Links & Backlinks](#links--backlinks)
23
24
  - [Admin Commands](#admin-commands)
24
25
  - [Sync & Watch Mode](#-sync--watch-mode)
25
26
  - [Configuration](#️-configuration)
@@ -372,6 +373,41 @@ lsvault webhooks create \
372
373
  --secret webhook_secret_key
373
374
  ```
374
375
 
376
+ ### Calendar
377
+
378
+ | Command | Description |
379
+ |---------|-------------|
380
+ | `lsvault calendar view <vaultId>` | Browse calendar views and activity heatmap |
381
+ | `lsvault calendar due <vaultId>` | List documents by due date |
382
+ | `lsvault calendar events <vaultId>` | List calendar events |
383
+ | `lsvault calendar create-event <vaultId>` | Create a calendar event |
384
+ | `lsvault calendar update-event <vaultId> <eventId>` | Update a calendar event |
385
+ | `lsvault calendar delete-event <vaultId> <eventId>` | Delete a calendar event |
386
+
387
+ ### Links & Backlinks
388
+
389
+ | Command | Description |
390
+ |---------|-------------|
391
+ | `lsvault links list <vaultId> <path>` | List forward links from a document |
392
+ | `lsvault links backlinks <vaultId> <path>` | List backlinks pointing to a document |
393
+ | `lsvault links graph <vaultId>` | Get the link graph for a vault |
394
+ | `lsvault links broken <vaultId>` | List unresolved (broken) links in a vault |
395
+
396
+ **Example:**
397
+ ```bash
398
+ # List forward links from a document
399
+ lsvault links list vault_abc123 notes/index.md
400
+
401
+ # Find all documents linking to a specific document
402
+ lsvault links backlinks vault_abc123 notes/important.md
403
+
404
+ # Get the full link graph for visualization
405
+ lsvault links graph vault_abc123 --output json > graph.json
406
+
407
+ # Find broken links
408
+ lsvault links broken vault_abc123
409
+ ```
410
+
375
411
  ### Admin Commands
376
412
 
377
413
  **Note:** Admin commands require admin role.
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerAiCommands(program: Command): void;
@@ -0,0 +1,111 @@
1
+ import chalk from 'chalk';
2
+ import { getClientAsync } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerAiCommands(program) {
6
+ const ai = program.command('ai').description('AI chat and document summarization');
7
+ const sessions = ai.command('sessions').description('AI chat session management');
8
+ addGlobalFlags(sessions.command('list')
9
+ .description('List AI chat sessions'))
10
+ .action(async (_opts) => {
11
+ const flags = resolveFlags(_opts);
12
+ const out = createOutput(flags);
13
+ out.startSpinner('Fetching AI sessions...');
14
+ try {
15
+ const client = await getClientAsync();
16
+ const list = await client.ai.listSessions();
17
+ out.stopSpinner();
18
+ out.list(list.map(s => ({ id: s.id, title: s.title ?? 'Untitled', createdAt: s.createdAt })), {
19
+ emptyMessage: 'No AI sessions found.',
20
+ columns: [
21
+ { key: 'id', header: 'ID' },
22
+ { key: 'title', header: 'Title' },
23
+ { key: 'createdAt', header: 'Created' },
24
+ ],
25
+ textFn: (s) => `${chalk.cyan(String(s.id))} — ${String(s.title)}`,
26
+ });
27
+ }
28
+ catch (err) {
29
+ handleError(out, err, 'Failed to fetch AI sessions');
30
+ }
31
+ });
32
+ addGlobalFlags(sessions.command('get')
33
+ .description('Get an AI chat session with messages')
34
+ .argument('<sessionId>', 'Session ID'))
35
+ .action(async (sessionId, _opts) => {
36
+ const flags = resolveFlags(_opts);
37
+ const out = createOutput(flags);
38
+ out.startSpinner('Fetching AI session...');
39
+ try {
40
+ const client = await getClientAsync();
41
+ const result = await client.ai.getSession(sessionId);
42
+ out.stopSpinner();
43
+ if (flags.output === 'json') {
44
+ out.raw(JSON.stringify(result, null, 2) + '\n');
45
+ }
46
+ else {
47
+ process.stdout.write(`Session: ${chalk.cyan(result.session.id)}\n`);
48
+ process.stdout.write(`Title: ${result.session.title ?? 'Untitled'}\n\n`);
49
+ for (const msg of result.messages ?? []) {
50
+ const role = msg.role === 'assistant' ? chalk.green('AI') : chalk.blue('You');
51
+ process.stdout.write(`${role}: ${String(msg.content ?? '')}\n\n`);
52
+ }
53
+ }
54
+ }
55
+ catch (err) {
56
+ handleError(out, err, 'Failed to fetch AI session');
57
+ }
58
+ });
59
+ addGlobalFlags(sessions.command('delete')
60
+ .description('Delete an AI chat session')
61
+ .argument('<sessionId>', 'Session ID'))
62
+ .action(async (sessionId, _opts) => {
63
+ const flags = resolveFlags(_opts);
64
+ const out = createOutput(flags);
65
+ out.startSpinner('Deleting AI session...');
66
+ try {
67
+ const client = await getClientAsync();
68
+ await client.ai.deleteSession(sessionId);
69
+ out.success('Session deleted', { id: sessionId });
70
+ }
71
+ catch (err) {
72
+ handleError(out, err, 'Failed to delete AI session');
73
+ }
74
+ });
75
+ addGlobalFlags(ai.command('chat')
76
+ .description('Send a message in an AI chat session')
77
+ .argument('<sessionId>', 'Session ID')
78
+ .argument('<message>', 'Message to send'))
79
+ .action(async (sessionId, message, _opts) => {
80
+ const flags = resolveFlags(_opts);
81
+ const out = createOutput(flags);
82
+ out.startSpinner('Sending message...');
83
+ try {
84
+ const client = await getClientAsync();
85
+ const response = await client.ai.chat({ message, sessionId });
86
+ out.stopSpinner();
87
+ process.stdout.write(String(response.content ?? JSON.stringify(response)) + '\n');
88
+ }
89
+ catch (err) {
90
+ handleError(out, err, 'Failed to send AI message');
91
+ }
92
+ });
93
+ addGlobalFlags(ai.command('summarize')
94
+ .description('Summarize a document with AI')
95
+ .argument('<vaultId>', 'Vault ID')
96
+ .argument('<docPath>', 'Document path'))
97
+ .action(async (vaultId, docPath, _opts) => {
98
+ const flags = resolveFlags(_opts);
99
+ const out = createOutput(flags);
100
+ out.startSpinner('Summarizing document...');
101
+ try {
102
+ const client = await getClientAsync();
103
+ const summary = await client.ai.summarize(vaultId, docPath);
104
+ out.stopSpinner();
105
+ process.stdout.write(String(summary.summary ?? JSON.stringify(summary)) + '\n');
106
+ }
107
+ catch (err) {
108
+ handleError(out, err, 'Failed to summarize document');
109
+ }
110
+ });
111
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerAnalyticsCommands(program: Command): void;
@@ -0,0 +1,84 @@
1
+ import chalk from 'chalk';
2
+ import { getClientAsync } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerAnalyticsCommands(program) {
6
+ const analytics = program.command('analytics').description('Analytics for published documents and share links');
7
+ addGlobalFlags(analytics.command('published')
8
+ .description('Summary of published document views'))
9
+ .action(async (_opts) => {
10
+ const flags = resolveFlags(_opts);
11
+ const out = createOutput(flags);
12
+ out.startSpinner('Fetching analytics...');
13
+ try {
14
+ const client = await getClientAsync();
15
+ const summary = await client.analytics.getPublishedSummary();
16
+ out.stopSpinner();
17
+ if (flags.output === 'json') {
18
+ out.raw(JSON.stringify(summary, null, 2) + '\n');
19
+ }
20
+ else {
21
+ process.stdout.write(`Total published: ${summary.totalPublished}, Total views: ${summary.totalViews}\n\n`);
22
+ out.list(summary.documents.map(d => ({ slug: d.slug, title: d.title ?? '', viewCount: d.viewCount, publishedAt: d.publishedAt })), {
23
+ emptyMessage: 'No published documents.',
24
+ columns: [
25
+ { key: 'slug', header: 'Slug' },
26
+ { key: 'title', header: 'Title' },
27
+ { key: 'viewCount', header: 'Views' },
28
+ { key: 'publishedAt', header: 'Published' },
29
+ ],
30
+ textFn: (d) => `${chalk.cyan(String(d.slug))} — ${String(d.viewCount)} views`,
31
+ });
32
+ }
33
+ }
34
+ catch (err) {
35
+ handleError(out, err, 'Failed to fetch analytics');
36
+ }
37
+ });
38
+ addGlobalFlags(analytics.command('share')
39
+ .description('Analytics for a share link')
40
+ .argument('<vaultId>', 'Vault ID')
41
+ .argument('<shareId>', 'Share ID'))
42
+ .action(async (vaultId, shareId, _opts) => {
43
+ const flags = resolveFlags(_opts);
44
+ const out = createOutput(flags);
45
+ out.startSpinner('Fetching share analytics...');
46
+ try {
47
+ const client = await getClientAsync();
48
+ const data = await client.analytics.getShareAnalytics(vaultId, shareId);
49
+ out.stopSpinner();
50
+ out.record({
51
+ shareId: data.shareId,
52
+ viewCount: data.viewCount,
53
+ uniqueViewers: data.uniqueViewers,
54
+ lastViewedAt: data.lastViewedAt,
55
+ });
56
+ }
57
+ catch (err) {
58
+ handleError(out, err, 'Failed to fetch share analytics');
59
+ }
60
+ });
61
+ addGlobalFlags(analytics.command('doc')
62
+ .description('Analytics for a published document')
63
+ .argument('<vaultId>', 'Vault ID')
64
+ .argument('<publishedDocId>', 'Published document ID'))
65
+ .action(async (vaultId, publishedDocId, _opts) => {
66
+ const flags = resolveFlags(_opts);
67
+ const out = createOutput(flags);
68
+ out.startSpinner('Fetching document analytics...');
69
+ try {
70
+ const client = await getClientAsync();
71
+ const data = await client.analytics.getPublishedDocAnalytics(vaultId, publishedDocId);
72
+ out.stopSpinner();
73
+ out.record({
74
+ publishedDocId: data.publishedDocId,
75
+ viewCount: data.viewCount,
76
+ uniqueViewers: data.uniqueViewers,
77
+ lastViewedAt: data.lastViewedAt,
78
+ });
79
+ }
80
+ catch (err) {
81
+ handleError(out, err, 'Failed to fetch document analytics');
82
+ }
83
+ });
84
+ }
@@ -11,11 +11,13 @@ export function registerAuthCommands(program) {
11
11
  .option('--api-key <key>', 'API key (lsv_k_... prefix)')
12
12
  .option('--email <email>', 'Email address for password login')
13
13
  .option('--password <password>', 'Password (prompts interactively if omitted)')
14
+ .option('--mfa-code <code>', 'MFA code (TOTP or backup code) if account has MFA enabled')
14
15
  .option('--api-url <url>', 'API server URL (default: https://vault.lifestreamdynamics.com)')
15
16
  .addHelpText('after', `
16
17
  EXAMPLES
17
18
  lsvault auth login --api-key lsv_k_abc123
18
19
  lsvault auth login --email user@example.com
20
+ lsvault auth login --email user@example.com --mfa-code 123456
19
21
  lsvault auth login --email user@example.com --api-url https://api.example.com`)
20
22
  .action(async (opts) => {
21
23
  const cm = getCredentialManager();
@@ -42,7 +44,20 @@ EXAMPLES
42
44
  const apiUrl = opts.apiUrl ?? config.apiUrl;
43
45
  const spinner = ora('Authenticating...').start();
44
46
  try {
45
- const { tokens, refreshToken } = await LifestreamVaultClient.login(apiUrl, opts.email, password);
47
+ const { tokens, refreshToken } = await LifestreamVaultClient.login(apiUrl, opts.email, password, {}, {
48
+ mfaCode: opts.mfaCode,
49
+ onMfaRequired: async (challenge) => {
50
+ spinner.stop();
51
+ console.log(chalk.yellow('MFA required for this account.'));
52
+ console.log(`Available methods: ${challenge.methods.join(', ')}`);
53
+ const code = await promptMfaCode();
54
+ if (!code) {
55
+ throw new Error('MFA code is required');
56
+ }
57
+ spinner.start('Verifying MFA code...');
58
+ return { method: 'totp', code };
59
+ },
60
+ });
46
61
  // Save tokens to secure storage
47
62
  await cm.saveCredentials({
48
63
  accessToken: tokens.accessToken,
@@ -245,6 +260,57 @@ async function promptPassword() {
245
260
  process.stdin.resume();
246
261
  });
247
262
  }
263
+ /**
264
+ * Prompt for an MFA code from stdin (6 digits, non-echoing).
265
+ * Returns the code or null if stdin is not a TTY.
266
+ */
267
+ async function promptMfaCode() {
268
+ // In non-interactive mode, cannot prompt
269
+ if (!process.stdin.isTTY) {
270
+ return null;
271
+ }
272
+ const readline = await import('node:readline');
273
+ return new Promise((resolve) => {
274
+ const rl = readline.createInterface({
275
+ input: process.stdin,
276
+ output: process.stderr,
277
+ terminal: true,
278
+ });
279
+ // Disable echoing
280
+ process.stderr.write('MFA code: ');
281
+ process.stdin.setRawMode?.(true);
282
+ let code = '';
283
+ const onData = (chunk) => {
284
+ const char = chunk.toString('utf-8');
285
+ if (char === '\n' || char === '\r' || char === '\u0004') {
286
+ process.stderr.write('\n');
287
+ process.stdin.setRawMode?.(false);
288
+ process.stdin.removeListener('data', onData);
289
+ rl.close();
290
+ resolve(code);
291
+ }
292
+ else if (char === '\u0003') {
293
+ // Ctrl+C
294
+ process.stderr.write('\n');
295
+ process.stdin.setRawMode?.(false);
296
+ process.stdin.removeListener('data', onData);
297
+ rl.close();
298
+ resolve(null);
299
+ }
300
+ else if (char === '\u007F' || char === '\b') {
301
+ // Backspace
302
+ if (code.length > 0) {
303
+ code = code.slice(0, -1);
304
+ }
305
+ }
306
+ else {
307
+ code += char;
308
+ }
309
+ };
310
+ process.stdin.on('data', onData);
311
+ process.stdin.resume();
312
+ });
313
+ }
248
314
  function formatMethod(method) {
249
315
  switch (method) {
250
316
  case 'keychain': return chalk.green('OS Keychain');
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCalendarCommands(program: Command): void;
@@ -0,0 +1,220 @@
1
+ import chalk from 'chalk';
2
+ import { getClientAsync } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerCalendarCommands(program) {
6
+ const calendar = program.command('calendar').description('Document calendar and due date management');
7
+ // calendar view
8
+ addGlobalFlags(calendar.command('view')
9
+ .description('View calendar activity for a vault')
10
+ .argument('<vaultId>', 'Vault ID')
11
+ .option('--start <date>', 'Start date (YYYY-MM-DD)', getDefaultStart())
12
+ .option('--end <date>', 'End date (YYYY-MM-DD)', getDefaultEnd()))
13
+ .action(async (vaultId, _opts) => {
14
+ const flags = resolveFlags(_opts);
15
+ const out = createOutput(flags);
16
+ out.startSpinner('Loading calendar...');
17
+ try {
18
+ const client = await getClientAsync();
19
+ const response = await client.calendar.getActivity(vaultId, {
20
+ start: _opts.start,
21
+ end: _opts.end,
22
+ });
23
+ out.stopSpinner();
24
+ if (flags.output === 'text') {
25
+ out.status(chalk.dim(`Activity from ${response.start} to ${response.end}:\n`));
26
+ }
27
+ out.list(response.days.map(d => ({
28
+ date: d.date,
29
+ created: String(d.created),
30
+ updated: String(d.updated),
31
+ deleted: String(d.deleted),
32
+ total: String(d.total),
33
+ })), {
34
+ emptyMessage: 'No activity in this period.',
35
+ columns: [
36
+ { key: 'date', header: 'Date' },
37
+ { key: 'created', header: 'Created' },
38
+ { key: 'updated', header: 'Updated' },
39
+ { key: 'deleted', header: 'Deleted' },
40
+ { key: 'total', header: 'Total' },
41
+ ],
42
+ textFn: (d) => {
43
+ const bar = '█'.repeat(Math.min(Number(d.total), 20));
44
+ return `${chalk.dim(String(d.date))} ${chalk.green(bar)} ${chalk.bold(String(d.total))}`;
45
+ },
46
+ });
47
+ }
48
+ catch (err) {
49
+ handleError(out, err, 'Calendar view failed');
50
+ }
51
+ });
52
+ // calendar due
53
+ addGlobalFlags(calendar.command('due')
54
+ .description('List documents with due dates')
55
+ .argument('<vaultId>', 'Vault ID')
56
+ .option('--status <status>', 'Filter: overdue, upcoming, all', 'all'))
57
+ .action(async (vaultId, _opts) => {
58
+ const flags = resolveFlags(_opts);
59
+ const out = createOutput(flags);
60
+ out.startSpinner('Loading due dates...');
61
+ try {
62
+ const client = await getClientAsync();
63
+ const docs = await client.calendar.getDueDates(vaultId, {
64
+ status: _opts.status,
65
+ });
66
+ out.stopSpinner();
67
+ out.list(docs.map(d => ({
68
+ title: d.title || d.path,
69
+ path: d.path,
70
+ dueAt: d.dueAt,
71
+ priority: d.priority || '-',
72
+ status: d.overdue ? 'OVERDUE' : d.completed ? 'Done' : 'Pending',
73
+ })), {
74
+ emptyMessage: 'No documents with due dates.',
75
+ columns: [
76
+ { key: 'title', header: 'Title' },
77
+ { key: 'dueAt', header: 'Due' },
78
+ { key: 'priority', header: 'Priority' },
79
+ { key: 'status', header: 'Status' },
80
+ ],
81
+ textFn: (d) => {
82
+ const statusColor = d.status === 'OVERDUE' ? chalk.red : d.status === 'Done' ? chalk.green : chalk.yellow;
83
+ return `${chalk.cyan(String(d.title))} — due ${chalk.dim(String(d.dueAt))} ${statusColor(String(d.status))}`;
84
+ },
85
+ });
86
+ }
87
+ catch (err) {
88
+ handleError(out, err, 'Due dates failed');
89
+ }
90
+ });
91
+ // calendar set-due
92
+ addGlobalFlags(calendar.command('set-due')
93
+ .description('Set due date on a document')
94
+ .argument('<vaultId>', 'Vault ID')
95
+ .argument('<path>', 'Document path')
96
+ .requiredOption('--date <date>', 'Due date (YYYY-MM-DD or "clear")')
97
+ .option('--priority <priority>', 'Priority (low/medium/high)')
98
+ .option('--recurrence <recurrence>', 'Recurrence (daily/weekly/monthly/yearly)'))
99
+ .action(async (vaultId, path, _opts) => {
100
+ const flags = resolveFlags(_opts);
101
+ const out = createOutput(flags);
102
+ out.startSpinner('Setting due date...');
103
+ try {
104
+ const client = await getClientAsync();
105
+ const dateStr = _opts.date;
106
+ await client.calendar.setDocumentDue(vaultId, path, {
107
+ dueAt: dateStr === 'clear' ? null : new Date(dateStr).toISOString(),
108
+ priority: _opts.priority || null,
109
+ recurrence: _opts.recurrence || null,
110
+ });
111
+ out.stopSpinner();
112
+ out.status(dateStr === 'clear'
113
+ ? chalk.green(`Due date cleared for ${path}`)
114
+ : chalk.green(`Due date set to ${dateStr} for ${path}`));
115
+ }
116
+ catch (err) {
117
+ handleError(out, err, 'Set due date failed');
118
+ }
119
+ });
120
+ // calendar events
121
+ addGlobalFlags(calendar.command('events')
122
+ .description('List calendar events')
123
+ .argument('<vaultId>', 'Vault ID')
124
+ .option('--start <date>', 'Start date')
125
+ .option('--end <date>', 'End date'))
126
+ .action(async (vaultId, _opts) => {
127
+ const flags = resolveFlags(_opts);
128
+ const out = createOutput(flags);
129
+ out.startSpinner('Loading events...');
130
+ try {
131
+ const client = await getClientAsync();
132
+ const events = await client.calendar.listEvents(vaultId, {
133
+ start: _opts.start,
134
+ end: _opts.end,
135
+ });
136
+ out.stopSpinner();
137
+ out.list(events.map(e => ({
138
+ title: e.title,
139
+ startDate: e.startDate,
140
+ priority: e.priority || '-',
141
+ completed: e.completed ? '✓' : '-',
142
+ })), {
143
+ emptyMessage: 'No calendar events.',
144
+ columns: [
145
+ { key: 'title', header: 'Title' },
146
+ { key: 'startDate', header: 'Date' },
147
+ { key: 'priority', header: 'Priority' },
148
+ { key: 'completed', header: 'Done' },
149
+ ],
150
+ textFn: (e) => `${chalk.cyan(String(e.title))} — ${chalk.dim(String(e.startDate))}`,
151
+ });
152
+ }
153
+ catch (err) {
154
+ handleError(out, err, 'Calendar events failed');
155
+ }
156
+ });
157
+ addGlobalFlags(calendar.command('agenda')
158
+ .description('View due-date agenda grouped by time period')
159
+ .argument('<vaultId>', 'Vault ID')
160
+ .option('--status <status>', 'Filter by status')
161
+ .option('--range <range>', 'Time range (e.g., week, month)')
162
+ .option('--group-by <groupBy>', 'Group by field'))
163
+ .action(async (vaultId, _opts) => {
164
+ const flags = resolveFlags(_opts);
165
+ const out = createOutput(flags);
166
+ out.startSpinner('Fetching agenda...');
167
+ try {
168
+ const client = await getClientAsync();
169
+ const agenda = await client.calendar.getAgenda(vaultId, {
170
+ status: _opts.status,
171
+ range: _opts.range,
172
+ groupBy: _opts.groupBy,
173
+ });
174
+ out.stopSpinner();
175
+ if (flags.output === 'json') {
176
+ out.raw(JSON.stringify(agenda, null, 2) + '\n');
177
+ }
178
+ else {
179
+ process.stdout.write(`Total: ${agenda.total} items\n\n`);
180
+ for (const group of agenda.groups) {
181
+ process.stdout.write(`${chalk.bold(group.label)}\n`);
182
+ for (const item of group.items) {
183
+ process.stdout.write(` ${chalk.cyan(item.path ?? '')} — due: ${String(item.dueAt ?? 'N/A')}\n`);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ catch (err) {
189
+ handleError(out, err, 'Failed to fetch agenda');
190
+ }
191
+ });
192
+ addGlobalFlags(calendar.command('ical')
193
+ .description('Output iCal feed for a vault to stdout')
194
+ .argument('<vaultId>', 'Vault ID')
195
+ .option('--include <types>', 'Types to include'))
196
+ .action(async (vaultId, _opts) => {
197
+ const flags = resolveFlags(_opts);
198
+ const out = createOutput(flags);
199
+ try {
200
+ const client = await getClientAsync();
201
+ const ical = await client.calendar.getIcalFeed(vaultId, {
202
+ include: _opts.include,
203
+ });
204
+ process.stdout.write(ical);
205
+ }
206
+ catch (err) {
207
+ handleError(out, err, 'Failed to fetch iCal feed');
208
+ }
209
+ });
210
+ }
211
+ function getDefaultStart() {
212
+ const d = new Date();
213
+ d.setDate(1);
214
+ return d.toISOString().split('T')[0];
215
+ }
216
+ function getDefaultEnd() {
217
+ const d = new Date();
218
+ d.setMonth(d.getMonth() + 1, 0);
219
+ return d.toISOString().split('T')[0];
220
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCustomDomainCommands(program: Command): void;