synap 0.5.1 → 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 +107 -7
- package/src/skill-installer.js +20 -17
- 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
|
|
|
@@ -1835,10 +1934,11 @@ async function main() {
|
|
|
1835
1934
|
skillResult = { ...skillResult, ...(await skillInstaller.install()) };
|
|
1836
1935
|
if (skillResult.installed) {
|
|
1837
1936
|
console.log(` ${chalk.green('✓')} Skill installed at ~/.claude/skills/synap-assistant/`);
|
|
1937
|
+
if (skillResult.backupFile) {
|
|
1938
|
+
console.log(` ${chalk.yellow('•')} Your modifications backed up to ${skillResult.backupFile}`);
|
|
1939
|
+
}
|
|
1838
1940
|
} else if (skillResult.skipped) {
|
|
1839
1941
|
console.log(` ${chalk.yellow('•')} Skill already up to date`);
|
|
1840
|
-
} else if (skillResult.needsForce) {
|
|
1841
|
-
console.log(` ${chalk.yellow('•')} Skill modified. Use ${chalk.cyan('synap install-skill --force')}`);
|
|
1842
1942
|
}
|
|
1843
1943
|
} catch (err) {
|
|
1844
1944
|
console.log(` ${chalk.red('✗')} Skill install failed: ${err.message}`);
|
|
@@ -2040,7 +2140,6 @@ async function main() {
|
|
|
2040
2140
|
.command('install-skill')
|
|
2041
2141
|
.description('Install Claude Code skill')
|
|
2042
2142
|
.option('--uninstall', 'Remove the skill')
|
|
2043
|
-
.option('--force', 'Override ownership check')
|
|
2044
2143
|
.action(async (options) => {
|
|
2045
2144
|
const skillInstaller = require('./skill-installer');
|
|
2046
2145
|
|
|
@@ -2048,13 +2147,14 @@ async function main() {
|
|
|
2048
2147
|
await skillInstaller.uninstall();
|
|
2049
2148
|
console.log(chalk.green('Skill uninstalled'));
|
|
2050
2149
|
} else {
|
|
2051
|
-
const result = await skillInstaller.install(
|
|
2150
|
+
const result = await skillInstaller.install();
|
|
2052
2151
|
if (result.installed) {
|
|
2053
2152
|
console.log(chalk.green('Skill installed to ~/.claude/skills/synap-assistant/'));
|
|
2153
|
+
if (result.backupFile) {
|
|
2154
|
+
console.log(chalk.yellow(`Your modifications were backed up to ${result.backupFile}`));
|
|
2155
|
+
}
|
|
2054
2156
|
} else if (result.skipped) {
|
|
2055
2157
|
console.log(chalk.yellow('Skill already up to date'));
|
|
2056
|
-
} else if (result.needsForce) {
|
|
2057
|
-
console.log(chalk.yellow('Skill was modified by user. Use --force to overwrite.'));
|
|
2058
2158
|
}
|
|
2059
2159
|
}
|
|
2060
2160
|
});
|
package/src/skill-installer.js
CHANGED
|
@@ -116,6 +116,8 @@ async function install(options = {}) {
|
|
|
116
116
|
fs.mkdirSync(TARGET_SKILL_DIR, { recursive: true });
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
let backupFile = null;
|
|
120
|
+
|
|
119
121
|
// Check if target exists
|
|
120
122
|
if (fs.existsSync(TARGET_SKILL_FILE)) {
|
|
121
123
|
const targetContent = fs.readFileSync(TARGET_SKILL_FILE, 'utf8');
|
|
@@ -124,32 +126,33 @@ async function install(options = {}) {
|
|
|
124
126
|
const canonicalTargetHash = getHash(canonicalTarget);
|
|
125
127
|
const targetMatchesSource = canonicalTargetHash === sourceHash;
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
// Already up to date
|
|
130
|
+
if (targetMatchesSource && targetContent === normalizedSourceContent) {
|
|
131
|
+
return { installed: false, skipped: true };
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
// Check if user modified the file (hash mismatch or different source)
|
|
135
|
+
const userModified = targetSource !== SKILL_SOURCE ||
|
|
136
|
+
(targetHash && targetHash !== canonicalTargetHash) ||
|
|
137
|
+
(!targetHash && !targetMatchesSource);
|
|
138
|
+
|
|
139
|
+
if (userModified) {
|
|
140
|
+
// Create timestamped backup
|
|
141
|
+
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
142
|
+
backupFile = `${TARGET_SKILL_FILE}.backup.${timestamp}`;
|
|
143
|
+
// If backup already exists today, add time
|
|
144
|
+
if (fs.existsSync(backupFile)) {
|
|
145
|
+
const time = new Date().toISOString().slice(11, 16).replace(':', '');
|
|
146
|
+
backupFile = `${TARGET_SKILL_FILE}.backup.${timestamp}-${time}`;
|
|
142
147
|
}
|
|
148
|
+
fs.writeFileSync(backupFile, targetContent);
|
|
143
149
|
}
|
|
144
|
-
|
|
145
|
-
const backupFile = TARGET_SKILL_FILE + '.backup';
|
|
146
|
-
fs.writeFileSync(backupFile, targetContent);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
// Install
|
|
150
153
|
fs.writeFileSync(TARGET_SKILL_FILE, normalizedSourceContent);
|
|
151
154
|
|
|
152
|
-
return { installed: true };
|
|
155
|
+
return { installed: true, backupFile };
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
/**
|
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,
|