synap 0.3.0 → 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)
@@ -74,6 +74,8 @@ Detect user intent and respond appropriately:
74
74
  | Search | `synap search "keyword"` |
75
75
  | Show details | `synap show <id>` |
76
76
  | Mark done | `synap done <id>` |
77
+ | Start working | `synap start <id>` |
78
+ | Stop working | `synap stop <id>` |
77
79
  | Get stats | `synap stats` |
78
80
  | Setup wizard | `synap setup` |
79
81
  | Edit preferences | `synap preferences --edit` |
@@ -110,7 +112,7 @@ synap add --type project --title "Website Redesign" "Complete overhaul of the ma
110
112
  - `--priority <1|2|3>`: 1=high, 2=medium, 3=low
111
113
  - `--tags <tags>`: Comma-separated tags
112
114
  - `--parent <id>`: Parent entry ID
113
- - `--due <date>`: Due date (YYYY-MM-DD, 3d/1w, or keywords: today, tomorrow, next monday)
115
+ - `--due <date>`: Due date (YYYY-MM-DD, 3d/1w, weekday names: monday/friday, or keywords: today, tomorrow, next monday)
114
116
  - `--json`: JSON output
115
117
 
116
118
  #### `synap todo <content>`
@@ -133,6 +135,18 @@ synap question "Should we migrate to TypeScript?"
133
135
 
134
136
  Options: `--priority`, `--tags`, `--parent`, `--due`, `--json`
135
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
+
136
150
  ### Query Commands
137
151
 
138
152
  #### `synap list`
@@ -154,7 +168,7 @@ synap list --json # JSON output for parsing
154
168
 
155
169
  **Options**:
156
170
  - `--type <type>`: Filter by entry type
157
- - `--status <status>`: raw, active, someday, done, archived (default: raw,active)
171
+ - `--status <status>`: raw, active, wip, someday, done, archived (default: raw,active)
158
172
  - `--tags <tags>`: Comma-separated, AND logic
159
173
  - `--priority <1|2|3>`: Filter by priority
160
174
  - `--parent <id>`: Children of specific entry
@@ -239,6 +253,36 @@ synap done --type todo --tags "sprint-1" # By filter
239
253
  synap done --dry-run --type todo # Preview first
240
254
  ```
241
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
+
242
286
  #### `synap archive <ids...>`
243
287
  Archive entries (hides from default view).
244
288
 
@@ -447,12 +491,13 @@ Critical for preventing accidental mass changes:
447
491
  "content": "The full text of the entry",
448
492
  "title": "Short title (optional)",
449
493
  "type": "idea|project|feature|todo|question|reference|note",
450
- "status": "raw|active|someday|done|archived",
494
+ "status": "raw|active|wip|someday|done|archived",
451
495
  "priority": 1|2|3|null,
452
496
  "tags": ["tag1", "tag2"],
453
497
  "parent": "parent-id|null",
454
498
  "related": ["id1", "id2"],
455
499
  "due": "2026-01-10T23:59:59.000Z",
500
+ "startedAt": "2026-01-10T08:30:00.000Z",
456
501
  "createdAt": "2026-01-05T08:30:00.000Z",
457
502
  "updatedAt": "2026-01-05T08:30:00.000Z",
458
503
  "source": "cli|agent|import"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synap",
3
- "version": "0.3.0",
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
@@ -81,6 +81,17 @@ async function main() {
81
81
  return date.toLocaleDateString();
82
82
  };
83
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
+
84
95
  // ============================================
85
96
  // CAPTURE COMMANDS
86
97
  // ============================================
@@ -297,6 +308,67 @@ async function main() {
297
308
  .option('--json', 'Output as JSON')
298
309
  .action(createTypeShorthand('reference', 'reference'));
299
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
+
300
372
  // ============================================
301
373
  // QUERY COMMANDS
302
374
  // ============================================
@@ -775,6 +847,126 @@ async function main() {
775
847
  }
776
848
  });
777
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
+
778
970
  program
779
971
  .command('archive [ids...]')
780
972
  .description('Archive entries')
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
  /**
@@ -250,6 +250,26 @@ function parseDate(input) {
250
250
  return endOfDay(new Date(now.getTime() + diff * dayMs)).toISOString();
251
251
  }
252
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
+
253
273
  const inMatch = lowered.match(/^in\s+(\d+)\s*(hour|hours|day|days|week|weeks|month|months)$/);
254
274
  if (inMatch) {
255
275
  const value = parseInt(inMatch[1], 10);
@@ -654,6 +674,7 @@ async function updateEntry(id, updates) {
654
674
  if (updates.priority === null) delete entry.priority;
655
675
  if (updates.parent === null) delete entry.parent;
656
676
  if (updates.due === null) delete entry.due;
677
+ if (updates.startedAt === null) delete entry.startedAt;
657
678
 
658
679
  saveEntries(data);
659
680