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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synap",
3
- "version": "0.1.1",
3
+ "version": "0.4.0",
4
4
  "description": "A CLI for externalizing your working memory",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
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
- const entry = await storage.addEntry({
84
- content,
85
- title: options.title,
86
- type: options.type,
87
- priority: options.priority ? parseInt(options.priority, 10) : undefined,
88
- tags,
89
- parent: options.parent,
90
- source: 'cli'
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
- if (options.json) {
94
- console.log(JSON.stringify({ success: true, entry }, null, 2));
95
- } else {
96
- const shortId = entry.id.slice(0, 8);
97
- const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
98
- console.log(chalk.green(`Added ${entry.type} ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"`);
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
- const entry = await storage.addEntry({
114
- content,
115
- type: 'todo',
116
- priority: options.priority ? parseInt(options.priority, 10) : undefined,
117
- tags,
118
- parent: options.parent,
119
- source: 'cli'
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
- if (options.json) {
123
- console.log(JSON.stringify({ success: true, entry }, null, 2));
124
- } else {
125
- const shortId = entry.id.slice(0, 8);
126
- const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
127
- console.log(chalk.green(`Added todo ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"`);
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
- const entry = await storage.addEntry({
143
- content,
144
- type: 'question',
145
- priority: options.priority ? parseInt(options.priority, 10) : undefined,
146
- tags,
147
- parent: options.parent,
148
- source: 'cli'
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
- if (options.json) {
152
- console.log(JSON.stringify({ success: true, entry }, null, 2));
153
- } else {
154
- const shortId = entry.id.slice(0, 8);
155
- const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
156
- console.log(chalk.green(`Added question ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"`);
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
- const entry = await storage.addEntry({
167
- content,
168
- type: typeName,
169
- priority: options.priority ? parseInt(options.priority, 10) : undefined,
170
- tags,
171
- parent: options.parent,
172
- source: 'cli'
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
- if (options.json) {
176
- console.log(JSON.stringify({ success: true, entry }, null, 2));
177
- } else {
178
- const shortId = entry.id.slice(0, 8);
179
- const priorityBadge = entry.priority ? chalk.yellow(`[P${entry.priority}] `) : '';
180
- console.log(chalk.green(`Added ${displayName} ${shortId}: `) + priorityBadge + `"${content.slice(0, 50)}${content.length > 50 ? '...' : ''}"`);
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
- console.log(` ${chalk.blue(shortId)} ${priorityBadge} ${title}${truncated}${tags} ${timeStr}`);
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
- console.log(` ${chalk.blue(shortId)} ${title}`);
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,