synap 0.7.0 → 0.8.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 +66 -1
- package/package.json +1 -1
- package/src/cli.js +136 -14
- package/src/deletion-log.js +20 -13
- package/src/preferences.js +23 -12
- package/src/storage.js +137 -17
|
@@ -36,7 +36,33 @@ When assisting users with their synap entries:
|
|
|
36
36
|
|
|
37
37
|
## User Preferences (Memory)
|
|
38
38
|
|
|
39
|
-
synap stores
|
|
39
|
+
synap stores user data in a configurable data directory (default: `~/.config/synap/`).
|
|
40
|
+
|
|
41
|
+
**Data files (syncable):**
|
|
42
|
+
- `entries.json` - Active entries
|
|
43
|
+
- `archive.json` - Archived entries
|
|
44
|
+
- `user-preferences.md` - Agent memory / user preferences
|
|
45
|
+
|
|
46
|
+
**Config files (local only):**
|
|
47
|
+
- `config.json` - Settings (including `dataDir` for custom data location)
|
|
48
|
+
- `deletion-log.json` - Audit trail for restore
|
|
49
|
+
|
|
50
|
+
### Custom Data Directory (for sync)
|
|
51
|
+
|
|
52
|
+
Users can point synap to a custom folder (e.g., a git repo for multi-device sync):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
synap config dataDir ~/synap-data
|
|
56
|
+
# Or during setup:
|
|
57
|
+
synap setup
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
When custom `dataDir` is set:
|
|
61
|
+
- Data files go to the custom location
|
|
62
|
+
- Config stays in `~/.config/synap/`
|
|
63
|
+
- User can sync data folder via git, Dropbox, iCloud, etc.
|
|
64
|
+
|
|
65
|
+
### Preferences Operations
|
|
40
66
|
|
|
41
67
|
- Read preferences at the start of a session when present.
|
|
42
68
|
- Prefer idempotent updates with `synap preferences set --section "Tag Meanings" --entry "#urgent = must do today"`.
|
|
@@ -377,8 +403,47 @@ synap import backup.json --merge # Update existing + add new
|
|
|
377
403
|
synap import backup.json --skip-existing # Only add new
|
|
378
404
|
```
|
|
379
405
|
|
|
406
|
+
#### `synap config`
|
|
407
|
+
View or update configuration.
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
synap config # Show all settings + paths
|
|
411
|
+
synap config dataDir # Show data directory
|
|
412
|
+
synap config dataDir ~/synap-data # Set custom data directory
|
|
413
|
+
synap config --reset # Reset to defaults
|
|
414
|
+
```
|
|
415
|
+
|
|
380
416
|
## Workflow Patterns
|
|
381
417
|
|
|
418
|
+
### Multi-Device Sync Setup
|
|
419
|
+
|
|
420
|
+
For users who want to sync their synap across devices:
|
|
421
|
+
|
|
422
|
+
1. **Set custom data directory:**
|
|
423
|
+
```bash
|
|
424
|
+
synap config dataDir ~/synap-data
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
2. **Initialize git (optional):**
|
|
428
|
+
```bash
|
|
429
|
+
cd ~/synap-data
|
|
430
|
+
git init
|
|
431
|
+
git remote add origin git@github.com:user/synap-data.git
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
3. **Daily sync workflow:**
|
|
435
|
+
```bash
|
|
436
|
+
cd ~/synap-data && git pull # Start of day
|
|
437
|
+
# ... use synap normally ...
|
|
438
|
+
cd ~/synap-data && git add . && git commit -m "sync" && git push # End of day
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
4. **On a new device:**
|
|
442
|
+
```bash
|
|
443
|
+
git clone git@github.com:user/synap-data.git ~/synap-data
|
|
444
|
+
synap config dataDir ~/synap-data
|
|
445
|
+
```
|
|
446
|
+
|
|
382
447
|
### Daily Review
|
|
383
448
|
|
|
384
449
|
Run this each morning to get oriented:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2032,6 +2032,8 @@ async function main() {
|
|
|
2032
2032
|
.option('--json', 'Output as JSON')
|
|
2033
2033
|
.action(async (options) => {
|
|
2034
2034
|
const fs = require('fs');
|
|
2035
|
+
const os = require('os');
|
|
2036
|
+
const path = require('path');
|
|
2035
2037
|
|
|
2036
2038
|
let hasEntries = false;
|
|
2037
2039
|
if (fs.existsSync(storage.ENTRIES_FILE)) {
|
|
@@ -2043,16 +2045,9 @@ async function main() {
|
|
|
2043
2045
|
}
|
|
2044
2046
|
}
|
|
2045
2047
|
|
|
2046
|
-
let createdEntry = null;
|
|
2047
|
-
if (!hasEntries) {
|
|
2048
|
-
createdEntry = await storage.addEntry({
|
|
2049
|
-
content: 'My first thought',
|
|
2050
|
-
type: config.defaultType || 'idea',
|
|
2051
|
-
source: 'setup'
|
|
2052
|
-
});
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
2048
|
let skillResult = { prompted: false };
|
|
2049
|
+
let dataLocationResult = { configured: false };
|
|
2050
|
+
|
|
2056
2051
|
if (!options.json) {
|
|
2057
2052
|
console.log('');
|
|
2058
2053
|
console.log(chalk.bold('Welcome to synap!\n'));
|
|
@@ -2061,19 +2056,93 @@ async function main() {
|
|
|
2061
2056
|
|
|
2062
2057
|
console.log('Let\'s get you set up:\n');
|
|
2063
2058
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2059
|
+
// Step 1: Data location (only ask if not already configured)
|
|
2060
|
+
const currentConfig = storage.loadConfig();
|
|
2061
|
+
console.log('[1/4] Data location...');
|
|
2062
|
+
|
|
2063
|
+
if (currentConfig.dataDir) {
|
|
2064
|
+
console.log(` ${chalk.gray('Already configured:')} ${storage.DATA_DIR}`);
|
|
2065
|
+
dataLocationResult.configured = true;
|
|
2066
|
+
dataLocationResult.path = storage.DATA_DIR;
|
|
2067
|
+
} else {
|
|
2068
|
+
const { select, input } = await import('@inquirer/prompts');
|
|
2069
|
+
const dataChoice = await select({
|
|
2070
|
+
message: 'Where should synap store your data?',
|
|
2071
|
+
choices: [
|
|
2072
|
+
{
|
|
2073
|
+
name: `Default (${path.join(os.homedir(), '.config', 'synap')})`,
|
|
2074
|
+
value: 'default',
|
|
2075
|
+
description: 'Recommended for most users'
|
|
2076
|
+
},
|
|
2077
|
+
{
|
|
2078
|
+
name: 'Custom path (for git sync, Dropbox, iCloud)',
|
|
2079
|
+
value: 'custom',
|
|
2080
|
+
description: 'Choose a custom folder for syncing across devices'
|
|
2081
|
+
}
|
|
2082
|
+
]
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
if (dataChoice === 'custom') {
|
|
2086
|
+
const defaultSuggestion = path.join(os.homedir(), 'synap-data');
|
|
2087
|
+
const customPath = await input({
|
|
2088
|
+
message: 'Enter path for synap data:',
|
|
2089
|
+
default: defaultSuggestion,
|
|
2090
|
+
validate: (val) => {
|
|
2091
|
+
if (!val.trim()) return 'Path is required';
|
|
2092
|
+
const expanded = val.replace(/^~/, os.homedir());
|
|
2093
|
+
const parentDir = path.dirname(expanded);
|
|
2094
|
+
if (!fs.existsSync(parentDir)) {
|
|
2095
|
+
return `Parent directory does not exist: ${parentDir}`;
|
|
2096
|
+
}
|
|
2097
|
+
return true;
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
const expanded = customPath.replace(/^~/, os.homedir());
|
|
2102
|
+
|
|
2103
|
+
// Create directory if needed
|
|
2104
|
+
if (!fs.existsSync(expanded)) {
|
|
2105
|
+
fs.mkdirSync(expanded, { recursive: true });
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// Save to config
|
|
2109
|
+
currentConfig.dataDir = customPath;
|
|
2110
|
+
storage.saveConfig(currentConfig);
|
|
2111
|
+
storage.refreshDataDir();
|
|
2112
|
+
|
|
2113
|
+
console.log(` ${chalk.green('✓')} Data directory set to: ${expanded}`);
|
|
2114
|
+
dataLocationResult.configured = true;
|
|
2115
|
+
dataLocationResult.path = expanded;
|
|
2116
|
+
dataLocationResult.custom = true;
|
|
2117
|
+
} else {
|
|
2118
|
+
console.log(` ${chalk.green('✓')} Using default location: ${storage.DATA_DIR}`);
|
|
2119
|
+
dataLocationResult.configured = true;
|
|
2120
|
+
dataLocationResult.path = storage.DATA_DIR;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Step 2: Quick capture test
|
|
2125
|
+
console.log('\n[2/4] Quick capture test...');
|
|
2126
|
+
let createdEntry = null;
|
|
2127
|
+
if (!hasEntries) {
|
|
2128
|
+
createdEntry = await storage.addEntry({
|
|
2129
|
+
content: 'My first thought',
|
|
2130
|
+
type: config.defaultType || 'idea',
|
|
2131
|
+
source: 'setup'
|
|
2132
|
+
});
|
|
2066
2133
|
console.log(` synap add "My first thought"`);
|
|
2067
2134
|
console.log(` ${chalk.green('✓')} Created entry ${createdEntry.id.slice(0, 8)}`);
|
|
2068
2135
|
} else {
|
|
2069
2136
|
console.log(` ${chalk.gray('Existing entries detected, skipping.')}`);
|
|
2070
2137
|
}
|
|
2071
2138
|
|
|
2072
|
-
|
|
2139
|
+
// Step 3: Configuration
|
|
2140
|
+
console.log('\n[3/4] Configuration...');
|
|
2073
2141
|
console.log(` Default type: ${chalk.cyan(config.defaultType || 'idea')}`);
|
|
2074
2142
|
console.log(` Change with: ${chalk.cyan('synap config defaultType todo')}`);
|
|
2075
2143
|
|
|
2076
|
-
|
|
2144
|
+
// Step 4: Claude Code Integration
|
|
2145
|
+
console.log('\n[4/4] Claude Code Integration');
|
|
2077
2146
|
const { confirm } = await import('@inquirer/prompts');
|
|
2078
2147
|
const shouldInstall = await confirm({
|
|
2079
2148
|
message: 'Install Claude skill for AI assistance?',
|
|
@@ -2105,10 +2174,26 @@ async function main() {
|
|
|
2105
2174
|
console.log(` ${chalk.cyan('synap todo "Something to do"')}`);
|
|
2106
2175
|
console.log(` ${chalk.cyan('synap focus')}`);
|
|
2107
2176
|
console.log(` ${chalk.cyan('synap review daily')}`);
|
|
2177
|
+
|
|
2178
|
+
if (dataLocationResult.custom) {
|
|
2179
|
+
console.log(chalk.bold('\n📁 Sync tip:'));
|
|
2180
|
+
console.log(` Your data is stored in: ${chalk.cyan(dataLocationResult.path)}`);
|
|
2181
|
+
console.log(` To sync with git: ${chalk.gray('cd ' + dataLocationResult.path + ' && git init')}`);
|
|
2182
|
+
}
|
|
2108
2183
|
console.log('');
|
|
2109
2184
|
return;
|
|
2110
2185
|
}
|
|
2111
2186
|
|
|
2187
|
+
// JSON mode - minimal interaction
|
|
2188
|
+
let createdEntry = null;
|
|
2189
|
+
if (!hasEntries) {
|
|
2190
|
+
createdEntry = await storage.addEntry({
|
|
2191
|
+
content: 'My first thought',
|
|
2192
|
+
type: config.defaultType || 'idea',
|
|
2193
|
+
source: 'setup'
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2112
2197
|
console.log(JSON.stringify({
|
|
2113
2198
|
success: true,
|
|
2114
2199
|
mode: hasEntries ? 'existing' : 'first-run',
|
|
@@ -2149,7 +2234,15 @@ async function main() {
|
|
|
2149
2234
|
// No key: show all config
|
|
2150
2235
|
if (!key) {
|
|
2151
2236
|
if (options.json) {
|
|
2152
|
-
console.log(JSON.stringify({
|
|
2237
|
+
console.log(JSON.stringify({
|
|
2238
|
+
success: true,
|
|
2239
|
+
config: currentConfig,
|
|
2240
|
+
defaults,
|
|
2241
|
+
paths: {
|
|
2242
|
+
configDir: storage.CONFIG_DIR,
|
|
2243
|
+
dataDir: storage.DATA_DIR
|
|
2244
|
+
}
|
|
2245
|
+
}, null, 2));
|
|
2153
2246
|
} else {
|
|
2154
2247
|
console.log(chalk.bold('Configuration:\n'));
|
|
2155
2248
|
for (const [k, v] of Object.entries(currentConfig)) {
|
|
@@ -2158,6 +2251,9 @@ async function main() {
|
|
|
2158
2251
|
const displayValue = Array.isArray(v) ? v.join(', ') || '(none)' : (v === null ? '(null)' : v);
|
|
2159
2252
|
console.log(` ${chalk.cyan(k)}: ${displayValue}${defaultNote}`);
|
|
2160
2253
|
}
|
|
2254
|
+
console.log(chalk.bold('\nPaths:'));
|
|
2255
|
+
console.log(` ${chalk.cyan('configDir')}: ${storage.CONFIG_DIR}`);
|
|
2256
|
+
console.log(` ${chalk.cyan('dataDir')}: ${storage.DATA_DIR}`);
|
|
2161
2257
|
console.log(chalk.gray('\nUse: synap config <key> <value> to set a value'));
|
|
2162
2258
|
}
|
|
2163
2259
|
return;
|
|
@@ -2201,16 +2297,42 @@ async function main() {
|
|
|
2201
2297
|
parsedValue = value.split(',').map(t => t.trim()).filter(Boolean);
|
|
2202
2298
|
} else if (key === 'editor' && (value === 'null' || value === '')) {
|
|
2203
2299
|
parsedValue = null;
|
|
2300
|
+
} else if (key === 'dataDir') {
|
|
2301
|
+
// Handle dataDir specially
|
|
2302
|
+
if (value === 'null' || value === '') {
|
|
2303
|
+
parsedValue = null;
|
|
2304
|
+
} else {
|
|
2305
|
+
// Expand ~ but keep the ~ in storage for portability
|
|
2306
|
+
const expanded = value.replace(/^~/, require('os').homedir());
|
|
2307
|
+
// Create directory if it doesn't exist
|
|
2308
|
+
if (!fs.existsSync(expanded)) {
|
|
2309
|
+
fs.mkdirSync(expanded, { recursive: true });
|
|
2310
|
+
if (!options.json) {
|
|
2311
|
+
console.log(chalk.gray(`Created directory: ${expanded}`));
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
// Store the original value (with ~ if used)
|
|
2315
|
+
parsedValue = value;
|
|
2316
|
+
}
|
|
2204
2317
|
}
|
|
2205
2318
|
|
|
2206
2319
|
currentConfig[key] = parsedValue;
|
|
2207
2320
|
storage.saveConfig(currentConfig);
|
|
2208
2321
|
|
|
2322
|
+
// If dataDir changed, refresh the storage paths
|
|
2323
|
+
if (key === 'dataDir') {
|
|
2324
|
+
storage.refreshDataDir();
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2209
2327
|
if (options.json) {
|
|
2210
2328
|
console.log(JSON.stringify({ success: true, key, value: parsedValue }));
|
|
2211
2329
|
} else {
|
|
2212
2330
|
const displayValue = Array.isArray(parsedValue) ? parsedValue.join(', ') : parsedValue;
|
|
2213
2331
|
console.log(chalk.green(`Set ${key} = ${displayValue}`));
|
|
2332
|
+
if (key === 'dataDir') {
|
|
2333
|
+
console.log(chalk.gray(`Data will be stored in: ${storage.DATA_DIR}`));
|
|
2334
|
+
console.log(chalk.yellow('\nNote: Restart synap for changes to take full effect.'));
|
|
2335
|
+
}
|
|
2214
2336
|
}
|
|
2215
2337
|
});
|
|
2216
2338
|
|
package/src/deletion-log.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* deletion-log.js - Audit logging for deleted entries, enables restore
|
|
3
|
+
*
|
|
4
|
+
* The deletion log is stored in DATA_DIR (syncable across devices)
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
const fs = require('fs');
|
|
6
8
|
const path = require('path');
|
|
7
|
-
const
|
|
9
|
+
const storage = require('./storage');
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Get deletion log file path (dynamic, based on DATA_DIR)
|
|
13
|
+
*/
|
|
14
|
+
function getDeletionLogPath() {
|
|
15
|
+
return path.join(storage.DATA_DIR, 'deletion-log.json');
|
|
16
|
+
}
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
|
-
* Ensure
|
|
19
|
+
* Ensure data directory exists
|
|
15
20
|
*/
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
function ensureDataDir() {
|
|
22
|
+
const dataDir = storage.DATA_DIR;
|
|
23
|
+
if (!fs.existsSync(dataDir)) {
|
|
24
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -32,12 +38,13 @@ function atomicWriteSync(filePath, data) {
|
|
|
32
38
|
* Load deletion log
|
|
33
39
|
*/
|
|
34
40
|
function loadLog() {
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
ensureDataDir();
|
|
42
|
+
const logFile = getDeletionLogPath();
|
|
43
|
+
if (!fs.existsSync(logFile)) {
|
|
37
44
|
return [];
|
|
38
45
|
}
|
|
39
46
|
try {
|
|
40
|
-
return JSON.parse(fs.readFileSync(
|
|
47
|
+
return JSON.parse(fs.readFileSync(logFile, 'utf8'));
|
|
41
48
|
} catch {
|
|
42
49
|
return [];
|
|
43
50
|
}
|
|
@@ -47,8 +54,8 @@ function loadLog() {
|
|
|
47
54
|
* Save deletion log
|
|
48
55
|
*/
|
|
49
56
|
function saveLog(log) {
|
|
50
|
-
|
|
51
|
-
atomicWriteSync(
|
|
57
|
+
ensureDataDir();
|
|
58
|
+
atomicWriteSync(getDeletionLogPath(), log);
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
/**
|
package/src/preferences.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* preferences.js - User preferences storage and helpers
|
|
3
|
+
*
|
|
4
|
+
* User preferences are stored in DATA_DIR (syncable) not CONFIG_DIR
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
const fs = require('fs');
|
|
@@ -7,7 +9,6 @@ const path = require('path');
|
|
|
7
9
|
const storage = require('./storage');
|
|
8
10
|
|
|
9
11
|
const TEMPLATE_PATH = path.join(__dirname, 'templates', 'user-preferences-template.md');
|
|
10
|
-
const PREFERENCES_FILE = path.join(storage.CONFIG_DIR, 'user-preferences.md');
|
|
11
12
|
const MAX_LINES = 500;
|
|
12
13
|
const SECTION_ALIASES = new Map([
|
|
13
14
|
['about', 'About Me'],
|
|
@@ -21,14 +22,22 @@ const SECTION_ALIASES = new Map([
|
|
|
21
22
|
['behavioral', 'Behavioral Preferences']
|
|
22
23
|
]);
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Get current preferences file path (dynamic, based on DATA_DIR)
|
|
27
|
+
*/
|
|
28
|
+
function getPreferencesFilePath() {
|
|
29
|
+
return path.join(storage.DATA_DIR, 'user-preferences.md');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureDataDir() {
|
|
33
|
+
const dataDir = storage.DATA_DIR;
|
|
34
|
+
if (!fs.existsSync(dataDir)) {
|
|
35
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
function getPreferencesPath() {
|
|
31
|
-
return
|
|
40
|
+
return getPreferencesFilePath();
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
function readTemplate() {
|
|
@@ -62,22 +71,24 @@ function savePreferences(content) {
|
|
|
62
71
|
throw new Error(validation.error);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
const
|
|
74
|
+
ensureDataDir();
|
|
75
|
+
const preferencesFile = getPreferencesFilePath();
|
|
76
|
+
const tmpPath = `${preferencesFile}.tmp`;
|
|
67
77
|
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
68
|
-
fs.renameSync(tmpPath,
|
|
78
|
+
fs.renameSync(tmpPath, preferencesFile);
|
|
69
79
|
return content;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
function loadPreferences() {
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
ensureDataDir();
|
|
84
|
+
const preferencesFile = getPreferencesFilePath();
|
|
85
|
+
if (!fs.existsSync(preferencesFile)) {
|
|
75
86
|
const template = readTemplate();
|
|
76
87
|
savePreferences(template);
|
|
77
88
|
return template;
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
const content = fs.readFileSync(
|
|
91
|
+
const content = fs.readFileSync(preferencesFile, 'utf8');
|
|
81
92
|
const validation = validatePreferences(content);
|
|
82
93
|
if (!validation.valid) {
|
|
83
94
|
throw new Error(validation.error);
|
|
@@ -349,6 +360,6 @@ module.exports = {
|
|
|
349
360
|
appendToSection,
|
|
350
361
|
resetPreferences,
|
|
351
362
|
validatePreferences,
|
|
352
|
-
PREFERENCES_FILE,
|
|
363
|
+
get PREFERENCES_FILE() { return getPreferencesFilePath(); },
|
|
353
364
|
TEMPLATE_PATH
|
|
354
365
|
};
|
package/src/storage.js
CHANGED
|
@@ -7,16 +7,30 @@ const path = require('path');
|
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const { v4: uuidv4 } = require('uuid');
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const ARCHIVE_FILE = path.join(CONFIG_DIR, 'archive.json');
|
|
14
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Directory Resolution (XDG-compliant with backward compatibility)
|
|
12
|
+
// =============================================================================
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the config directory path
|
|
16
|
+
* Priority:
|
|
17
|
+
* 1. SYNAP_CONFIG_DIR env var (explicit override for config only)
|
|
18
|
+
* 2. SYNAP_DIR env var (legacy - affects both config and data)
|
|
19
|
+
* 3. Default: ~/.config/synap
|
|
20
|
+
*/
|
|
21
|
+
function resolveConfigDir() {
|
|
22
|
+
if (process.env.SYNAP_CONFIG_DIR) {
|
|
23
|
+
return process.env.SYNAP_CONFIG_DIR;
|
|
24
|
+
}
|
|
25
|
+
if (process.env.SYNAP_DIR) {
|
|
26
|
+
return process.env.SYNAP_DIR;
|
|
27
|
+
}
|
|
28
|
+
return path.join(os.homedir(), '.config', 'synap');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Config directory - always local, not synced
|
|
32
|
+
const CONFIG_DIR = resolveConfigDir();
|
|
33
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
20
34
|
|
|
21
35
|
/**
|
|
22
36
|
* Ensure config directory exists
|
|
@@ -27,6 +41,90 @@ function ensureConfigDir() {
|
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Load configuration (needed before resolving DATA_DIR)
|
|
46
|
+
*/
|
|
47
|
+
function loadConfigRaw() {
|
|
48
|
+
ensureConfigDir();
|
|
49
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the data directory path
|
|
61
|
+
* Priority:
|
|
62
|
+
* 1. SYNAP_DATA_DIR env var (explicit override for data only)
|
|
63
|
+
* 2. config.json -> dataDir setting
|
|
64
|
+
* 3. SYNAP_DIR env var (legacy - affects both config and data)
|
|
65
|
+
* 4. Default: ~/.local/share/synap (XDG compliant)
|
|
66
|
+
*/
|
|
67
|
+
function resolveDataDir() {
|
|
68
|
+
// 1. Explicit env var override for data
|
|
69
|
+
if (process.env.SYNAP_DATA_DIR) {
|
|
70
|
+
return process.env.SYNAP_DATA_DIR;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Config file setting
|
|
74
|
+
const rawConfig = loadConfigRaw();
|
|
75
|
+
if (rawConfig.dataDir) {
|
|
76
|
+
// Expand ~ to home directory
|
|
77
|
+
const expanded = rawConfig.dataDir.replace(/^~/, os.homedir());
|
|
78
|
+
return expanded;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Legacy env var (affects both config and data)
|
|
82
|
+
if (process.env.SYNAP_DIR) {
|
|
83
|
+
return process.env.SYNAP_DIR;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Default: XDG-compliant data directory
|
|
87
|
+
return path.join(os.homedir(), '.local', 'share', 'synap');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve DATA_DIR at module load time
|
|
91
|
+
// This will be re-evaluated if config changes require restart
|
|
92
|
+
let DATA_DIR = resolveDataDir();
|
|
93
|
+
|
|
94
|
+
// Data files (syncable)
|
|
95
|
+
let ENTRIES_FILE = path.join(DATA_DIR, 'entries.json');
|
|
96
|
+
let ARCHIVE_FILE = path.join(DATA_DIR, 'archive.json');
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Refresh data directory paths (call after config change)
|
|
100
|
+
*/
|
|
101
|
+
function refreshDataDir() {
|
|
102
|
+
DATA_DIR = resolveDataDir();
|
|
103
|
+
ENTRIES_FILE = path.join(DATA_DIR, 'entries.json');
|
|
104
|
+
ARCHIVE_FILE = path.join(DATA_DIR, 'archive.json');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get current data directory path
|
|
109
|
+
*/
|
|
110
|
+
function getDataDir() {
|
|
111
|
+
return DATA_DIR;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Ensure data directory exists
|
|
116
|
+
*/
|
|
117
|
+
function ensureDataDir() {
|
|
118
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
119
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Valid types, statuses, and date formats
|
|
124
|
+
const VALID_TYPES = ['idea', 'project', 'feature', 'todo', 'question', 'reference', 'note'];
|
|
125
|
+
const VALID_STATUSES = ['raw', 'active', 'wip', 'someday', 'done', 'archived'];
|
|
126
|
+
const VALID_DATE_FORMATS = ['relative', 'absolute', 'locale'];
|
|
127
|
+
|
|
30
128
|
/**
|
|
31
129
|
* Atomic file write - write to temp file then rename
|
|
32
130
|
*/
|
|
@@ -40,7 +138,7 @@ function atomicWriteSync(filePath, data) {
|
|
|
40
138
|
* Load entries from file
|
|
41
139
|
*/
|
|
42
140
|
function loadEntries() {
|
|
43
|
-
|
|
141
|
+
ensureDataDir();
|
|
44
142
|
if (!fs.existsSync(ENTRIES_FILE)) {
|
|
45
143
|
return { version: 1, entries: [] };
|
|
46
144
|
}
|
|
@@ -55,7 +153,7 @@ function loadEntries() {
|
|
|
55
153
|
* Save entries to file
|
|
56
154
|
*/
|
|
57
155
|
function saveEntries(data) {
|
|
58
|
-
|
|
156
|
+
ensureDataDir();
|
|
59
157
|
atomicWriteSync(ENTRIES_FILE, data);
|
|
60
158
|
}
|
|
61
159
|
|
|
@@ -63,7 +161,7 @@ function saveEntries(data) {
|
|
|
63
161
|
* Load archived entries
|
|
64
162
|
*/
|
|
65
163
|
function loadArchive() {
|
|
66
|
-
|
|
164
|
+
ensureDataDir();
|
|
67
165
|
if (!fs.existsSync(ARCHIVE_FILE)) {
|
|
68
166
|
return { version: 1, entries: [] };
|
|
69
167
|
}
|
|
@@ -78,7 +176,7 @@ function loadArchive() {
|
|
|
78
176
|
* Save archived entries
|
|
79
177
|
*/
|
|
80
178
|
function saveArchive(data) {
|
|
81
|
-
|
|
179
|
+
ensureDataDir();
|
|
82
180
|
atomicWriteSync(ARCHIVE_FILE, data);
|
|
83
181
|
}
|
|
84
182
|
|
|
@@ -91,7 +189,8 @@ function loadConfig() {
|
|
|
91
189
|
defaultType: 'idea',
|
|
92
190
|
defaultTags: [],
|
|
93
191
|
editor: null, // Falls back to EDITOR env var in CLI
|
|
94
|
-
dateFormat: 'relative'
|
|
192
|
+
dateFormat: 'relative',
|
|
193
|
+
dataDir: null // Custom data directory (null = use default)
|
|
95
194
|
};
|
|
96
195
|
|
|
97
196
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
@@ -123,6 +222,12 @@ function loadConfig() {
|
|
|
123
222
|
.map(t => t.trim());
|
|
124
223
|
}
|
|
125
224
|
|
|
225
|
+
// Validate dataDir if set
|
|
226
|
+
if (config.dataDir !== null && typeof config.dataDir !== 'string') {
|
|
227
|
+
console.warn(`Warning: Invalid dataDir in config. Ignoring.`);
|
|
228
|
+
config.dataDir = null;
|
|
229
|
+
}
|
|
230
|
+
|
|
126
231
|
return config;
|
|
127
232
|
} catch (err) {
|
|
128
233
|
console.warn(`Warning: Could not parse config.json: ${err.message}`);
|
|
@@ -138,7 +243,8 @@ function getDefaultConfig() {
|
|
|
138
243
|
defaultType: 'idea',
|
|
139
244
|
defaultTags: [],
|
|
140
245
|
editor: null,
|
|
141
|
-
dateFormat: 'relative'
|
|
246
|
+
dateFormat: 'relative',
|
|
247
|
+
dataDir: null
|
|
142
248
|
};
|
|
143
249
|
}
|
|
144
250
|
|
|
@@ -170,6 +276,17 @@ function validateConfigValue(key, value) {
|
|
|
170
276
|
case 'editor':
|
|
171
277
|
// Any string or null is valid
|
|
172
278
|
break;
|
|
279
|
+
case 'dataDir':
|
|
280
|
+
// Path validation - must be a valid path string or null
|
|
281
|
+
if (value !== null && value !== 'null' && typeof value === 'string') {
|
|
282
|
+
const expanded = value.replace(/^~/, os.homedir());
|
|
283
|
+
// Check if parent directory exists (we'll create the target if needed)
|
|
284
|
+
const parentDir = path.dirname(expanded);
|
|
285
|
+
if (!fs.existsSync(parentDir)) {
|
|
286
|
+
return { valid: false, error: `Parent directory does not exist: ${parentDir}` };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
173
290
|
}
|
|
174
291
|
return { valid: true };
|
|
175
292
|
}
|
|
@@ -1018,9 +1135,12 @@ module.exports = {
|
|
|
1018
1135
|
validateConfigValue,
|
|
1019
1136
|
saveConfig,
|
|
1020
1137
|
parseDate,
|
|
1138
|
+
refreshDataDir,
|
|
1139
|
+
getDataDir,
|
|
1021
1140
|
CONFIG_DIR,
|
|
1022
|
-
|
|
1023
|
-
|
|
1141
|
+
get DATA_DIR() { return DATA_DIR; },
|
|
1142
|
+
get ENTRIES_FILE() { return ENTRIES_FILE; },
|
|
1143
|
+
get ARCHIVE_FILE() { return ARCHIVE_FILE; },
|
|
1024
1144
|
VALID_TYPES,
|
|
1025
1145
|
VALID_STATUSES,
|
|
1026
1146
|
VALID_DATE_FORMATS
|