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.
@@ -36,7 +36,33 @@ When assisting users with their synap entries:
36
36
 
37
37
  ## User Preferences (Memory)
38
38
 
39
- synap stores long-term user preferences at `~/.config/synap/user-preferences.md`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synap",
3
- "version": "0.7.0",
3
+ "version": "0.8.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
@@ -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
- console.log('[1/3] Quick capture test...');
2065
- if (createdEntry) {
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
- console.log('\n[2/3] Configuration...');
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
- console.log('\n[3/3] Claude Code Integration');
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({ success: true, config: currentConfig, defaults }, null, 2));
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
 
@@ -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 os = require('os');
9
+ const storage = require('./storage');
8
10
 
9
- // Storage directory
10
- const CONFIG_DIR = process.env.SYNAP_DIR || path.join(os.homedir(), '.config', 'synap');
11
- const DELETION_LOG_FILE = path.join(CONFIG_DIR, 'deletion-log.json');
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 config directory exists
19
+ * Ensure data directory exists
15
20
  */
16
- function ensureConfigDir() {
17
- if (!fs.existsSync(CONFIG_DIR)) {
18
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
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
- ensureConfigDir();
36
- if (!fs.existsSync(DELETION_LOG_FILE)) {
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(DELETION_LOG_FILE, 'utf8'));
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
- ensureConfigDir();
51
- atomicWriteSync(DELETION_LOG_FILE, log);
57
+ ensureDataDir();
58
+ atomicWriteSync(getDeletionLogPath(), log);
52
59
  }
53
60
 
54
61
  /**
@@ -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
- function ensureConfigDir() {
25
- if (!fs.existsSync(storage.CONFIG_DIR)) {
26
- fs.mkdirSync(storage.CONFIG_DIR, { recursive: true });
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 PREFERENCES_FILE;
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
- ensureConfigDir();
66
- const tmpPath = `${PREFERENCES_FILE}.tmp`;
74
+ ensureDataDir();
75
+ const preferencesFile = getPreferencesFilePath();
76
+ const tmpPath = `${preferencesFile}.tmp`;
67
77
  fs.writeFileSync(tmpPath, content, 'utf8');
68
- fs.renameSync(tmpPath, PREFERENCES_FILE);
78
+ fs.renameSync(tmpPath, preferencesFile);
69
79
  return content;
70
80
  }
71
81
 
72
82
  function loadPreferences() {
73
- ensureConfigDir();
74
- if (!fs.existsSync(PREFERENCES_FILE)) {
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(PREFERENCES_FILE, 'utf8');
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
- // Storage directory
11
- const CONFIG_DIR = process.env.SYNAP_DIR || path.join(os.homedir(), '.config', 'synap');
12
- const ENTRIES_FILE = path.join(CONFIG_DIR, 'entries.json');
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
- // Valid types, statuses, and date formats
17
- const VALID_TYPES = ['idea', 'project', 'feature', 'todo', 'question', 'reference', 'note'];
18
- const VALID_STATUSES = ['raw', 'active', 'wip', 'someday', 'done', 'archived'];
19
- const VALID_DATE_FORMATS = ['relative', 'absolute', 'locale'];
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
- ensureConfigDir();
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
- ensureConfigDir();
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
- ensureConfigDir();
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
- ensureConfigDir();
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
- ENTRIES_FILE,
1023
- ARCHIVE_FILE,
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