synap 0.1.0 → 0.3.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/.claude/skills/synap-assistant/SKILL.md +17 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +13 -26
- package/src/cli.js +192 -61
- package/src/storage.js +137 -0
|
@@ -67,6 +67,7 @@ Detect user intent and respond appropriately:
|
|
|
67
67
|
|------|---------|
|
|
68
68
|
| Capture idea | `synap add "your thought here"` |
|
|
69
69
|
| Add todo | `synap todo "task description"` |
|
|
70
|
+
| Add todo with due date | `synap todo "Review PR #42" --due tomorrow` |
|
|
70
71
|
| Add question | `synap question "what you're wondering"` |
|
|
71
72
|
| List active | `synap list` |
|
|
72
73
|
| See all | `synap list --all` |
|
|
@@ -98,6 +99,7 @@ Quick capture of a thought.
|
|
|
98
99
|
```bash
|
|
99
100
|
synap add "What if we used a graph database?"
|
|
100
101
|
synap add "Need to review the API design" --type todo --priority 1
|
|
102
|
+
synap add "Prep for demo" --type todo --due 2025-02-15
|
|
101
103
|
synap add "Meeting notes from standup" --type note --tags "meetings,weekly"
|
|
102
104
|
synap add --type project --title "Website Redesign" "Complete overhaul of the marketing site..."
|
|
103
105
|
```
|
|
@@ -108,6 +110,7 @@ synap add --type project --title "Website Redesign" "Complete overhaul of the ma
|
|
|
108
110
|
- `--priority <1|2|3>`: 1=high, 2=medium, 3=low
|
|
109
111
|
- `--tags <tags>`: Comma-separated tags
|
|
110
112
|
- `--parent <id>`: Parent entry ID
|
|
113
|
+
- `--due <date>`: Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)
|
|
111
114
|
- `--json`: JSON output
|
|
112
115
|
|
|
113
116
|
#### `synap todo <content>`
|
|
@@ -118,6 +121,8 @@ synap todo "Review PR #42"
|
|
|
118
121
|
# Equivalent to: synap add "Review PR #42" --type todo
|
|
119
122
|
```
|
|
120
123
|
|
|
124
|
+
Options: `--priority`, `--tags`, `--parent`, `--due`, `--json`
|
|
125
|
+
|
|
121
126
|
#### `synap question <content>`
|
|
122
127
|
Shorthand for adding a question.
|
|
123
128
|
|
|
@@ -126,6 +131,8 @@ synap question "Should we migrate to TypeScript?"
|
|
|
126
131
|
# Equivalent to: synap add "..." --type question
|
|
127
132
|
```
|
|
128
133
|
|
|
134
|
+
Options: `--priority`, `--tags`, `--parent`, `--due`, `--json`
|
|
135
|
+
|
|
129
136
|
### Query Commands
|
|
130
137
|
|
|
131
138
|
#### `synap list`
|
|
@@ -139,6 +146,9 @@ synap list --status raw # Needs triage
|
|
|
139
146
|
synap list --priority 1 # High priority only
|
|
140
147
|
synap list --tags work,urgent # Has ALL specified tags
|
|
141
148
|
synap list --since 7d # Created in last 7 days
|
|
149
|
+
synap list --overdue # Overdue entries
|
|
150
|
+
synap list --due-before 7d # Due in next 7 days
|
|
151
|
+
synap list --has-due # Entries with due dates
|
|
142
152
|
synap list --json # JSON output for parsing
|
|
143
153
|
```
|
|
144
154
|
|
|
@@ -150,11 +160,16 @@ synap list --json # JSON output for parsing
|
|
|
150
160
|
- `--parent <id>`: Children of specific entry
|
|
151
161
|
- `--orphans`: Only entries without parent
|
|
152
162
|
- `--since <duration>`: e.g., 7d, 24h, 2w
|
|
163
|
+
- `--due-before <date>`: Due before date (YYYY-MM-DD or 3d/1w)
|
|
164
|
+
- `--due-after <date>`: Due after date (YYYY-MM-DD or 3d/1w)
|
|
165
|
+
- `--overdue`: Only overdue entries
|
|
166
|
+
- `--has-due`: Only entries with due dates
|
|
167
|
+
- `--no-due`: Only entries without due dates
|
|
153
168
|
- `--all`: All statuses except archived
|
|
154
169
|
- `--done`: Include done entries
|
|
155
170
|
- `--archived`: Show only archived
|
|
156
171
|
- `--limit <n>`: Max entries (default: 50)
|
|
157
|
-
- `--sort <field>`: created, updated, priority
|
|
172
|
+
- `--sort <field>`: created, updated, priority, due
|
|
158
173
|
- `--reverse`: Reverse sort order
|
|
159
174
|
- `--json`: JSON output
|
|
160
175
|
|
|
@@ -437,6 +452,7 @@ Critical for preventing accidental mass changes:
|
|
|
437
452
|
"tags": ["tag1", "tag2"],
|
|
438
453
|
"parent": "parent-id|null",
|
|
439
454
|
"related": ["id1", "id2"],
|
|
455
|
+
"due": "2026-01-10T23:59:59.000Z",
|
|
440
456
|
"createdAt": "2026-01-05T08:30:00.000Z",
|
|
441
457
|
"updatedAt": "2026-01-05T08:30:00.000Z",
|
|
442
458
|
"source": "cli|agent|import"
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -2,17 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* postinstall.js - Run after npm install
|
|
5
|
-
*
|
|
5
|
+
* Auto-installs/updates the Claude skill
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const os = require('os');
|
|
11
|
-
|
|
12
|
-
const SKILL_NAME = 'synap-assistant';
|
|
13
|
-
const TARGET_SKILL_DIR = path.join(os.homedir(), '.claude', 'skills', SKILL_NAME);
|
|
14
|
-
const TARGET_SKILL_FILE = path.join(TARGET_SKILL_DIR, 'SKILL.md');
|
|
15
|
-
|
|
16
8
|
// ANSI colors
|
|
17
9
|
const CYAN = '\x1b[36m';
|
|
18
10
|
const RESET = '\x1b[0m';
|
|
@@ -23,24 +15,19 @@ async function main() {
|
|
|
23
15
|
console.log(`${BOLD}synap${RESET} - A CLI for externalizing your working memory`);
|
|
24
16
|
console.log('');
|
|
25
17
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
console.log(`${CYAN}Skill modified locally.${RESET} Run 'synap install-skill --force' to update.`);
|
|
37
|
-
}
|
|
38
|
-
} catch (err) {
|
|
39
|
-
console.log(`${CYAN}Skill update failed.${RESET} Run 'synap install-skill' to retry.`);
|
|
18
|
+
// Auto-install/update skill
|
|
19
|
+
try {
|
|
20
|
+
const skillInstaller = require('../src/skill-installer');
|
|
21
|
+
const result = await skillInstaller.install();
|
|
22
|
+
if (result.installed) {
|
|
23
|
+
console.log(`${CYAN}Claude skill installed.${RESET}`);
|
|
24
|
+
} else if (result.skipped) {
|
|
25
|
+
console.log(`${CYAN}Claude skill up to date.${RESET}`);
|
|
26
|
+
} else if (result.needsForce) {
|
|
27
|
+
console.log(`${CYAN}Skill modified locally.${RESET} Run 'synap install-skill --force' to update.`);
|
|
40
28
|
}
|
|
41
|
-
}
|
|
42
|
-
console.log(
|
|
43
|
-
console.log(` ${CYAN}synap install-skill${RESET}`);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.log(`${CYAN}Skill install failed.${RESET} Run 'synap install-skill' to retry.`);
|
|
44
31
|
}
|
|
45
32
|
|
|
46
33
|
console.log('');
|
package/src/cli.js
CHANGED
|
@@ -63,6 +63,24 @@ async function main() {
|
|
|
63
63
|
return date.toLocaleString();
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
// Helper: Format due date for display
|
|
67
|
+
const formatDueDate = (dateStr) => {
|
|
68
|
+
const date = new Date(dateStr);
|
|
69
|
+
if (isNaN(date.getTime())) return 'invalid date';
|
|
70
|
+
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
73
|
+
const startOfDue = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
74
|
+
const diffDays = Math.round((startOfDue - startOfToday) / (24 * 60 * 60 * 1000));
|
|
75
|
+
|
|
76
|
+
if (diffDays === 0) return 'today';
|
|
77
|
+
if (diffDays === 1) return 'tomorrow';
|
|
78
|
+
if (diffDays === -1) return 'yesterday';
|
|
79
|
+
if (diffDays > 1 && diffDays <= 7) return `in ${diffDays} days`;
|
|
80
|
+
if (diffDays < -1 && diffDays >= -7) return `${Math.abs(diffDays)} days ago`;
|
|
81
|
+
return date.toLocaleDateString();
|
|
82
|
+
};
|
|
83
|
+
|
|
66
84
|
// ============================================
|
|
67
85
|
// CAPTURE COMMANDS
|
|
68
86
|
// ============================================
|
|
@@ -75,27 +93,39 @@ async function main() {
|
|
|
75
93
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
76
94
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
77
95
|
.option('--parent <id>', 'Parent entry ID')
|
|
96
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
78
97
|
.option('--json', 'Output as JSON')
|
|
79
98
|
.action(async (contentParts, options) => {
|
|
80
99
|
const content = contentParts.join(' ');
|
|
81
100
|
const tags = mergeTags(options.tags);
|
|
82
101
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
try {
|
|
103
|
+
const entry = await storage.addEntry({
|
|
104
|
+
content,
|
|
105
|
+
title: options.title,
|
|
106
|
+
type: options.type,
|
|
107
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
108
|
+
tags,
|
|
109
|
+
parent: options.parent,
|
|
110
|
+
due: options.due,
|
|
111
|
+
source: 'cli'
|
|
112
|
+
});
|
|
92
113
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
if (options.json) {
|
|
115
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
116
|
+
} else {
|
|
117
|
+
const shortId = entry.id.slice(0, 8);
|
|
118
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
119
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
120
|
+
console.log(chalk.green(`Added ${entry.type} ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (options.json) {
|
|
124
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
125
|
+
} else {
|
|
126
|
+
console.error(chalk.red(err.message));
|
|
127
|
+
}
|
|
128
|
+
process.exit(1);
|
|
99
129
|
}
|
|
100
130
|
});
|
|
101
131
|
|
|
@@ -105,26 +135,38 @@ async function main() {
|
|
|
105
135
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
106
136
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
107
137
|
.option('--parent <id>', 'Parent entry ID')
|
|
138
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
108
139
|
.option('--json', 'Output as JSON')
|
|
109
140
|
.action(async (contentParts, options) => {
|
|
110
141
|
const content = contentParts.join(' ');
|
|
111
142
|
const tags = mergeTags(options.tags);
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
try {
|
|
145
|
+
const entry = await storage.addEntry({
|
|
146
|
+
content,
|
|
147
|
+
type: 'todo',
|
|
148
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
149
|
+
tags,
|
|
150
|
+
parent: options.parent,
|
|
151
|
+
due: options.due,
|
|
152
|
+
source: 'cli'
|
|
153
|
+
});
|
|
121
154
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
155
|
+
if (options.json) {
|
|
156
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
157
|
+
} else {
|
|
158
|
+
const shortId = entry.id.slice(0, 8);
|
|
159
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
160
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
161
|
+
console.log(chalk.green(`Added todo ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (options.json) {
|
|
165
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
166
|
+
} else {
|
|
167
|
+
console.error(chalk.red(err.message));
|
|
168
|
+
}
|
|
169
|
+
process.exit(1);
|
|
128
170
|
}
|
|
129
171
|
});
|
|
130
172
|
|
|
@@ -134,26 +176,38 @@ async function main() {
|
|
|
134
176
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
135
177
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
136
178
|
.option('--parent <id>', 'Parent entry ID')
|
|
179
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
137
180
|
.option('--json', 'Output as JSON')
|
|
138
181
|
.action(async (contentParts, options) => {
|
|
139
182
|
const content = contentParts.join(' ');
|
|
140
183
|
const tags = mergeTags(options.tags);
|
|
141
184
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
try {
|
|
186
|
+
const entry = await storage.addEntry({
|
|
187
|
+
content,
|
|
188
|
+
type: 'question',
|
|
189
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
190
|
+
tags,
|
|
191
|
+
parent: options.parent,
|
|
192
|
+
due: options.due,
|
|
193
|
+
source: 'cli'
|
|
194
|
+
});
|
|
150
195
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
196
|
+
if (options.json) {
|
|
197
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
const shortId = entry.id.slice(0, 8);
|
|
200
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
201
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
202
|
+
console.log(chalk.green(`Added question ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (options.json) {
|
|
206
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
207
|
+
} else {
|
|
208
|
+
console.error(chalk.red(err.message));
|
|
209
|
+
}
|
|
210
|
+
process.exit(1);
|
|
157
211
|
}
|
|
158
212
|
});
|
|
159
213
|
|
|
@@ -163,21 +217,32 @@ async function main() {
|
|
|
163
217
|
const content = contentParts.join(' ');
|
|
164
218
|
const tags = mergeTags(options.tags);
|
|
165
219
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
220
|
+
try {
|
|
221
|
+
const entry = await storage.addEntry({
|
|
222
|
+
content,
|
|
223
|
+
type: typeName,
|
|
224
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
225
|
+
tags,
|
|
226
|
+
parent: options.parent,
|
|
227
|
+
due: options.due,
|
|
228
|
+
source: 'cli'
|
|
229
|
+
});
|
|
174
230
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
231
|
+
if (options.json) {
|
|
232
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
233
|
+
} else {
|
|
234
|
+
const shortId = entry.id.slice(0, 8);
|
|
235
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
236
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
237
|
+
console.log(chalk.green(`Added ${displayName} ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (options.json) {
|
|
241
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
242
|
+
} else {
|
|
243
|
+
console.error(chalk.red(err.message));
|
|
244
|
+
}
|
|
245
|
+
process.exit(1);
|
|
181
246
|
}
|
|
182
247
|
};
|
|
183
248
|
};
|
|
@@ -188,6 +253,7 @@ async function main() {
|
|
|
188
253
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
189
254
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
190
255
|
.option('--parent <id>', 'Parent entry ID')
|
|
256
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
191
257
|
.option('--json', 'Output as JSON')
|
|
192
258
|
.action(createTypeShorthand('idea'));
|
|
193
259
|
|
|
@@ -197,6 +263,7 @@ async function main() {
|
|
|
197
263
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
198
264
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
199
265
|
.option('--parent <id>', 'Parent entry ID')
|
|
266
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
200
267
|
.option('--json', 'Output as JSON')
|
|
201
268
|
.action(createTypeShorthand('project'));
|
|
202
269
|
|
|
@@ -206,6 +273,7 @@ async function main() {
|
|
|
206
273
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
207
274
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
208
275
|
.option('--parent <id>', 'Parent entry ID')
|
|
276
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
209
277
|
.option('--json', 'Output as JSON')
|
|
210
278
|
.action(createTypeShorthand('feature'));
|
|
211
279
|
|
|
@@ -215,6 +283,7 @@ async function main() {
|
|
|
215
283
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
216
284
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
217
285
|
.option('--parent <id>', 'Parent entry ID')
|
|
286
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
218
287
|
.option('--json', 'Output as JSON')
|
|
219
288
|
.action(createTypeShorthand('note'));
|
|
220
289
|
|
|
@@ -224,6 +293,7 @@ async function main() {
|
|
|
224
293
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
225
294
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
226
295
|
.option('--parent <id>', 'Parent entry ID')
|
|
296
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
227
297
|
.option('--json', 'Output as JSON')
|
|
228
298
|
.action(createTypeShorthand('reference', 'reference'));
|
|
229
299
|
|
|
@@ -246,11 +316,16 @@ async function main() {
|
|
|
246
316
|
.option('--since <duration>', 'Created after (e.g., 7d, 24h)')
|
|
247
317
|
.option('--before <duration>', 'Created before (e.g., 7d, 24h)')
|
|
248
318
|
.option('--between <range>', 'Date range: start,end (e.g., 2025-01-01,2025-01-31)')
|
|
319
|
+
.option('--due-before <date>', 'Due before date (YYYY-MM-DD or 3d/1w)')
|
|
320
|
+
.option('--due-after <date>', 'Due after date (YYYY-MM-DD or 3d/1w)')
|
|
321
|
+
.option('--overdue', 'Only overdue entries')
|
|
322
|
+
.option('--has-due', 'Only entries with due dates')
|
|
323
|
+
.option('--no-due', 'Only entries without due dates')
|
|
249
324
|
.option('-a, --all', 'Include all statuses except archived')
|
|
250
325
|
.option('--done', 'Include done entries')
|
|
251
326
|
.option('--archived', 'Show only archived entries')
|
|
252
327
|
.option('-n, --limit <n>', 'Max entries to return', '50')
|
|
253
|
-
.option('--sort <field>', 'Sort by: created, updated, priority', 'created')
|
|
328
|
+
.option('--sort <field>', 'Sort by: created, updated, priority, due', 'created')
|
|
254
329
|
.option('--reverse', 'Reverse sort order')
|
|
255
330
|
.option('--json', 'Output as JSON')
|
|
256
331
|
.action(async (options) => {
|
|
@@ -271,6 +346,10 @@ async function main() {
|
|
|
271
346
|
const [start, end] = options.between.split(',');
|
|
272
347
|
return { start: start.trim(), end: end.trim() };
|
|
273
348
|
})() : undefined,
|
|
349
|
+
dueBefore: options.dueBefore,
|
|
350
|
+
dueAfter: options.dueAfter,
|
|
351
|
+
overdue: options.overdue,
|
|
352
|
+
hasDue: options.hasDue ? true : (options.due === false ? false : undefined),
|
|
274
353
|
includeDone: options.done || options.all,
|
|
275
354
|
limit: parseInt(options.limit, 10),
|
|
276
355
|
sort: options.sort,
|
|
@@ -314,7 +393,13 @@ async function main() {
|
|
|
314
393
|
const title = entry.title || entry.content.slice(0, 40);
|
|
315
394
|
const truncated = title.length > 40 ? '...' : '';
|
|
316
395
|
const timeStr = chalk.gray(formatTime(entry.createdAt));
|
|
317
|
-
|
|
396
|
+
let dueStr = '';
|
|
397
|
+
if (entry.due) {
|
|
398
|
+
const isOverdue = new Date(entry.due) < new Date() && entry.status !== 'done';
|
|
399
|
+
const dueText = formatDueDate(entry.due);
|
|
400
|
+
dueStr = isOverdue ? chalk.red(` [OVERDUE: ${dueText}]`) : chalk.magenta(` [due: ${dueText}]`);
|
|
401
|
+
}
|
|
402
|
+
console.log(` ${chalk.blue(shortId)} ${priorityBadge} ${title}${truncated}${tags}${dueStr} ${timeStr}`);
|
|
318
403
|
}
|
|
319
404
|
console.log('');
|
|
320
405
|
}
|
|
@@ -374,6 +459,12 @@ async function main() {
|
|
|
374
459
|
|
|
375
460
|
console.log(` ${chalk.gray('Created:')} ${formatTime(entry.createdAt)}`);
|
|
376
461
|
console.log(` ${chalk.gray('Updated:')} ${formatTime(entry.updatedAt)}`);
|
|
462
|
+
if (entry.due) {
|
|
463
|
+
const isOverdue = new Date(entry.due) < new Date() && entry.status !== 'done';
|
|
464
|
+
const dueColor = isOverdue ? chalk.red : chalk.magenta;
|
|
465
|
+
const overdueLabel = isOverdue ? ' (OVERDUE)' : '';
|
|
466
|
+
console.log(` ${chalk.gray('Due:')} ${dueColor(formatDueDate(entry.due))}${overdueLabel}`);
|
|
467
|
+
}
|
|
377
468
|
if (entry.source) {
|
|
378
469
|
console.log(` ${chalk.gray('Source:')} ${entry.source}`);
|
|
379
470
|
}
|
|
@@ -517,6 +608,8 @@ async function main() {
|
|
|
517
608
|
.option('--remove-tags <tags>', 'Remove tags')
|
|
518
609
|
.option('--parent <id>', 'Set parent')
|
|
519
610
|
.option('--clear-parent', 'Remove parent')
|
|
611
|
+
.option('--due <date>', 'Set due date')
|
|
612
|
+
.option('--clear-due', 'Remove due date')
|
|
520
613
|
.option('--json', 'Output as JSON')
|
|
521
614
|
.action(async (id, options) => {
|
|
522
615
|
const entry = await storage.getEntry(id);
|
|
@@ -546,6 +639,19 @@ async function main() {
|
|
|
546
639
|
}
|
|
547
640
|
if (options.parent) updates.parent = options.parent;
|
|
548
641
|
if (options.clearParent) updates.parent = null;
|
|
642
|
+
if (options.due) {
|
|
643
|
+
const dueDate = storage.parseDate(options.due);
|
|
644
|
+
if (!dueDate) {
|
|
645
|
+
if (options.json) {
|
|
646
|
+
console.log(JSON.stringify({ success: false, error: `Invalid due date: ${options.due}`, code: 'INVALID_DUE_DATE' }));
|
|
647
|
+
} else {
|
|
648
|
+
console.error(chalk.red(`Invalid due date: ${options.due}`));
|
|
649
|
+
}
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
updates.due = dueDate;
|
|
653
|
+
}
|
|
654
|
+
if (options.clearDue) updates.due = null;
|
|
549
655
|
|
|
550
656
|
const updated = await storage.updateEntry(entry.id, updates);
|
|
551
657
|
|
|
@@ -1043,7 +1149,7 @@ async function main() {
|
|
|
1043
1149
|
|
|
1044
1150
|
program
|
|
1045
1151
|
.command('focus')
|
|
1046
|
-
.description('Show what to work on now: P1 todos + active projects')
|
|
1152
|
+
.description('Show what to work on now: P1 todos + overdue + active projects')
|
|
1047
1153
|
.option('--json', 'Output as JSON')
|
|
1048
1154
|
.action(async (options) => {
|
|
1049
1155
|
// Get P1 todos
|
|
@@ -1054,6 +1160,17 @@ async function main() {
|
|
|
1054
1160
|
limit: 100
|
|
1055
1161
|
});
|
|
1056
1162
|
|
|
1163
|
+
// Get overdue entries
|
|
1164
|
+
const overdueEntries = await storage.listEntries({
|
|
1165
|
+
overdue: true,
|
|
1166
|
+
status: 'raw,active',
|
|
1167
|
+
limit: 100
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// Deduplicate (some P1 todos may also be overdue)
|
|
1171
|
+
const p1TodoIds = new Set(p1Todos.entries.map(e => e.id));
|
|
1172
|
+
const overdueNotP1 = overdueEntries.entries.filter(e => !p1TodoIds.has(e.id));
|
|
1173
|
+
|
|
1057
1174
|
// Get active projects with progress
|
|
1058
1175
|
const projects = await storage.listEntries({
|
|
1059
1176
|
type: 'project',
|
|
@@ -1075,18 +1192,32 @@ async function main() {
|
|
|
1075
1192
|
console.log(JSON.stringify({
|
|
1076
1193
|
success: true,
|
|
1077
1194
|
p1Todos: p1Todos.entries,
|
|
1195
|
+
overdueItems: overdueNotP1,
|
|
1078
1196
|
activeProjects: projectsWithProgress
|
|
1079
1197
|
}, null, 2));
|
|
1080
1198
|
} else {
|
|
1081
1199
|
console.log(chalk.bold('Focus: What to work on now\n'));
|
|
1082
1200
|
|
|
1201
|
+
// Overdue section (show first - most urgent)
|
|
1202
|
+
if (overdueNotP1.length > 0) {
|
|
1203
|
+
console.log(chalk.red.bold('Overdue:'));
|
|
1204
|
+
for (const item of overdueNotP1) {
|
|
1205
|
+
const shortId = item.id.slice(0, 8);
|
|
1206
|
+
const title = item.title || item.content.slice(0, 50);
|
|
1207
|
+
const dueStr = formatDueDate(item.due);
|
|
1208
|
+
console.log(` ${chalk.blue(shortId)} ${title} ${chalk.red(`[${dueStr}]`)}`);
|
|
1209
|
+
}
|
|
1210
|
+
console.log();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1083
1213
|
// P1 Todos
|
|
1084
1214
|
if (p1Todos.entries.length > 0) {
|
|
1085
1215
|
console.log(chalk.yellow.bold('P1 Todos:'));
|
|
1086
1216
|
for (const todo of p1Todos.entries) {
|
|
1087
1217
|
const shortId = todo.id.slice(0, 8);
|
|
1088
1218
|
const title = todo.title || todo.content.slice(0, 50);
|
|
1089
|
-
|
|
1219
|
+
const dueStr = todo.due ? chalk.magenta(` [due: ${formatDueDate(todo.due)}]`) : '';
|
|
1220
|
+
console.log(` ${chalk.blue(shortId)} ${title}${dueStr}`);
|
|
1090
1221
|
}
|
|
1091
1222
|
console.log();
|
|
1092
1223
|
} else {
|
package/src/storage.js
CHANGED
|
@@ -201,6 +201,89 @@ function parseDuration(duration) {
|
|
|
201
201
|
return value * multipliers[unit];
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Parse date input to ISO string
|
|
206
|
+
* Supports: ISO dates ("2025-01-15"), relative ("3d", "1w"), natural language ("tomorrow", "next Monday")
|
|
207
|
+
* @param {string} input - Date input string
|
|
208
|
+
* @returns {string|null} - ISO date string or null if invalid
|
|
209
|
+
*/
|
|
210
|
+
function parseDate(input) {
|
|
211
|
+
if (typeof input !== 'string') return null;
|
|
212
|
+
const trimmed = input.trim();
|
|
213
|
+
if (!trimmed) return null;
|
|
214
|
+
|
|
215
|
+
const lowered = trimmed.toLowerCase();
|
|
216
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
217
|
+
|
|
218
|
+
const endOfDay = (date) => {
|
|
219
|
+
const next = new Date(date);
|
|
220
|
+
next.setHours(23, 59, 59, 999);
|
|
221
|
+
return next;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (lowered === 'today') {
|
|
225
|
+
return endOfDay(new Date()).toISOString();
|
|
226
|
+
}
|
|
227
|
+
if (lowered === 'tomorrow') {
|
|
228
|
+
return endOfDay(new Date(Date.now() + dayMs)).toISOString();
|
|
229
|
+
}
|
|
230
|
+
if (lowered === 'yesterday') {
|
|
231
|
+
return endOfDay(new Date(Date.now() - dayMs)).toISOString();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const nextWeekdayMatch = lowered.match(/^next\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)$/);
|
|
235
|
+
if (nextWeekdayMatch) {
|
|
236
|
+
const weekdays = {
|
|
237
|
+
sunday: 0,
|
|
238
|
+
monday: 1,
|
|
239
|
+
tuesday: 2,
|
|
240
|
+
wednesday: 3,
|
|
241
|
+
thursday: 4,
|
|
242
|
+
friday: 5,
|
|
243
|
+
saturday: 6
|
|
244
|
+
};
|
|
245
|
+
const target = weekdays[nextWeekdayMatch[1]];
|
|
246
|
+
const now = new Date();
|
|
247
|
+
const current = now.getDay();
|
|
248
|
+
let diff = (target + 7 - current) % 7;
|
|
249
|
+
if (diff === 0) diff = 7;
|
|
250
|
+
return endOfDay(new Date(now.getTime() + diff * dayMs)).toISOString();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const inMatch = lowered.match(/^in\s+(\d+)\s*(hour|hours|day|days|week|weeks|month|months)$/);
|
|
254
|
+
if (inMatch) {
|
|
255
|
+
const value = parseInt(inMatch[1], 10);
|
|
256
|
+
const unit = inMatch[2];
|
|
257
|
+
const multipliers = {
|
|
258
|
+
hour: 60 * 60 * 1000,
|
|
259
|
+
hours: 60 * 60 * 1000,
|
|
260
|
+
day: dayMs,
|
|
261
|
+
days: dayMs,
|
|
262
|
+
week: 7 * dayMs,
|
|
263
|
+
weeks: 7 * dayMs,
|
|
264
|
+
month: 30 * dayMs,
|
|
265
|
+
months: 30 * dayMs
|
|
266
|
+
};
|
|
267
|
+
return new Date(Date.now() + value * multipliers[unit]).toISOString();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Try relative duration first (3d, 1w, etc.) - for FUTURE dates
|
|
271
|
+
const durationMs = parseDuration(trimmed);
|
|
272
|
+
if (durationMs !== null) {
|
|
273
|
+
return new Date(Date.now() + durationMs).toISOString();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Try ISO date (YYYY-MM-DD)
|
|
277
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
278
|
+
const date = new Date(trimmed + 'T23:59:59');
|
|
279
|
+
if (!isNaN(date.getTime())) {
|
|
280
|
+
return date.toISOString();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
204
287
|
/**
|
|
205
288
|
* Add a new entry
|
|
206
289
|
*/
|
|
@@ -216,6 +299,16 @@ async function addEntry(options) {
|
|
|
216
299
|
}
|
|
217
300
|
}
|
|
218
301
|
|
|
302
|
+
// Parse due date if provided
|
|
303
|
+
let dueDate = undefined;
|
|
304
|
+
const dueInput = typeof options.due === 'string' ? options.due.trim() : options.due;
|
|
305
|
+
if (dueInput) {
|
|
306
|
+
dueDate = parseDate(dueInput);
|
|
307
|
+
if (!dueDate) {
|
|
308
|
+
throw new Error(`Invalid due date: ${options.due}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
219
312
|
const now = new Date().toISOString();
|
|
220
313
|
const entry = {
|
|
221
314
|
id: uuidv4(),
|
|
@@ -227,6 +320,7 @@ async function addEntry(options) {
|
|
|
227
320
|
tags: options.tags || [],
|
|
228
321
|
parent: parentId || undefined,
|
|
229
322
|
related: [],
|
|
323
|
+
due: dueDate,
|
|
230
324
|
createdAt: now,
|
|
231
325
|
updatedAt: now,
|
|
232
326
|
source: options.source || 'cli'
|
|
@@ -392,6 +486,40 @@ async function listEntries(query = {}) {
|
|
|
392
486
|
});
|
|
393
487
|
}
|
|
394
488
|
|
|
489
|
+
// Filter by due before
|
|
490
|
+
if (query.dueBefore) {
|
|
491
|
+
const cutoffDate = parseDate(query.dueBefore);
|
|
492
|
+
if (cutoffDate) {
|
|
493
|
+
entries = entries.filter(e => e.due && new Date(e.due) <= new Date(cutoffDate));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Filter by due after
|
|
498
|
+
if (query.dueAfter) {
|
|
499
|
+
const cutoffDate = parseDate(query.dueAfter);
|
|
500
|
+
if (cutoffDate) {
|
|
501
|
+
entries = entries.filter(e => e.due && new Date(e.due) >= new Date(cutoffDate));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Filter overdue (due before now, not done/archived)
|
|
506
|
+
if (query.overdue) {
|
|
507
|
+
const now = new Date();
|
|
508
|
+
entries = entries.filter(e =>
|
|
509
|
+
e.due &&
|
|
510
|
+
new Date(e.due) < now &&
|
|
511
|
+
e.status !== 'done' &&
|
|
512
|
+
e.status !== 'archived'
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Filter entries with/without due date
|
|
517
|
+
if (query.hasDue === true) {
|
|
518
|
+
entries = entries.filter(e => e.due);
|
|
519
|
+
} else if (query.hasDue === false) {
|
|
520
|
+
entries = entries.filter(e => !e.due);
|
|
521
|
+
}
|
|
522
|
+
|
|
395
523
|
// Include done if requested
|
|
396
524
|
if (!query.includeDone && query.status !== 'done') {
|
|
397
525
|
entries = entries.filter(e => e.status !== 'done');
|
|
@@ -400,6 +528,13 @@ async function listEntries(query = {}) {
|
|
|
400
528
|
// Sort
|
|
401
529
|
const sortField = query.sort || 'created';
|
|
402
530
|
entries.sort((a, b) => {
|
|
531
|
+
if (sortField === 'due') {
|
|
532
|
+
// Entries without due dates go to the end
|
|
533
|
+
if (!a.due && !b.due) return 0;
|
|
534
|
+
if (!a.due) return 1;
|
|
535
|
+
if (!b.due) return -1;
|
|
536
|
+
return new Date(a.due) - new Date(b.due);
|
|
537
|
+
}
|
|
403
538
|
if (sortField === 'priority') {
|
|
404
539
|
const pA = a.priority || 99;
|
|
405
540
|
const pB = b.priority || 99;
|
|
@@ -518,6 +653,7 @@ async function updateEntry(id, updates) {
|
|
|
518
653
|
// Handle null values (clear fields)
|
|
519
654
|
if (updates.priority === null) delete entry.priority;
|
|
520
655
|
if (updates.parent === null) delete entry.parent;
|
|
656
|
+
if (updates.due === null) delete entry.due;
|
|
521
657
|
|
|
522
658
|
saveEntries(data);
|
|
523
659
|
|
|
@@ -794,6 +930,7 @@ module.exports = {
|
|
|
794
930
|
getDefaultConfig,
|
|
795
931
|
validateConfigValue,
|
|
796
932
|
saveConfig,
|
|
933
|
+
parseDate,
|
|
797
934
|
CONFIG_DIR,
|
|
798
935
|
ENTRIES_FILE,
|
|
799
936
|
ARCHIVE_FILE,
|