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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synap",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "A CLI for externalizing your working memory",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -2,17 +2,9 @@
2
2
 
3
3
  /**
4
4
  * postinstall.js - Run after npm install
5
- * Shows hints and optionally auto-updates the skill
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
- // Check if skill is installed
27
- if (fs.existsSync(TARGET_SKILL_FILE)) {
28
- try {
29
- const skillInstaller = require('../src/skill-installer');
30
- const result = await skillInstaller.install();
31
- if (result.installed) {
32
- console.log(`${CYAN}Skill auto-updated.${RESET}`);
33
- } else if (result.skipped) {
34
- console.log(`${CYAN}Skill already up to date.${RESET}`);
35
- } else if (result.needsForce) {
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
- } else {
42
- console.log('To enable AI agent integration, run:');
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
- 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
- });
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
- 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 ? '...' : ''}"`);
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
- 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
- });
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
- 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 ? '...' : ''}"`);
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
- 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
- });
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
- 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 ? '...' : ''}"`);
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
- 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
- });
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
- 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 ? '...' : ''}"`);
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
- console.log(` ${chalk.blue(shortId)} ${priorityBadge} ${title}${truncated}${tags} ${timeStr}`);
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
- console.log(` ${chalk.blue(shortId)} ${title}`);
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,