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.
- package/.claude/skills/synap-assistant/SKILL.md +49 -4
- package/package.json +1 -1
- package/src/cli.js +192 -0
- package/src/storage.js +22 -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)
|
|
@@ -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
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
|
|