synap 0.5.2 → 0.6.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 +46 -0
- package/package.json +1 -1
- package/src/cli.js +100 -1
- package/src/storage.js +67 -1
|
@@ -147,6 +147,33 @@ synap log a1b2c3d4 "Completed first draft" --inherit-tags
|
|
|
147
147
|
- `--inherit-tags`: Copy tags from parent entry
|
|
148
148
|
- `--json`: JSON output
|
|
149
149
|
|
|
150
|
+
#### `synap batch-add`
|
|
151
|
+
Add multiple entries in one operation.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# From file
|
|
155
|
+
synap batch-add --file entries.json
|
|
156
|
+
|
|
157
|
+
# From stdin (pipe)
|
|
158
|
+
echo '[{"content":"Task 1","type":"todo"},{"content":"Task 2","type":"todo"}]' | synap batch-add
|
|
159
|
+
|
|
160
|
+
# Dry run
|
|
161
|
+
synap batch-add --file entries.json --dry-run
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Input format (JSON array):**
|
|
165
|
+
```json
|
|
166
|
+
[
|
|
167
|
+
{"content": "First entry", "type": "idea"},
|
|
168
|
+
{"content": "Second entry", "type": "todo", "priority": 1, "tags": ["work"]}
|
|
169
|
+
]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Options**:
|
|
173
|
+
- `--file <path>`: Read from JSON file
|
|
174
|
+
- `--dry-run`: Preview what would be added
|
|
175
|
+
- `--json`: JSON output
|
|
176
|
+
|
|
150
177
|
### Query Commands
|
|
151
178
|
|
|
152
179
|
#### `synap list`
|
|
@@ -390,6 +417,24 @@ When user is dumping thoughts rapidly:
|
|
|
390
417
|
|
|
391
418
|
**Smart status defaulting**: When capturing with priority set, the CLI auto-promotes to `active` status (skipping triage). When adding entries with full metadata (priority, tags, due), there's no need to manually set status—the entry is already triaged.
|
|
392
419
|
|
|
420
|
+
### Grouping Detection Pattern
|
|
421
|
+
|
|
422
|
+
After capture sessions, detect opportunities to group related entries:
|
|
423
|
+
|
|
424
|
+
| Signal | Action |
|
|
425
|
+
|--------|--------|
|
|
426
|
+
| 3+ entries with same tag in one session | Suggest parent project with that tag as context |
|
|
427
|
+
| 3+ entries mentioning same keyword/topic | Suggest linking as related or creating parent |
|
|
428
|
+
| User mentions "for the X project" multiple times | Proactively suggest creating/linking to X project |
|
|
429
|
+
|
|
430
|
+
**Grouping workflow:**
|
|
431
|
+
1. After capture, analyze recent additions: `synap list --since 1h --json`
|
|
432
|
+
2. Group by common tags or detect semantic similarity
|
|
433
|
+
3. If grouping detected, propose: "These 4 entries seem related to [topic]. Create a parent project?"
|
|
434
|
+
4. On confirmation:
|
|
435
|
+
- `synap add "[Topic] Project" --type project --tags "topic"`
|
|
436
|
+
- For each child: `synap link <child-id> <project-id> --as-parent`
|
|
437
|
+
|
|
393
438
|
## Classification Rules
|
|
394
439
|
|
|
395
440
|
### Type Detection Heuristics
|
|
@@ -429,6 +474,7 @@ When user is dumping thoughts rapidly:
|
|
|
429
474
|
- If P1 todos exist, suggest `synap focus`.
|
|
430
475
|
- If many stale active items exist, suggest a weekly review.
|
|
431
476
|
- If preferences specify cadence, follow it by default.
|
|
477
|
+
- If 3+ entries added with common tags/context, suggest grouping under a parent project (see Grouping Detection Pattern).
|
|
432
478
|
|
|
433
479
|
## Batch Processing Protocols
|
|
434
480
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { program } = require('commander');
|
|
9
|
+
const fs = require('fs');
|
|
9
10
|
const pkg = require('../package.json');
|
|
10
11
|
|
|
11
12
|
// Storage and utility modules
|
|
@@ -381,6 +382,101 @@ async function main() {
|
|
|
381
382
|
}
|
|
382
383
|
});
|
|
383
384
|
|
|
385
|
+
program
|
|
386
|
+
.command('batch-add')
|
|
387
|
+
.description('Add multiple entries from JSON stdin or file')
|
|
388
|
+
.option('--file <path>', 'Read entries from JSON file')
|
|
389
|
+
.option('--dry-run', 'Preview what would be added')
|
|
390
|
+
.option('--json', 'Output as JSON')
|
|
391
|
+
.action(async (options) => {
|
|
392
|
+
let entriesData;
|
|
393
|
+
|
|
394
|
+
if (options.file) {
|
|
395
|
+
// Read from file
|
|
396
|
+
if (!fs.existsSync(options.file)) {
|
|
397
|
+
if (options.json) {
|
|
398
|
+
console.log(JSON.stringify({ success: false, error: `File not found: ${options.file}`, code: 'FILE_NOT_FOUND' }));
|
|
399
|
+
} else {
|
|
400
|
+
console.error(chalk.red(`File not found: ${options.file}`));
|
|
401
|
+
}
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
const content = fs.readFileSync(options.file, 'utf8');
|
|
405
|
+
entriesData = JSON.parse(content);
|
|
406
|
+
} else {
|
|
407
|
+
// Read from stdin
|
|
408
|
+
const chunks = [];
|
|
409
|
+
process.stdin.setEncoding('utf8');
|
|
410
|
+
for await (const chunk of process.stdin) {
|
|
411
|
+
chunks.push(chunk);
|
|
412
|
+
}
|
|
413
|
+
const input = chunks.join('');
|
|
414
|
+
if (!input.trim()) {
|
|
415
|
+
if (options.json) {
|
|
416
|
+
console.log(JSON.stringify({ success: false, error: 'No input provided. Pipe JSON array to stdin or use --file', code: 'NO_INPUT' }));
|
|
417
|
+
} else {
|
|
418
|
+
console.error(chalk.red('No input provided. Pipe JSON array to stdin or use --file'));
|
|
419
|
+
}
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
entriesData = JSON.parse(input);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Normalize to array
|
|
426
|
+
if (!Array.isArray(entriesData)) {
|
|
427
|
+
entriesData = entriesData.entries || [entriesData];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (entriesData.length === 0) {
|
|
431
|
+
if (options.json) {
|
|
432
|
+
console.log(JSON.stringify({ success: true, count: 0, entries: [] }));
|
|
433
|
+
} else {
|
|
434
|
+
console.log(chalk.gray('No entries to add'));
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Merge config tags with each entry's tags
|
|
440
|
+
entriesData = entriesData.map(e => ({
|
|
441
|
+
...e,
|
|
442
|
+
tags: [...new Set([...(config.defaultTags || []), ...(e.tags || [])])],
|
|
443
|
+
source: e.source || 'cli'
|
|
444
|
+
}));
|
|
445
|
+
|
|
446
|
+
if (options.dryRun) {
|
|
447
|
+
if (options.json) {
|
|
448
|
+
console.log(JSON.stringify({ success: true, dryRun: true, count: entriesData.length, entries: entriesData }));
|
|
449
|
+
} else {
|
|
450
|
+
console.log(chalk.yellow(`Would add ${entriesData.length} entries:`));
|
|
451
|
+
for (const entry of entriesData) {
|
|
452
|
+
console.log(` [${entry.type || 'idea'}] ${entry.content?.slice(0, 50)}${entry.content?.length > 50 ? '...' : ''}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const created = await storage.addEntries(entriesData);
|
|
460
|
+
|
|
461
|
+
if (options.json) {
|
|
462
|
+
console.log(JSON.stringify({ success: true, count: created.length, entries: created }, null, 2));
|
|
463
|
+
} else {
|
|
464
|
+
console.log(chalk.green(`Added ${created.length} entries`));
|
|
465
|
+
for (const entry of created) {
|
|
466
|
+
const shortId = entry.id.slice(0, 8);
|
|
467
|
+
console.log(` ${chalk.blue(shortId)} [${entry.type}] ${entry.title || entry.content.slice(0, 40)}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (options.json) {
|
|
472
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
473
|
+
} else {
|
|
474
|
+
console.error(chalk.red(err.message));
|
|
475
|
+
}
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
384
480
|
// ============================================
|
|
385
481
|
// QUERY COMMANDS
|
|
386
482
|
// ============================================
|
|
@@ -1216,7 +1312,10 @@ async function main() {
|
|
|
1216
1312
|
console.log(` ${type.padEnd(12)} ${count}`);
|
|
1217
1313
|
}
|
|
1218
1314
|
console.log('');
|
|
1219
|
-
|
|
1315
|
+
const p1Display = stats.highPriority === stats.highPriorityActive
|
|
1316
|
+
? `${stats.highPriority}`
|
|
1317
|
+
: `${stats.highPriority} (${stats.highPriorityActive} active)`;
|
|
1318
|
+
console.log(` High Priority (P1): ${p1Display}`);
|
|
1220
1319
|
console.log(` Created this week: ${stats.createdThisWeek}`);
|
|
1221
1320
|
console.log(` Updated today: ${stats.updatedToday}`);
|
|
1222
1321
|
|
package/src/storage.js
CHANGED
|
@@ -357,6 +357,65 @@ async function addEntry(options) {
|
|
|
357
357
|
return entry;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Add multiple entries in a single batch
|
|
362
|
+
* @param {Array} entriesData - Array of entry options
|
|
363
|
+
* @returns {Array} - Array of created entries
|
|
364
|
+
*/
|
|
365
|
+
async function addEntries(entriesData) {
|
|
366
|
+
const data = loadEntries();
|
|
367
|
+
const created = [];
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
|
|
370
|
+
for (const options of entriesData) {
|
|
371
|
+
// Resolve partial parent ID to full ID
|
|
372
|
+
let parentId = options.parent;
|
|
373
|
+
if (parentId) {
|
|
374
|
+
const parentEntry = data.entries.find(e => e.id.startsWith(parentId));
|
|
375
|
+
if (parentEntry) {
|
|
376
|
+
parentId = parentEntry.id;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Parse due date if provided
|
|
381
|
+
let dueDate = undefined;
|
|
382
|
+
const dueInput = typeof options.due === 'string' ? options.due.trim() : options.due;
|
|
383
|
+
if (dueInput) {
|
|
384
|
+
dueDate = parseDate(dueInput);
|
|
385
|
+
if (!dueDate) {
|
|
386
|
+
throw new Error(`Invalid due date: ${options.due}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const entry = {
|
|
391
|
+
id: uuidv4(),
|
|
392
|
+
content: options.content,
|
|
393
|
+
title: options.title || extractTitle(options.content),
|
|
394
|
+
type: VALID_TYPES.includes(options.type) ? options.type : 'idea',
|
|
395
|
+
status: VALID_STATUSES.includes(options.status) ? options.status : (options.priority ? 'active' : 'raw'),
|
|
396
|
+
priority: options.priority && [1, 2, 3].includes(options.priority) ? options.priority : undefined,
|
|
397
|
+
tags: options.tags || [],
|
|
398
|
+
parent: parentId || undefined,
|
|
399
|
+
related: [],
|
|
400
|
+
due: dueDate,
|
|
401
|
+
createdAt: now,
|
|
402
|
+
updatedAt: now,
|
|
403
|
+
source: options.source || 'cli'
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// Clean up undefined fields
|
|
407
|
+
Object.keys(entry).forEach(key => {
|
|
408
|
+
if (entry[key] === undefined) delete entry[key];
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
data.entries.push(entry);
|
|
412
|
+
created.push(entry);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
saveEntries(data);
|
|
416
|
+
return created;
|
|
417
|
+
}
|
|
418
|
+
|
|
360
419
|
/**
|
|
361
420
|
* Extract title from content (first line, max 60 chars)
|
|
362
421
|
*/
|
|
@@ -764,6 +823,7 @@ async function getStats() {
|
|
|
764
823
|
byStatus: {},
|
|
765
824
|
byType: {},
|
|
766
825
|
highPriority: 0,
|
|
826
|
+
highPriorityActive: 0,
|
|
767
827
|
createdThisWeek: 0,
|
|
768
828
|
updatedToday: 0
|
|
769
829
|
};
|
|
@@ -780,7 +840,12 @@ async function getStats() {
|
|
|
780
840
|
stats.byType[entry.type] = (stats.byType[entry.type] || 0) + 1;
|
|
781
841
|
|
|
782
842
|
// High priority
|
|
783
|
-
if (entry.priority === 1)
|
|
843
|
+
if (entry.priority === 1) {
|
|
844
|
+
stats.highPriority++;
|
|
845
|
+
if (entry.status === 'raw' || entry.status === 'active') {
|
|
846
|
+
stats.highPriorityActive++;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
784
849
|
|
|
785
850
|
// Created this week
|
|
786
851
|
if (new Date(entry.createdAt) >= oneWeekAgo) stats.createdThisWeek++;
|
|
@@ -932,6 +997,7 @@ async function importEntries(entries, options = {}) {
|
|
|
932
997
|
|
|
933
998
|
module.exports = {
|
|
934
999
|
addEntry,
|
|
1000
|
+
addEntries,
|
|
935
1001
|
getEntry,
|
|
936
1002
|
getEntriesByIds,
|
|
937
1003
|
getChildren,
|