synap 0.1.1 → 0.4.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 +65 -4
- package/package.json +1 -1
- package/src/cli.js +384 -61
- package/src/storage.js +159 -1
|
@@ -51,7 +51,7 @@ Detect user intent and respond appropriately:
|
|
|
51
51
|
| **Capture** | "Add this...", "Remind me...", "I had an idea..." | Fast capture, minimal questions, default to idea type |
|
|
52
52
|
| **Review** | "What's on my plate?", "Daily review", "Show me..." | Stats + prioritized summary, grouped by type |
|
|
53
53
|
| **Triage** | "Process my synap", "Process my brain dump", "What needs attention?" | Surface raw entries, help classify and prioritize |
|
|
54
|
-
| **Focus** | "What should I work on?", "Priority items" | P1 todos + active projects, clear next actions |
|
|
54
|
+
| **Focus** | "What should I work on?", "Priority items" | WIP items + P1 todos + active projects, clear next actions |
|
|
55
55
|
| **Cleanup** | "Archive completed", "Clean up old stuff" | Bulk operations with preview and confirmation |
|
|
56
56
|
|
|
57
57
|
### Volume Modes (Quick vs Deep)
|
|
@@ -67,12 +67,15 @@ 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` |
|
|
73
74
|
| Search | `synap search "keyword"` |
|
|
74
75
|
| Show details | `synap show <id>` |
|
|
75
76
|
| Mark done | `synap done <id>` |
|
|
77
|
+
| Start working | `synap start <id>` |
|
|
78
|
+
| Stop working | `synap stop <id>` |
|
|
76
79
|
| Get stats | `synap stats` |
|
|
77
80
|
| Setup wizard | `synap setup` |
|
|
78
81
|
| Edit preferences | `synap preferences --edit` |
|
|
@@ -98,6 +101,7 @@ Quick capture of a thought.
|
|
|
98
101
|
```bash
|
|
99
102
|
synap add "What if we used a graph database?"
|
|
100
103
|
synap add "Need to review the API design" --type todo --priority 1
|
|
104
|
+
synap add "Prep for demo" --type todo --due 2025-02-15
|
|
101
105
|
synap add "Meeting notes from standup" --type note --tags "meetings,weekly"
|
|
102
106
|
synap add --type project --title "Website Redesign" "Complete overhaul of the marketing site..."
|
|
103
107
|
```
|
|
@@ -108,6 +112,7 @@ synap add --type project --title "Website Redesign" "Complete overhaul of the ma
|
|
|
108
112
|
- `--priority <1|2|3>`: 1=high, 2=medium, 3=low
|
|
109
113
|
- `--tags <tags>`: Comma-separated tags
|
|
110
114
|
- `--parent <id>`: Parent entry ID
|
|
115
|
+
- `--due <date>`: Due date (YYYY-MM-DD, 3d/1w, weekday names: monday/friday, or keywords: today, tomorrow, next monday)
|
|
111
116
|
- `--json`: JSON output
|
|
112
117
|
|
|
113
118
|
#### `synap todo <content>`
|
|
@@ -118,6 +123,8 @@ synap todo "Review PR #42"
|
|
|
118
123
|
# Equivalent to: synap add "Review PR #42" --type todo
|
|
119
124
|
```
|
|
120
125
|
|
|
126
|
+
Options: `--priority`, `--tags`, `--parent`, `--due`, `--json`
|
|
127
|
+
|
|
121
128
|
#### `synap question <content>`
|
|
122
129
|
Shorthand for adding a question.
|
|
123
130
|
|
|
@@ -126,6 +133,20 @@ synap question "Should we migrate to TypeScript?"
|
|
|
126
133
|
# Equivalent to: synap add "..." --type question
|
|
127
134
|
```
|
|
128
135
|
|
|
136
|
+
Options: `--priority`, `--tags`, `--parent`, `--due`, `--json`
|
|
137
|
+
|
|
138
|
+
#### `synap log <id> <message>`
|
|
139
|
+
Add a timestamped log entry under a parent entry.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
synap log a1b2c3d4 "Started implementation"
|
|
143
|
+
synap log a1b2c3d4 "Completed first draft" --inherit-tags
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Options**:
|
|
147
|
+
- `--inherit-tags`: Copy tags from parent entry
|
|
148
|
+
- `--json`: JSON output
|
|
149
|
+
|
|
129
150
|
### Query Commands
|
|
130
151
|
|
|
131
152
|
#### `synap list`
|
|
@@ -139,22 +160,30 @@ synap list --status raw # Needs triage
|
|
|
139
160
|
synap list --priority 1 # High priority only
|
|
140
161
|
synap list --tags work,urgent # Has ALL specified tags
|
|
141
162
|
synap list --since 7d # Created in last 7 days
|
|
163
|
+
synap list --overdue # Overdue entries
|
|
164
|
+
synap list --due-before 7d # Due in next 7 days
|
|
165
|
+
synap list --has-due # Entries with due dates
|
|
142
166
|
synap list --json # JSON output for parsing
|
|
143
167
|
```
|
|
144
168
|
|
|
145
169
|
**Options**:
|
|
146
170
|
- `--type <type>`: Filter by entry type
|
|
147
|
-
- `--status <status>`: raw, active, someday, done, archived (default: raw,active)
|
|
171
|
+
- `--status <status>`: raw, active, wip, someday, done, archived (default: raw,active)
|
|
148
172
|
- `--tags <tags>`: Comma-separated, AND logic
|
|
149
173
|
- `--priority <1|2|3>`: Filter by priority
|
|
150
174
|
- `--parent <id>`: Children of specific entry
|
|
151
175
|
- `--orphans`: Only entries without parent
|
|
152
176
|
- `--since <duration>`: e.g., 7d, 24h, 2w
|
|
177
|
+
- `--due-before <date>`: Due before date (YYYY-MM-DD or 3d/1w)
|
|
178
|
+
- `--due-after <date>`: Due after date (YYYY-MM-DD or 3d/1w)
|
|
179
|
+
- `--overdue`: Only overdue entries
|
|
180
|
+
- `--has-due`: Only entries with due dates
|
|
181
|
+
- `--no-due`: Only entries without due dates
|
|
153
182
|
- `--all`: All statuses except archived
|
|
154
183
|
- `--done`: Include done entries
|
|
155
184
|
- `--archived`: Show only archived
|
|
156
185
|
- `--limit <n>`: Max entries (default: 50)
|
|
157
|
-
- `--sort <field>`: created, updated, priority
|
|
186
|
+
- `--sort <field>`: created, updated, priority, due
|
|
158
187
|
- `--reverse`: Reverse sort order
|
|
159
188
|
- `--json`: JSON output
|
|
160
189
|
|
|
@@ -224,6 +253,36 @@ synap done --type todo --tags "sprint-1" # By filter
|
|
|
224
253
|
synap done --dry-run --type todo # Preview first
|
|
225
254
|
```
|
|
226
255
|
|
|
256
|
+
#### `synap start <ids...>`
|
|
257
|
+
Start working on entries (mark as WIP).
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
synap start a1b2c3d4 # Single entry
|
|
261
|
+
synap start a1b2c3d4 b2c3d4e5 # Multiple
|
|
262
|
+
synap start --type todo --tags urgent # By filter
|
|
263
|
+
synap start --dry-run --type todo # Preview first
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Options**:
|
|
267
|
+
- `-t, --type <type>`: Filter by type
|
|
268
|
+
- `--tags <tags>`: Filter by tags
|
|
269
|
+
- `--dry-run`: Show what would be started
|
|
270
|
+
- `--json`: JSON output
|
|
271
|
+
|
|
272
|
+
#### `synap stop <ids...>`
|
|
273
|
+
Stop working on entries (remove WIP status).
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
synap stop a1b2c3d4 # Single entry
|
|
277
|
+
synap stop --all # Stop all WIP entries
|
|
278
|
+
synap stop --dry-run # Preview first
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Options**:
|
|
282
|
+
- `--all`: Stop all WIP entries
|
|
283
|
+
- `--dry-run`: Show what would be stopped
|
|
284
|
+
- `--json`: JSON output
|
|
285
|
+
|
|
227
286
|
#### `synap archive <ids...>`
|
|
228
287
|
Archive entries (hides from default view).
|
|
229
288
|
|
|
@@ -432,11 +491,13 @@ Critical for preventing accidental mass changes:
|
|
|
432
491
|
"content": "The full text of the entry",
|
|
433
492
|
"title": "Short title (optional)",
|
|
434
493
|
"type": "idea|project|feature|todo|question|reference|note",
|
|
435
|
-
"status": "raw|active|someday|done|archived",
|
|
494
|
+
"status": "raw|active|wip|someday|done|archived",
|
|
436
495
|
"priority": 1|2|3|null,
|
|
437
496
|
"tags": ["tag1", "tag2"],
|
|
438
497
|
"parent": "parent-id|null",
|
|
439
498
|
"related": ["id1", "id2"],
|
|
499
|
+
"due": "2026-01-10T23:59:59.000Z",
|
|
500
|
+
"startedAt": "2026-01-10T08:30:00.000Z",
|
|
440
501
|
"createdAt": "2026-01-05T08:30:00.000Z",
|
|
441
502
|
"updatedAt": "2026-01-05T08:30:00.000Z",
|
|
442
503
|
"source": "cli|agent|import"
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -63,6 +63,35 @@ 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
|
+
|
|
84
|
+
const formatDuration = (ms) => {
|
|
85
|
+
const minutes = Math.floor(ms / (1000 * 60));
|
|
86
|
+
const hours = Math.floor(minutes / 60);
|
|
87
|
+
const days = Math.floor(hours / 24);
|
|
88
|
+
|
|
89
|
+
if (days > 0) return `(${days}d ${hours % 24}h)`;
|
|
90
|
+
if (hours > 0) return `(${hours}h ${minutes % 60}m)`;
|
|
91
|
+
if (minutes > 0) return `(${minutes}m)`;
|
|
92
|
+
return '(<1m)';
|
|
93
|
+
};
|
|
94
|
+
|
|
66
95
|
// ============================================
|
|
67
96
|
// CAPTURE COMMANDS
|
|
68
97
|
// ============================================
|
|
@@ -75,27 +104,39 @@ async function main() {
|
|
|
75
104
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
76
105
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
77
106
|
.option('--parent <id>', 'Parent entry ID')
|
|
107
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
78
108
|
.option('--json', 'Output as JSON')
|
|
79
109
|
.action(async (contentParts, options) => {
|
|
80
110
|
const content = contentParts.join(' ');
|
|
81
111
|
const tags = mergeTags(options.tags);
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
try {
|
|
114
|
+
const entry = await storage.addEntry({
|
|
115
|
+
content,
|
|
116
|
+
title: options.title,
|
|
117
|
+
type: options.type,
|
|
118
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
119
|
+
tags,
|
|
120
|
+
parent: options.parent,
|
|
121
|
+
due: options.due,
|
|
122
|
+
source: 'cli'
|
|
123
|
+
});
|
|
92
124
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
if (options.json) {
|
|
126
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
127
|
+
} else {
|
|
128
|
+
const shortId = entry.id.slice(0, 8);
|
|
129
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
130
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
131
|
+
console.log(chalk.green(`Added ${entry.type} ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (options.json) {
|
|
135
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
136
|
+
} else {
|
|
137
|
+
console.error(chalk.red(err.message));
|
|
138
|
+
}
|
|
139
|
+
process.exit(1);
|
|
99
140
|
}
|
|
100
141
|
});
|
|
101
142
|
|
|
@@ -105,26 +146,38 @@ async function main() {
|
|
|
105
146
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
106
147
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
107
148
|
.option('--parent <id>', 'Parent entry ID')
|
|
149
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
108
150
|
.option('--json', 'Output as JSON')
|
|
109
151
|
.action(async (contentParts, options) => {
|
|
110
152
|
const content = contentParts.join(' ');
|
|
111
153
|
const tags = mergeTags(options.tags);
|
|
112
154
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
try {
|
|
156
|
+
const entry = await storage.addEntry({
|
|
157
|
+
content,
|
|
158
|
+
type: 'todo',
|
|
159
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
160
|
+
tags,
|
|
161
|
+
parent: options.parent,
|
|
162
|
+
due: options.due,
|
|
163
|
+
source: 'cli'
|
|
164
|
+
});
|
|
121
165
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
166
|
+
if (options.json) {
|
|
167
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
168
|
+
} else {
|
|
169
|
+
const shortId = entry.id.slice(0, 8);
|
|
170
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
171
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
172
|
+
console.log(chalk.green(`Added todo ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (options.json) {
|
|
176
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
177
|
+
} else {
|
|
178
|
+
console.error(chalk.red(err.message));
|
|
179
|
+
}
|
|
180
|
+
process.exit(1);
|
|
128
181
|
}
|
|
129
182
|
});
|
|
130
183
|
|
|
@@ -134,26 +187,38 @@ async function main() {
|
|
|
134
187
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
135
188
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
136
189
|
.option('--parent <id>', 'Parent entry ID')
|
|
190
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
137
191
|
.option('--json', 'Output as JSON')
|
|
138
192
|
.action(async (contentParts, options) => {
|
|
139
193
|
const content = contentParts.join(' ');
|
|
140
194
|
const tags = mergeTags(options.tags);
|
|
141
195
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
196
|
+
try {
|
|
197
|
+
const entry = await storage.addEntry({
|
|
198
|
+
content,
|
|
199
|
+
type: 'question',
|
|
200
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
201
|
+
tags,
|
|
202
|
+
parent: options.parent,
|
|
203
|
+
due: options.due,
|
|
204
|
+
source: 'cli'
|
|
205
|
+
});
|
|
150
206
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
207
|
+
if (options.json) {
|
|
208
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
209
|
+
} else {
|
|
210
|
+
const shortId = entry.id.slice(0, 8);
|
|
211
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
212
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
213
|
+
console.log(chalk.green(`Added question ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (options.json) {
|
|
217
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
218
|
+
} else {
|
|
219
|
+
console.error(chalk.red(err.message));
|
|
220
|
+
}
|
|
221
|
+
process.exit(1);
|
|
157
222
|
}
|
|
158
223
|
});
|
|
159
224
|
|
|
@@ -163,21 +228,32 @@ async function main() {
|
|
|
163
228
|
const content = contentParts.join(' ');
|
|
164
229
|
const tags = mergeTags(options.tags);
|
|
165
230
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
231
|
+
try {
|
|
232
|
+
const entry = await storage.addEntry({
|
|
233
|
+
content,
|
|
234
|
+
type: typeName,
|
|
235
|
+
priority: options.priority ? parseInt(options.priority, 10) : undefined,
|
|
236
|
+
tags,
|
|
237
|
+
parent: options.parent,
|
|
238
|
+
due: options.due,
|
|
239
|
+
source: 'cli'
|
|
240
|
+
});
|
|
174
241
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
if (options.json) {
|
|
243
|
+
console.log(JSON.stringify({ success: true, entry }, null, 2));
|
|
244
|
+
} else {
|
|
245
|
+
const shortId = entry.id.slice(0, 8);
|
|
246
|
+
const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
|
|
247
|
+
const dueBadge = entry.due ? chalk.magenta(` [due: ${formatDueDate(entry.due)}]`) : '';
|
|
248
|
+
console.log(chalk.green(`Added ${displayName} ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"` + dueBadge);
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (options.json) {
|
|
252
|
+
console.log(JSON.stringify({ success: false, error: err.message, code: 'INVALID_DUE_DATE' }));
|
|
253
|
+
} else {
|
|
254
|
+
console.error(chalk.red(err.message));
|
|
255
|
+
}
|
|
256
|
+
process.exit(1);
|
|
181
257
|
}
|
|
182
258
|
};
|
|
183
259
|
};
|
|
@@ -188,6 +264,7 @@ async function main() {
|
|
|
188
264
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
189
265
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
190
266
|
.option('--parent <id>', 'Parent entry ID')
|
|
267
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
191
268
|
.option('--json', 'Output as JSON')
|
|
192
269
|
.action(createTypeShorthand('idea'));
|
|
193
270
|
|
|
@@ -197,6 +274,7 @@ async function main() {
|
|
|
197
274
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
198
275
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
199
276
|
.option('--parent <id>', 'Parent entry ID')
|
|
277
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
200
278
|
.option('--json', 'Output as JSON')
|
|
201
279
|
.action(createTypeShorthand('project'));
|
|
202
280
|
|
|
@@ -206,6 +284,7 @@ async function main() {
|
|
|
206
284
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
207
285
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
208
286
|
.option('--parent <id>', 'Parent entry ID')
|
|
287
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
209
288
|
.option('--json', 'Output as JSON')
|
|
210
289
|
.action(createTypeShorthand('feature'));
|
|
211
290
|
|
|
@@ -215,6 +294,7 @@ async function main() {
|
|
|
215
294
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
216
295
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
217
296
|
.option('--parent <id>', 'Parent entry ID')
|
|
297
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
218
298
|
.option('--json', 'Output as JSON')
|
|
219
299
|
.action(createTypeShorthand('note'));
|
|
220
300
|
|
|
@@ -224,9 +304,71 @@ async function main() {
|
|
|
224
304
|
.option('-p, --priority <priority>', 'Priority level (1=high, 2=medium, 3=low)')
|
|
225
305
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
226
306
|
.option('--parent <id>', 'Parent entry ID')
|
|
307
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)')
|
|
227
308
|
.option('--json', 'Output as JSON')
|
|
228
309
|
.action(createTypeShorthand('reference', 'reference'));
|
|
229
310
|
|
|
311
|
+
program
|
|
312
|
+
.command('log <id> <message...>')
|
|
313
|
+
.description('Add a timestamped log entry under a parent')
|
|
314
|
+
.option('--inherit-tags', 'Copy tags from parent entry')
|
|
315
|
+
.option('--json', 'Output as JSON')
|
|
316
|
+
.action(async (id, messageParts, options) => {
|
|
317
|
+
const message = messageParts.join(' ');
|
|
318
|
+
|
|
319
|
+
const parent = await storage.getEntry(id);
|
|
320
|
+
if (!parent) {
|
|
321
|
+
if (options.json) {
|
|
322
|
+
console.log(JSON.stringify({ success: false, error: `Parent entry not found: ${id}`, code: 'ENTRY_NOT_FOUND' }));
|
|
323
|
+
} else {
|
|
324
|
+
console.error(chalk.red(`Parent entry not found: ${id}`));
|
|
325
|
+
}
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const now = new Date();
|
|
330
|
+
const timestamp = now.toISOString().slice(0, 16).replace('T', ' ');
|
|
331
|
+
const content = `[${timestamp}] ${message}`;
|
|
332
|
+
const title = message.length <= 40 ? message : message.slice(0, 37) + '...';
|
|
333
|
+
|
|
334
|
+
let tags = [];
|
|
335
|
+
if (options.inheritTags && parent.tags && parent.tags.length > 0) {
|
|
336
|
+
tags = [...parent.tags];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const entry = await storage.addEntry({
|
|
341
|
+
content,
|
|
342
|
+
title,
|
|
343
|
+
type: 'note',
|
|
344
|
+
tags,
|
|
345
|
+
parent: parent.id,
|
|
346
|
+
source: 'cli'
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (options.json) {
|
|
350
|
+
console.log(JSON.stringify({
|
|
351
|
+
success: true,
|
|
352
|
+
entry,
|
|
353
|
+
parent: { id: parent.id, title: parent.title || parent.content.slice(0, 40) }
|
|
354
|
+
}, null, 2));
|
|
355
|
+
} else {
|
|
356
|
+
const shortId = entry.id.slice(0, 8);
|
|
357
|
+
const parentShortId = parent.id.slice(0, 8);
|
|
358
|
+
console.log(chalk.green(`Logged ${shortId} under ${parentShortId}:`));
|
|
359
|
+
console.log(` Parent: ${chalk.cyan(parent.title || parent.content.slice(0, 30))}`);
|
|
360
|
+
console.log(` Log: ${message.slice(0, 60)}${message.length > 60 ? '...' : ''}`);
|
|
361
|
+
}
|
|
362
|
+
} catch (err) {
|
|
363
|
+
if (options.json) {
|
|
364
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
365
|
+
} else {
|
|
366
|
+
console.error(chalk.red(err.message));
|
|
367
|
+
}
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
230
372
|
// ============================================
|
|
231
373
|
// QUERY COMMANDS
|
|
232
374
|
// ============================================
|
|
@@ -246,11 +388,16 @@ async function main() {
|
|
|
246
388
|
.option('--since <duration>', 'Created after (e.g., 7d, 24h)')
|
|
247
389
|
.option('--before <duration>', 'Created before (e.g., 7d, 24h)')
|
|
248
390
|
.option('--between <range>', 'Date range: start,end (e.g., 2025-01-01,2025-01-31)')
|
|
391
|
+
.option('--due-before <date>', 'Due before date (YYYY-MM-DD or 3d/1w)')
|
|
392
|
+
.option('--due-after <date>', 'Due after date (YYYY-MM-DD or 3d/1w)')
|
|
393
|
+
.option('--overdue', 'Only overdue entries')
|
|
394
|
+
.option('--has-due', 'Only entries with due dates')
|
|
395
|
+
.option('--no-due', 'Only entries without due dates')
|
|
249
396
|
.option('-a, --all', 'Include all statuses except archived')
|
|
250
397
|
.option('--done', 'Include done entries')
|
|
251
398
|
.option('--archived', 'Show only archived entries')
|
|
252
399
|
.option('-n, --limit <n>', 'Max entries to return', '50')
|
|
253
|
-
.option('--sort <field>', 'Sort by: created, updated, priority', 'created')
|
|
400
|
+
.option('--sort <field>', 'Sort by: created, updated, priority, due', 'created')
|
|
254
401
|
.option('--reverse', 'Reverse sort order')
|
|
255
402
|
.option('--json', 'Output as JSON')
|
|
256
403
|
.action(async (options) => {
|
|
@@ -271,6 +418,10 @@ async function main() {
|
|
|
271
418
|
const [start, end] = options.between.split(',');
|
|
272
419
|
return { start: start.trim(), end: end.trim() };
|
|
273
420
|
})() : undefined,
|
|
421
|
+
dueBefore: options.dueBefore,
|
|
422
|
+
dueAfter: options.dueAfter,
|
|
423
|
+
overdue: options.overdue,
|
|
424
|
+
hasDue: options.hasDue ? true : (options.due === false ? false : undefined),
|
|
274
425
|
includeDone: options.done || options.all,
|
|
275
426
|
limit: parseInt(options.limit, 10),
|
|
276
427
|
sort: options.sort,
|
|
@@ -314,7 +465,13 @@ async function main() {
|
|
|
314
465
|
const title = entry.title || entry.content.slice(0, 40);
|
|
315
466
|
const truncated = title.length > 40 ? '...' : '';
|
|
316
467
|
const timeStr = chalk.gray(formatTime(entry.createdAt));
|
|
317
|
-
|
|
468
|
+
let dueStr = '';
|
|
469
|
+
if (entry.due) {
|
|
470
|
+
const isOverdue = new Date(entry.due) < new Date() && entry.status !== 'done';
|
|
471
|
+
const dueText = formatDueDate(entry.due);
|
|
472
|
+
dueStr = isOverdue ? chalk.red(` [OVERDUE: ${dueText}]`) : chalk.magenta(` [due: ${dueText}]`);
|
|
473
|
+
}
|
|
474
|
+
console.log(` ${chalk.blue(shortId)} ${priorityBadge} ${title}${truncated}${tags}${dueStr} ${timeStr}`);
|
|
318
475
|
}
|
|
319
476
|
console.log('');
|
|
320
477
|
}
|
|
@@ -374,6 +531,12 @@ async function main() {
|
|
|
374
531
|
|
|
375
532
|
console.log(` ${chalk.gray('Created:')} ${formatTime(entry.createdAt)}`);
|
|
376
533
|
console.log(` ${chalk.gray('Updated:')} ${formatTime(entry.updatedAt)}`);
|
|
534
|
+
if (entry.due) {
|
|
535
|
+
const isOverdue = new Date(entry.due) < new Date() && entry.status !== 'done';
|
|
536
|
+
const dueColor = isOverdue ? chalk.red : chalk.magenta;
|
|
537
|
+
const overdueLabel = isOverdue ? ' (OVERDUE)' : '';
|
|
538
|
+
console.log(` ${chalk.gray('Due:')} ${dueColor(formatDueDate(entry.due))}${overdueLabel}`);
|
|
539
|
+
}
|
|
377
540
|
if (entry.source) {
|
|
378
541
|
console.log(` ${chalk.gray('Source:')} ${entry.source}`);
|
|
379
542
|
}
|
|
@@ -517,6 +680,8 @@ async function main() {
|
|
|
517
680
|
.option('--remove-tags <tags>', 'Remove tags')
|
|
518
681
|
.option('--parent <id>', 'Set parent')
|
|
519
682
|
.option('--clear-parent', 'Remove parent')
|
|
683
|
+
.option('--due <date>', 'Set due date')
|
|
684
|
+
.option('--clear-due', 'Remove due date')
|
|
520
685
|
.option('--json', 'Output as JSON')
|
|
521
686
|
.action(async (id, options) => {
|
|
522
687
|
const entry = await storage.getEntry(id);
|
|
@@ -546,6 +711,19 @@ async function main() {
|
|
|
546
711
|
}
|
|
547
712
|
if (options.parent) updates.parent = options.parent;
|
|
548
713
|
if (options.clearParent) updates.parent = null;
|
|
714
|
+
if (options.due) {
|
|
715
|
+
const dueDate = storage.parseDate(options.due);
|
|
716
|
+
if (!dueDate) {
|
|
717
|
+
if (options.json) {
|
|
718
|
+
console.log(JSON.stringify({ success: false, error: `Invalid due date: ${options.due}`, code: 'INVALID_DUE_DATE' }));
|
|
719
|
+
} else {
|
|
720
|
+
console.error(chalk.red(`Invalid due date: ${options.due}`));
|
|
721
|
+
}
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
updates.due = dueDate;
|
|
725
|
+
}
|
|
726
|
+
if (options.clearDue) updates.due = null;
|
|
549
727
|
|
|
550
728
|
const updated = await storage.updateEntry(entry.id, updates);
|
|
551
729
|
|
|
@@ -669,6 +847,126 @@ async function main() {
|
|
|
669
847
|
}
|
|
670
848
|
});
|
|
671
849
|
|
|
850
|
+
program
|
|
851
|
+
.command('start [ids...]')
|
|
852
|
+
.description('Start working on entries (mark as WIP)')
|
|
853
|
+
.option('-t, --type <type>', 'Filter by type')
|
|
854
|
+
.option('--tags <tags>', 'Filter by tags')
|
|
855
|
+
.option('--dry-run', 'Show what would be started')
|
|
856
|
+
.option('--json', 'Output as JSON')
|
|
857
|
+
.action(async (ids, options) => {
|
|
858
|
+
let entries;
|
|
859
|
+
|
|
860
|
+
if (ids.length > 0) {
|
|
861
|
+
entries = await storage.getEntriesByIds(ids);
|
|
862
|
+
} else if (options.type || options.tags) {
|
|
863
|
+
const result = await storage.listEntries({
|
|
864
|
+
type: options.type,
|
|
865
|
+
tags: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined,
|
|
866
|
+
status: 'raw,active',
|
|
867
|
+
limit: 1000
|
|
868
|
+
});
|
|
869
|
+
entries = result.entries;
|
|
870
|
+
} else {
|
|
871
|
+
console.error(chalk.red('Please provide entry IDs or filter options (--type, --tags)'));
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
entries = entries.filter(e => e.status !== 'wip');
|
|
876
|
+
|
|
877
|
+
if (entries.length === 0) {
|
|
878
|
+
if (options.json) {
|
|
879
|
+
console.log(JSON.stringify({ success: true, count: 0, entries: [] }));
|
|
880
|
+
} else {
|
|
881
|
+
console.log(chalk.gray('No entries to start (already WIP or not found)'));
|
|
882
|
+
}
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (options.dryRun) {
|
|
887
|
+
if (options.json) {
|
|
888
|
+
console.log(JSON.stringify({ success: true, dryRun: true, count: entries.length, entries }));
|
|
889
|
+
} else {
|
|
890
|
+
console.log(chalk.yellow(`Would start ${entries.length} entries:`));
|
|
891
|
+
for (const entry of entries) {
|
|
892
|
+
console.log(` ${entry.id.slice(0, 8)} (${entry.type}): ${entry.title || entry.content.slice(0, 40)}...`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const startedAt = new Date().toISOString();
|
|
899
|
+
const updatedEntries = [];
|
|
900
|
+
for (const entry of entries) {
|
|
901
|
+
const updated = await storage.updateEntry(entry.id, { status: 'wip', startedAt });
|
|
902
|
+
updatedEntries.push(updated);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (options.json) {
|
|
906
|
+
console.log(JSON.stringify({ success: true, count: updatedEntries.length, entries: updatedEntries }));
|
|
907
|
+
} else {
|
|
908
|
+
console.log(chalk.green(`Started ${updatedEntries.length} entries (marked as WIP)`));
|
|
909
|
+
for (const entry of updatedEntries) {
|
|
910
|
+
console.log(` ${chalk.blue(entry.id.slice(0, 8))} ${entry.title || entry.content.slice(0, 40)}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
program
|
|
916
|
+
.command('stop [ids...]')
|
|
917
|
+
.description('Stop working on entries (remove WIP status)')
|
|
918
|
+
.option('--all', 'Stop all WIP entries')
|
|
919
|
+
.option('--dry-run', 'Show what would be stopped')
|
|
920
|
+
.option('--json', 'Output as JSON')
|
|
921
|
+
.action(async (ids, options) => {
|
|
922
|
+
let entries;
|
|
923
|
+
|
|
924
|
+
if (ids.length > 0) {
|
|
925
|
+
entries = await storage.getEntriesByIds(ids);
|
|
926
|
+
entries = entries.filter(e => e.status === 'wip');
|
|
927
|
+
} else if (options.all) {
|
|
928
|
+
const result = await storage.listEntries({ status: 'wip', limit: 1000 });
|
|
929
|
+
entries = result.entries;
|
|
930
|
+
} else {
|
|
931
|
+
console.error(chalk.red('Please provide entry IDs or use --all to stop all WIP entries'));
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (entries.length === 0) {
|
|
936
|
+
if (options.json) {
|
|
937
|
+
console.log(JSON.stringify({ success: true, count: 0, entries: [] }));
|
|
938
|
+
} else {
|
|
939
|
+
console.log(chalk.gray('No WIP entries to stop'));
|
|
940
|
+
}
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (options.dryRun) {
|
|
945
|
+
if (options.json) {
|
|
946
|
+
console.log(JSON.stringify({ success: true, dryRun: true, count: entries.length, entries }));
|
|
947
|
+
} else {
|
|
948
|
+
console.log(chalk.yellow(`Would stop ${entries.length} WIP entries:`));
|
|
949
|
+
for (const entry of entries) {
|
|
950
|
+
const duration = entry.startedAt ? formatDuration(Date.now() - new Date(entry.startedAt).getTime()) : '';
|
|
951
|
+
console.log(` ${entry.id.slice(0, 8)}: ${entry.title || entry.content.slice(0, 40)}... ${duration}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const updatedEntries = [];
|
|
958
|
+
for (const entry of entries) {
|
|
959
|
+
const updated = await storage.updateEntry(entry.id, { status: 'active', startedAt: null });
|
|
960
|
+
updatedEntries.push(updated);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (options.json) {
|
|
964
|
+
console.log(JSON.stringify({ success: true, count: updatedEntries.length, entries: updatedEntries }));
|
|
965
|
+
} else {
|
|
966
|
+
console.log(chalk.green(`Stopped ${updatedEntries.length} entries (marked as active)`));
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
672
970
|
program
|
|
673
971
|
.command('archive [ids...]')
|
|
674
972
|
.description('Archive entries')
|
|
@@ -1043,7 +1341,7 @@ async function main() {
|
|
|
1043
1341
|
|
|
1044
1342
|
program
|
|
1045
1343
|
.command('focus')
|
|
1046
|
-
.description('Show what to work on now: P1 todos + active projects')
|
|
1344
|
+
.description('Show what to work on now: P1 todos + overdue + active projects')
|
|
1047
1345
|
.option('--json', 'Output as JSON')
|
|
1048
1346
|
.action(async (options) => {
|
|
1049
1347
|
// Get P1 todos
|
|
@@ -1054,6 +1352,17 @@ async function main() {
|
|
|
1054
1352
|
limit: 100
|
|
1055
1353
|
});
|
|
1056
1354
|
|
|
1355
|
+
// Get overdue entries
|
|
1356
|
+
const overdueEntries = await storage.listEntries({
|
|
1357
|
+
overdue: true,
|
|
1358
|
+
status: 'raw,active',
|
|
1359
|
+
limit: 100
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
// Deduplicate (some P1 todos may also be overdue)
|
|
1363
|
+
const p1TodoIds = new Set(p1Todos.entries.map(e => e.id));
|
|
1364
|
+
const overdueNotP1 = overdueEntries.entries.filter(e => !p1TodoIds.has(e.id));
|
|
1365
|
+
|
|
1057
1366
|
// Get active projects with progress
|
|
1058
1367
|
const projects = await storage.listEntries({
|
|
1059
1368
|
type: 'project',
|
|
@@ -1075,18 +1384,32 @@ async function main() {
|
|
|
1075
1384
|
console.log(JSON.stringify({
|
|
1076
1385
|
success: true,
|
|
1077
1386
|
p1Todos: p1Todos.entries,
|
|
1387
|
+
overdueItems: overdueNotP1,
|
|
1078
1388
|
activeProjects: projectsWithProgress
|
|
1079
1389
|
}, null, 2));
|
|
1080
1390
|
} else {
|
|
1081
1391
|
console.log(chalk.bold('Focus: What to work on now\n'));
|
|
1082
1392
|
|
|
1393
|
+
// Overdue section (show first - most urgent)
|
|
1394
|
+
if (overdueNotP1.length > 0) {
|
|
1395
|
+
console.log(chalk.red.bold('Overdue:'));
|
|
1396
|
+
for (const item of overdueNotP1) {
|
|
1397
|
+
const shortId = item.id.slice(0, 8);
|
|
1398
|
+
const title = item.title || item.content.slice(0, 50);
|
|
1399
|
+
const dueStr = formatDueDate(item.due);
|
|
1400
|
+
console.log(` ${chalk.blue(shortId)} ${title} ${chalk.red(`[${dueStr}]`)}`);
|
|
1401
|
+
}
|
|
1402
|
+
console.log();
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1083
1405
|
// P1 Todos
|
|
1084
1406
|
if (p1Todos.entries.length > 0) {
|
|
1085
1407
|
console.log(chalk.yellow.bold('P1 Todos:'));
|
|
1086
1408
|
for (const todo of p1Todos.entries) {
|
|
1087
1409
|
const shortId = todo.id.slice(0, 8);
|
|
1088
1410
|
const title = todo.title || todo.content.slice(0, 50);
|
|
1089
|
-
|
|
1411
|
+
const dueStr = todo.due ? chalk.magenta(` [due: ${formatDueDate(todo.due)}]`) : '';
|
|
1412
|
+
console.log(` ${chalk.blue(shortId)} ${title}${dueStr}`);
|
|
1090
1413
|
}
|
|
1091
1414
|
console.log();
|
|
1092
1415
|
} else {
|
package/src/storage.js
CHANGED
|
@@ -15,7 +15,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
15
15
|
|
|
16
16
|
// Valid types, statuses, and date formats
|
|
17
17
|
const VALID_TYPES = ['idea', 'project', 'feature', 'todo', 'question', 'reference', 'note'];
|
|
18
|
-
const VALID_STATUSES = ['raw', 'active', 'someday', 'done', 'archived'];
|
|
18
|
+
const VALID_STATUSES = ['raw', 'active', 'wip', 'someday', 'done', 'archived'];
|
|
19
19
|
const VALID_DATE_FORMATS = ['relative', 'absolute', 'locale'];
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -201,6 +201,109 @@ 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
|
+
// Bare weekday names (e.g., "monday", "friday")
|
|
254
|
+
const bareWeekdayMatch = lowered.match(/^(monday|tuesday|wednesday|thursday|friday|saturday|sunday)$/);
|
|
255
|
+
if (bareWeekdayMatch) {
|
|
256
|
+
const weekdays = {
|
|
257
|
+
sunday: 0,
|
|
258
|
+
monday: 1,
|
|
259
|
+
tuesday: 2,
|
|
260
|
+
wednesday: 3,
|
|
261
|
+
thursday: 4,
|
|
262
|
+
friday: 5,
|
|
263
|
+
saturday: 6
|
|
264
|
+
};
|
|
265
|
+
const target = weekdays[bareWeekdayMatch[1]];
|
|
266
|
+
const now = new Date();
|
|
267
|
+
const current = now.getDay();
|
|
268
|
+
let diff = (target - current + 7) % 7;
|
|
269
|
+
if (diff === 0) diff = 7;
|
|
270
|
+
return endOfDay(new Date(now.getTime() + diff * dayMs)).toISOString();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const inMatch = lowered.match(/^in\s+(\d+)\s*(hour|hours|day|days|week|weeks|month|months)$/);
|
|
274
|
+
if (inMatch) {
|
|
275
|
+
const value = parseInt(inMatch[1], 10);
|
|
276
|
+
const unit = inMatch[2];
|
|
277
|
+
const multipliers = {
|
|
278
|
+
hour: 60 * 60 * 1000,
|
|
279
|
+
hours: 60 * 60 * 1000,
|
|
280
|
+
day: dayMs,
|
|
281
|
+
days: dayMs,
|
|
282
|
+
week: 7 * dayMs,
|
|
283
|
+
weeks: 7 * dayMs,
|
|
284
|
+
month: 30 * dayMs,
|
|
285
|
+
months: 30 * dayMs
|
|
286
|
+
};
|
|
287
|
+
return new Date(Date.now() + value * multipliers[unit]).toISOString();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Try relative duration first (3d, 1w, etc.) - for FUTURE dates
|
|
291
|
+
const durationMs = parseDuration(trimmed);
|
|
292
|
+
if (durationMs !== null) {
|
|
293
|
+
return new Date(Date.now() + durationMs).toISOString();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Try ISO date (YYYY-MM-DD)
|
|
297
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
298
|
+
const date = new Date(trimmed + 'T23:59:59');
|
|
299
|
+
if (!isNaN(date.getTime())) {
|
|
300
|
+
return date.toISOString();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
204
307
|
/**
|
|
205
308
|
* Add a new entry
|
|
206
309
|
*/
|
|
@@ -216,6 +319,16 @@ async function addEntry(options) {
|
|
|
216
319
|
}
|
|
217
320
|
}
|
|
218
321
|
|
|
322
|
+
// Parse due date if provided
|
|
323
|
+
let dueDate = undefined;
|
|
324
|
+
const dueInput = typeof options.due === 'string' ? options.due.trim() : options.due;
|
|
325
|
+
if (dueInput) {
|
|
326
|
+
dueDate = parseDate(dueInput);
|
|
327
|
+
if (!dueDate) {
|
|
328
|
+
throw new Error(`Invalid due date: ${options.due}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
219
332
|
const now = new Date().toISOString();
|
|
220
333
|
const entry = {
|
|
221
334
|
id: uuidv4(),
|
|
@@ -227,6 +340,7 @@ async function addEntry(options) {
|
|
|
227
340
|
tags: options.tags || [],
|
|
228
341
|
parent: parentId || undefined,
|
|
229
342
|
related: [],
|
|
343
|
+
due: dueDate,
|
|
230
344
|
createdAt: now,
|
|
231
345
|
updatedAt: now,
|
|
232
346
|
source: options.source || 'cli'
|
|
@@ -392,6 +506,40 @@ async function listEntries(query = {}) {
|
|
|
392
506
|
});
|
|
393
507
|
}
|
|
394
508
|
|
|
509
|
+
// Filter by due before
|
|
510
|
+
if (query.dueBefore) {
|
|
511
|
+
const cutoffDate = parseDate(query.dueBefore);
|
|
512
|
+
if (cutoffDate) {
|
|
513
|
+
entries = entries.filter(e => e.due && new Date(e.due) <= new Date(cutoffDate));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Filter by due after
|
|
518
|
+
if (query.dueAfter) {
|
|
519
|
+
const cutoffDate = parseDate(query.dueAfter);
|
|
520
|
+
if (cutoffDate) {
|
|
521
|
+
entries = entries.filter(e => e.due && new Date(e.due) >= new Date(cutoffDate));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Filter overdue (due before now, not done/archived)
|
|
526
|
+
if (query.overdue) {
|
|
527
|
+
const now = new Date();
|
|
528
|
+
entries = entries.filter(e =>
|
|
529
|
+
e.due &&
|
|
530
|
+
new Date(e.due) < now &&
|
|
531
|
+
e.status !== 'done' &&
|
|
532
|
+
e.status !== 'archived'
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Filter entries with/without due date
|
|
537
|
+
if (query.hasDue === true) {
|
|
538
|
+
entries = entries.filter(e => e.due);
|
|
539
|
+
} else if (query.hasDue === false) {
|
|
540
|
+
entries = entries.filter(e => !e.due);
|
|
541
|
+
}
|
|
542
|
+
|
|
395
543
|
// Include done if requested
|
|
396
544
|
if (!query.includeDone && query.status !== 'done') {
|
|
397
545
|
entries = entries.filter(e => e.status !== 'done');
|
|
@@ -400,6 +548,13 @@ async function listEntries(query = {}) {
|
|
|
400
548
|
// Sort
|
|
401
549
|
const sortField = query.sort || 'created';
|
|
402
550
|
entries.sort((a, b) => {
|
|
551
|
+
if (sortField === 'due') {
|
|
552
|
+
// Entries without due dates go to the end
|
|
553
|
+
if (!a.due && !b.due) return 0;
|
|
554
|
+
if (!a.due) return 1;
|
|
555
|
+
if (!b.due) return -1;
|
|
556
|
+
return new Date(a.due) - new Date(b.due);
|
|
557
|
+
}
|
|
403
558
|
if (sortField === 'priority') {
|
|
404
559
|
const pA = a.priority || 99;
|
|
405
560
|
const pB = b.priority || 99;
|
|
@@ -518,6 +673,8 @@ async function updateEntry(id, updates) {
|
|
|
518
673
|
// Handle null values (clear fields)
|
|
519
674
|
if (updates.priority === null) delete entry.priority;
|
|
520
675
|
if (updates.parent === null) delete entry.parent;
|
|
676
|
+
if (updates.due === null) delete entry.due;
|
|
677
|
+
if (updates.startedAt === null) delete entry.startedAt;
|
|
521
678
|
|
|
522
679
|
saveEntries(data);
|
|
523
680
|
|
|
@@ -794,6 +951,7 @@ module.exports = {
|
|
|
794
951
|
getDefaultConfig,
|
|
795
952
|
validateConfigValue,
|
|
796
953
|
saveConfig,
|
|
954
|
+
parseDate,
|
|
797
955
|
CONFIG_DIR,
|
|
798
956
|
ENTRIES_FILE,
|
|
799
957
|
ARCHIVE_FILE,
|