@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 +36 -0
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +111 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +84 -0
- package/dist/commands/auth.js +67 -1
- package/dist/commands/calendar.d.ts +2 -0
- package/dist/commands/calendar.js +220 -0
- package/dist/commands/custom-domains.d.ts +2 -0
- package/dist/commands/custom-domains.js +149 -0
- package/dist/commands/docs.js +140 -0
- package/dist/commands/links.d.ts +2 -0
- package/dist/commands/links.js +126 -0
- package/dist/commands/mfa.d.ts +2 -0
- package/dist/commands/mfa.js +224 -0
- package/dist/commands/publish-vault.d.ts +2 -0
- package/dist/commands/publish-vault.js +117 -0
- package/dist/commands/publish.js +51 -0
- package/dist/commands/search.js +7 -2
- package/dist/commands/subscription.js +1 -1
- package/dist/commands/sync.js +4 -1
- package/dist/commands/teams.js +96 -0
- package/dist/commands/user.js +331 -0
- package/dist/commands/vaults.js +223 -0
- package/dist/index.js +14 -0
- package/dist/sync/daemon-worker.js +62 -0
- package/dist/sync/daemon.d.ts +9 -1
- package/dist/sync/daemon.js +22 -1
- package/package.json +2 -2
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,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,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
|
+
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -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,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
|
+
}
|