synabun 2026.3.310 → 2026.3.312

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.
@@ -72,8 +72,8 @@ if (process.platform !== 'win32') {
72
72
  }
73
73
  }
74
74
 
75
- // Load .env from parent directory
76
- dotenvConfig({ path: resolve(__dirname, '..', '.env') });
75
+ // Load .env from data home (or parent directory in dev mode)
76
+ dotenvConfig({ path: resolve(process.env.SYNABUN_DATA_HOME || resolve(__dirname, '..'), '.env') });
77
77
 
78
78
  const app = express();
79
79
  const SERVER_BOOT_ID = randomUUID();
@@ -173,12 +173,13 @@ app.use((req, res, next) => {
173
173
  const PORT = process.env.NEURAL_PORT || 3344;
174
174
  // SQLite database is used directly via lib/db.js
175
175
  const EMBEDDING_DIMS = getEmbeddingDims();
176
- const PROJECT_ROOT = resolve(__dirname, '..');
177
- const IMAGES_DIR = resolve(PROJECT_ROOT, 'data', 'images');
176
+ const PACKAGE_ROOT = resolve(__dirname, '..');
177
+ const DATA_HOME = process.env.SYNABUN_DATA_HOME || PACKAGE_ROOT;
178
+ const IMAGES_DIR = resolve(DATA_HOME, 'data', 'images');
178
179
  if (!existsSync(IMAGES_DIR)) mkdirSync(IMAGES_DIR, { recursive: true });
179
180
 
180
181
  // ── Image favorites persistence ──
181
- const IMAGE_FAVORITES_PATH = resolve(PROJECT_ROOT, 'data', 'image-favorites.json');
182
+ const IMAGE_FAVORITES_PATH = resolve(DATA_HOME, 'data', 'image-favorites.json');
182
183
  function loadImageFavorites() {
183
184
  try { return JSON.parse(readFileSync(IMAGE_FAVORITES_PATH, 'utf-8')); }
184
185
  catch { return []; }
@@ -211,8 +212,8 @@ function reloadConfig() {
211
212
  let _reindexJob = null;
212
213
 
213
214
  // --- Custom File Icons ---
214
- const CUSTOM_ICONS_DIR = resolve(PROJECT_ROOT, 'data', 'custom-icons');
215
- const CUSTOM_ICONS_CONFIG = resolve(PROJECT_ROOT, 'data', 'custom-icons.json');
215
+ const CUSTOM_ICONS_DIR = resolve(DATA_HOME, 'data', 'custom-icons');
216
+ const CUSTOM_ICONS_CONFIG = resolve(DATA_HOME, 'data', 'custom-icons.json');
216
217
 
217
218
  function loadCustomIconsConfig() {
218
219
  try { return JSON.parse(readFileSync(CUSTOM_ICONS_CONFIG, 'utf-8')); }
@@ -225,7 +226,7 @@ function saveCustomIconsConfig(cfg) {
225
226
 
226
227
  // --- Category Helpers (per-connection) ---
227
228
 
228
- const CATEGORIES_DATA_DIR = resolve(__dirname, '..', 'mcp-server', 'data');
229
+ const CATEGORIES_DATA_DIR = resolve(DATA_HOME, 'mcp-data');
229
230
  const GLOBAL_CATEGORIES_PATH = resolve(CATEGORIES_DATA_DIR, 'custom-categories.json');
230
231
  const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
231
232
  const CATEGORIES_POINT_ID = '00000000-0000-0000-0000-000000000000';
@@ -303,7 +304,7 @@ function saveCategories(categories) {
303
304
 
304
305
  // --- .env Helpers ---
305
306
 
306
- const ENV_PATH = resolve(__dirname, '..', '.env');
307
+ const ENV_PATH = resolve(DATA_HOME, '.env');
307
308
 
308
309
  function parseEnvFile(filePath) {
309
310
  try {
@@ -434,7 +435,7 @@ app.get('/offline.html', (req, res) => {
434
435
  const html = readFileSync(join(__dirname, 'public', 'offline.html'), 'utf-8');
435
436
  res.type('html').send(html.replace(
436
437
  `const projectDir = localStorage.getItem('synabun-project-dir');`,
437
- `const projectDir = localStorage.getItem('synabun-project-dir') || ${JSON.stringify(PROJECT_ROOT)};`
438
+ `const projectDir = localStorage.getItem('synabun-project-dir') || ${JSON.stringify(DATA_HOME)};`
438
439
  ));
439
440
  });
440
441
 
@@ -1166,7 +1167,7 @@ app.delete('/api/trash/purge', async (req, res) => {
1166
1167
  // Hash a file's content with SHA-256, returns null if unreadable
1167
1168
  function hashFileContent(filePath) {
1168
1169
  try {
1169
- const absPath = resolve(__dirname, '..', filePath);
1170
+ const absPath = resolve(PACKAGE_ROOT, filePath);
1170
1171
  const content = readFileSync(absPath);
1171
1172
  return createHash('sha256').update(content).digest('hex');
1172
1173
  } catch {
@@ -1725,7 +1726,7 @@ app.post('/api/settings/reindex/cancel', (req, res) => {
1725
1726
 
1726
1727
  // --- Display Settings Routes (MCP response control) ---
1727
1728
 
1728
- const DISPLAY_SETTINGS_PATH = resolve(__dirname, '..', 'mcp-server', 'data', 'display-settings.json');
1729
+ const DISPLAY_SETTINGS_PATH = resolve(DATA_HOME, 'mcp-data', 'display-settings.json');
1729
1730
 
1730
1731
  function loadDisplaySettings() {
1731
1732
  try {
@@ -1767,7 +1768,7 @@ app.put('/api/display-settings', (req, res) => {
1767
1768
  // ═══════════════════════════════════════════
1768
1769
 
1769
1770
  const SKINS_DIR = join(__dirname, 'skins');
1770
- const SKIN_CONFIG_PATH = resolve(PROJECT_ROOT, 'data', 'skin-config.json');
1771
+ const SKIN_CONFIG_PATH = resolve(DATA_HOME, 'data', 'skin-config.json');
1771
1772
  const SKIN_ID_RE = /^[a-z][a-z0-9-]*$/;
1772
1773
 
1773
1774
  function loadSkinConfig() {
@@ -2214,7 +2215,7 @@ function handleClaudeSkinWebSocket(ws) {
2214
2215
  // ── Auto-compact early detection via pending-compact file watcher ──
2215
2216
  // Pre-compact hook writes data/pending-compact/{sessionId}.json BEFORE compaction.
2216
2217
  // Watch for new files and send compact_started event to UI immediately.
2217
- const _pendingCompactDir = resolve(PROJECT_ROOT, 'data', 'pending-compact');
2218
+ const _pendingCompactDir = resolve(DATA_HOME, 'data', 'pending-compact');
2218
2219
  let _compactWatcher = null;
2219
2220
  const _seenCompactFiles = new Set();
2220
2221
  try {
@@ -2262,7 +2263,7 @@ function handleClaudeSkinWebSocket(ws) {
2262
2263
 
2263
2264
  function spawnProc(sessionId, model, cwd, effort, prompt) {
2264
2265
  killProc();
2265
- const workDir = validateWorkDir(cwd) || PROJECT_ROOT;
2266
+ const workDir = validateWorkDir(cwd) || PACKAGE_ROOT;
2266
2267
  const args = [
2267
2268
  '--print',
2268
2269
  '--output-format', 'stream-json',
@@ -2891,11 +2892,11 @@ function handleClaudeSkinWebSocket(ws) {
2891
2892
  app.get('/api/claude/config', (req, res) => {
2892
2893
  try {
2893
2894
  const cliCfg = (() => {
2894
- try { return JSON.parse(readFileSync(resolve(PROJECT_ROOT, 'data', 'cli-config.json'), 'utf-8')); }
2895
+ try { return JSON.parse(readFileSync(resolve(DATA_HOME, 'data', 'cli-config.json'), 'utf-8')); }
2895
2896
  catch { return {}; }
2896
2897
  })();
2897
2898
  const projects = (() => {
2898
- try { return JSON.parse(readFileSync(resolve(PROJECT_ROOT, 'data', 'claude-code-projects.json'), 'utf-8')); }
2899
+ try { return JSON.parse(readFileSync(resolve(DATA_HOME, 'data', 'claude-code-projects.json'), 'utf-8')); }
2899
2900
  catch { return []; }
2900
2901
  })();
2901
2902
  const models = [
@@ -2914,7 +2915,7 @@ app.get('/api/claude/config', (req, res) => {
2914
2915
 
2915
2916
  // --- Claude Skin Cost Tracking ---
2916
2917
 
2917
- const COST_PATH = resolve(__dirname, '..', 'data', 'cost-tracking.json');
2918
+ const COST_PATH = resolve(DATA_HOME, 'data', 'cost-tracking.json');
2918
2919
 
2919
2920
  function loadCostData() {
2920
2921
  try { return JSON.parse(readFileSync(COST_PATH, 'utf-8')); }
@@ -3175,7 +3176,7 @@ app.get('/api/claude-skin/session-locks', (req, res) => {
3175
3176
 
3176
3177
  // --- Keybinds Persistence ---
3177
3178
 
3178
- const KEYBINDS_PATH = resolve(__dirname, '..', 'data', 'keybinds.json');
3179
+ const KEYBINDS_PATH = resolve(DATA_HOME, 'data', 'keybinds.json');
3179
3180
 
3180
3181
  function loadKeybinds() {
3181
3182
  try { return JSON.parse(readFileSync(KEYBINDS_PATH, 'utf-8')); }
@@ -3213,7 +3214,7 @@ app.put('/api/keybinds', (req, res) => {
3213
3214
 
3214
3215
  // --- UI State Persistence (replaces browser localStorage) ---
3215
3216
 
3216
- const UI_STATE_PATH = resolve(__dirname, '..', 'data', 'ui-state.json');
3217
+ const UI_STATE_PATH = resolve(DATA_HOME, 'data', 'ui-state.json');
3217
3218
 
3218
3219
  function loadUiState() {
3219
3220
  try {
@@ -4332,9 +4333,9 @@ app.get('/api/connections', (req, res) => {
4332
4333
  // LOOP TEMPLATES
4333
4334
  // ═══════════════════════════════════════════
4334
4335
 
4335
- const LOOP_TEMPLATES_PATH = resolve(PROJECT_ROOT, 'data', 'loop-templates.json');
4336
- const LOOP_SCHEDULES_PATH = resolve(PROJECT_ROOT, 'data', 'loop-schedules.json');
4337
- const LOOP_DIR = resolve(PROJECT_ROOT, 'data', 'loop');
4336
+ const LOOP_TEMPLATES_PATH = resolve(DATA_HOME, 'data', 'loop-templates.json');
4337
+ const LOOP_SCHEDULES_PATH = resolve(DATA_HOME, 'data', 'loop-schedules.json');
4338
+ const LOOP_DIR = resolve(DATA_HOME, 'data', 'loop');
4338
4339
 
4339
4340
  function readLoopTemplates() {
4340
4341
  try {
@@ -4646,7 +4647,7 @@ app.post('/api/loop/launch', async (req, res) => {
4646
4647
  // Default CWD to Synabun project root — it has no .mcp.json, so no auth-requiring
4647
4648
  // MCP servers (e.g. supabase OAuth) trigger the "needs auth" prompt that blocks loops.
4648
4649
  // The SynaBun MCP server is registered globally in ~/.claude.json, so it's always available.
4649
- const loopCwd = cwd || PROJECT_ROOT;
4650
+ const loopCwd = cwd || PACKAGE_ROOT;
4650
4651
  // Pre-generate terminal session ID so we can inject it into the PTY environment.
4651
4652
  // SYNABUN_TERMINAL_SESSION lets hooks correlate with the correct loop file
4652
4653
  // after /clear changes the Claude session ID. This is the key to multi-loop isolation.
@@ -4953,7 +4954,7 @@ async function launchScheduledLoop(schedule) {
4953
4954
  }
4954
4955
  }
4955
4956
 
4956
- const terminalSessionId = createTerminalSession(cliProfile, 120, 30, PROJECT_ROOT, { model: cliModel, extraEnv: schedExtraEnv });
4957
+ const terminalSessionId = createTerminalSession(cliProfile, 120, 30, PACKAGE_ROOT, { model: cliModel, extraEnv: schedExtraEnv });
4957
4958
 
4958
4959
  const pendingId = 'pending-' + randomBytes(8).toString('hex');
4959
4960
  const loopState = {
@@ -5417,9 +5418,9 @@ const AGENT_IS_WIN = process.platform === 'win32';
5417
5418
  */
5418
5419
  function buildAgentMcpConfig(withSynabun, browserSessionId) {
5419
5420
  if (!withSynabun) return '{"mcpServers":{}}';
5420
- const mcpPreload = resolve(PROJECT_ROOT, 'mcp-server', 'dist', 'preload.js');
5421
- const dotenvPath = resolve(PROJECT_ROOT, '.env');
5422
- const env = { DOTENV_PATH: dotenvPath };
5421
+ const mcpPreload = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'preload.js');
5422
+ const dotenvPath = resolve(DATA_HOME, '.env');
5423
+ const env = { DOTENV_PATH: dotenvPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') };
5423
5424
  if (browserSessionId) env.SYNABUN_BROWSER_SESSION = browserSessionId;
5424
5425
  return JSON.stringify({
5425
5426
  mcpServers: {
@@ -5568,7 +5569,7 @@ async function launchAgentController(opts) {
5568
5569
 
5569
5570
  const agentId = randomUUID();
5570
5571
  const mcpConfig = buildAgentMcpConfig(withSynabun, browserSessionId);
5571
- const agentCwd = cwd || PROJECT_ROOT;
5572
+ const agentCwd = cwd || PACKAGE_ROOT;
5572
5573
 
5573
5574
  // Build default system prompt for agents — overrides CLAUDE.md boot sequence
5574
5575
  // so the agent goes straight to the task instead of greeting/recalling
@@ -6032,7 +6033,7 @@ let _npmUpdateCache = { current: null, latest: null, updateAvailable: false, che
6032
6033
 
6033
6034
  async function checkNpmUpdate() {
6034
6035
  try {
6035
- const pkgPath = resolve(PROJECT_ROOT, 'package.json');
6036
+ const pkgPath = resolve(PACKAGE_ROOT, 'package.json');
6036
6037
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
6037
6038
  const current = pkg.version;
6038
6039
  _npmUpdateCache.current = current;
@@ -6062,7 +6063,7 @@ async function checkNpmUpdate() {
6062
6063
  } catch (err) {
6063
6064
  if (!_npmUpdateCache.current) {
6064
6065
  try {
6065
- const pkg = JSON.parse(readFileSync(resolve(PROJECT_ROOT, 'package.json'), 'utf-8'));
6066
+ const pkg = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8'));
6066
6067
  _npmUpdateCache.current = pkg.version;
6067
6068
  } catch {}
6068
6069
  }
@@ -6157,7 +6158,7 @@ app.get('/api/system/backup', async (req, res) => {
6157
6158
  addFile('env.bak', ENV_PATH);
6158
6159
 
6159
6160
  // 2. data/ directory
6160
- const dataDir = resolve(PROJECT_ROOT, 'data');
6161
+ const dataDir = resolve(DATA_HOME, 'data');
6161
6162
  for (const f of ['ui-state.json', 'greeting-config.json', 'hook-features.json',
6162
6163
  'claude-code-projects.json', 'mcp-api-key.json', 'keybinds.json', 'cli-config.json',
6163
6164
  'invite-key.json', 'invite-proxy.json', 'invite-permissions.json',
@@ -6216,7 +6217,7 @@ app.get('/api/system/backup', async (req, res) => {
6216
6217
  }
6217
6218
  }
6218
6219
 
6219
- // 6. Bundled SynaBun skills (PROJECT_ROOT/skills/) — full directory tree
6220
+ // 6. Bundled SynaBun skills (PACKAGE_ROOT/skills/) — full directory tree
6220
6221
  if (existsSync(SKILLS_SOURCE_DIR)) {
6221
6222
  for (const entry of readdirSync(SKILLS_SOURCE_DIR, { withFileTypes: true })) {
6222
6223
  if (!isDirEntry(entry)) continue;
@@ -6321,7 +6322,7 @@ app.post('/api/system/restore',
6321
6322
  }
6322
6323
 
6323
6324
  // data/ files
6324
- const dataDir = resolve(PROJECT_ROOT, 'data');
6325
+ const dataDir = resolve(DATA_HOME, 'data');
6325
6326
  mkdirSync(dataDir, { recursive: true });
6326
6327
  for (const subdir of ['pending-remember', 'pending-compact', 'loop']) {
6327
6328
  mkdirSync(resolve(dataDir, subdir), { recursive: true });
@@ -6335,7 +6336,7 @@ app.post('/api/system/restore',
6335
6336
  const rel = entry.entryName.replace(`${prefix}/`, '');
6336
6337
 
6337
6338
  if (rel.startsWith('data/') && rel !== 'data/') {
6338
- const target = resolve(PROJECT_ROOT, rel);
6339
+ const target = resolve(DATA_HOME, rel);
6339
6340
  const dir = dirname(target);
6340
6341
  mkdirSync(dir, { recursive: true });
6341
6342
  writeFileSync(target, entry.getData());
@@ -6366,7 +6367,7 @@ app.post('/api/system/restore',
6366
6367
  results.files.push(rel);
6367
6368
  }
6368
6369
 
6369
- // Bundled SynaBun skills → PROJECT_ROOT/skills/
6370
+ // Bundled SynaBun skills → PACKAGE_ROOT/skills/
6370
6371
  if (rel.startsWith('bundled-skills/') && rel !== 'bundled-skills/') {
6371
6372
  const subPath = rel.replace('bundled-skills/', '');
6372
6373
  const target = resolve(SKILLS_SOURCE_DIR, subPath);
@@ -6442,6 +6443,22 @@ app.post('/api/system/restore',
6442
6443
  }
6443
6444
  }
6444
6445
 
6446
+ // Post-restore: ensure MCP server is built (critical for fresh installs + restore)
6447
+ const mcpDistCheck = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js');
6448
+ if (!existsSync(mcpDistCheck)) {
6449
+ try {
6450
+ // Ensure dev deps are installed for TypeScript
6451
+ const mcpDir = resolve(PACKAGE_ROOT, 'mcp-server');
6452
+ if (!existsSync(resolve(mcpDir, 'node_modules', 'typescript'))) {
6453
+ execSync('npm install', { cwd: mcpDir, stdio: 'pipe', timeout: 180_000, env: { ...process.env, NODE_ENV: 'development' } });
6454
+ }
6455
+ execSync('npm run build', { cwd: mcpDir, stdio: 'pipe', timeout: 60_000 });
6456
+ results.files.push('(auto-built MCP server)');
6457
+ } catch (buildErr) {
6458
+ results.errors.push(`MCP server auto-build failed: ${buildErr.message}`);
6459
+ }
6460
+ }
6461
+
6445
6462
  res.json({ ok: true, message: 'Backup restored successfully', results });
6446
6463
  } catch (err) {
6447
6464
  console.error('POST /api/system/restore error:', err.message);
@@ -6453,7 +6470,7 @@ app.post('/api/system/restore',
6453
6470
  // AUTO-BACKUP SYSTEM
6454
6471
  // ═══════════════════════════════════════════
6455
6472
 
6456
- const AUTO_BACKUP_CONFIG_PATH = resolve(PROJECT_ROOT, 'data', 'auto-backup-config.json');
6473
+ const AUTO_BACKUP_CONFIG_PATH = resolve(DATA_HOME, 'data', 'auto-backup-config.json');
6457
6474
 
6458
6475
  function loadAutoBackupConfig() {
6459
6476
  try {
@@ -6523,7 +6540,7 @@ function buildBackupZipToFile(destPath) {
6523
6540
  // Same content as manual backup
6524
6541
  addFile('env.bak', ENV_PATH);
6525
6542
 
6526
- const dataDir = resolve(PROJECT_ROOT, 'data');
6543
+ const dataDir = resolve(DATA_HOME, 'data');
6527
6544
  for (const f of ['ui-state.json', 'greeting-config.json', 'hook-features.json',
6528
6545
  'claude-code-projects.json', 'mcp-api-key.json', 'keybinds.json', 'cli-config.json',
6529
6546
  'invite-key.json', 'invite-proxy.json', 'invite-permissions.json',
@@ -6712,8 +6729,8 @@ function validateProjectPath(filePath) {
6712
6729
  const normalized = resolve(filePath);
6713
6730
  const registeredProjects = loadHookProjects();
6714
6731
  const claudePlansDir = resolve(join(process.env.USERPROFILE || process.env.HOME || '', '.claude', 'plans'));
6715
- const localPlansDir = resolve(join(PROJECT_ROOT, 'data', 'plans'));
6716
- const allowedRoots = [resolve(PROJECT_ROOT), ...registeredProjects.map(p => resolve(p.path)), localPlansDir, claudePlansDir];
6732
+ const localPlansDir = resolve(join(DATA_HOME, 'data', 'plans'));
6733
+ const allowedRoots = [resolve(DATA_HOME), ...registeredProjects.map(p => resolve(p.path)), localPlansDir, claudePlansDir];
6717
6734
  const matched = allowedRoots.find(r => normalized === r || normalized.startsWith(r + sep));
6718
6735
  return matched ? normalized : null;
6719
6736
  }
@@ -6829,7 +6846,7 @@ app.delete('/api/file-content', (req, res) => {
6829
6846
  // GET /api/latest-plan — Find most recently modified plan file (data/plans/ preferred, ~/.claude/plans/ fallback)
6830
6847
  app.get('/api/latest-plan', (req, res) => {
6831
6848
  try {
6832
- const localPlansDir = join(PROJECT_ROOT, 'data', 'plans');
6849
+ const localPlansDir = join(DATA_HOME, 'data', 'plans');
6833
6850
  const legacyPlansDir = join(process.env.USERPROFILE || process.env.HOME || '', '.claude', 'plans');
6834
6851
  const maxAge = 24 * 60 * 60 * 1000; // 24 hours
6835
6852
  const now = Date.now();
@@ -6860,7 +6877,7 @@ app.post('/api/create-plan', (req, res) => {
6860
6877
  const { content } = req.body;
6861
6878
  if (!content || typeof content !== 'string') return res.status(400).json({ error: 'Missing content' });
6862
6879
 
6863
- const plansDir = join(PROJECT_ROOT, 'data', 'plans');
6880
+ const plansDir = join(DATA_HOME, 'data', 'plans');
6864
6881
  if (!existsSync(plansDir)) mkdirSync(plansDir, { recursive: true });
6865
6882
 
6866
6883
  const name = `synabun-plan-${Date.now()}.md`;
@@ -6875,7 +6892,7 @@ app.post('/api/create-plan', (req, res) => {
6875
6892
  // GET /api/project-files — List ALL files (incl. dotfiles) for file explorer sidebar
6876
6893
  app.get('/api/project-files', (req, res) => {
6877
6894
  try {
6878
- const root = PROJECT_ROOT;
6895
+ const root = DATA_HOME;
6879
6896
  let dir = req.query.path ? resolve(req.query.path) : root;
6880
6897
  const search = (req.query.search || '').trim();
6881
6898
 
@@ -7272,7 +7289,7 @@ app.get('/api/setup/onboarding', async (req, res) => {
7272
7289
  const vars = parseEnvFile(ENV_PATH);
7273
7290
  const setupComplete = vars.SETUP_COMPLETE === 'true';
7274
7291
  const dbExists = existsSync(getDbPath());
7275
- const mcpBuilt = existsSync(resolve(PROJECT_ROOT, 'mcp-server', 'dist', 'index.js'));
7292
+ const mcpBuilt = existsSync(resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js'));
7276
7293
 
7277
7294
  res.json({
7278
7295
  setupComplete,
@@ -7280,7 +7297,7 @@ app.get('/api/setup/onboarding', async (req, res) => {
7280
7297
  dbExists,
7281
7298
  embedding: 'local',
7282
7299
  mcpBuilt,
7283
- projectDir: PROJECT_ROOT,
7300
+ projectDir: DATA_HOME,
7284
7301
  platform: process.platform,
7285
7302
  defaultDbDir: dirname(getDbPath()),
7286
7303
  });
@@ -7333,14 +7350,21 @@ app.post('/api/setup/build', async (req, res) => {
7333
7350
  }
7334
7351
 
7335
7352
  const dirs = [
7336
- { name: 'Neural Interface', path: resolve(PROJECT_ROOT, 'neural-interface') },
7337
- { name: 'MCP Server', path: resolve(PROJECT_ROOT, 'mcp-server') },
7353
+ { name: 'Neural Interface', path: resolve(PACKAGE_ROOT, 'neural-interface') },
7354
+ { name: 'MCP Server', path: resolve(PACKAGE_ROOT, 'mcp-server') },
7338
7355
  ];
7339
7356
 
7357
+ // Check if MCP server needs building (determines if we need dev deps)
7358
+ const mcpDist = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js');
7359
+ const mcpNeedsBuild = !existsSync(mcpDist);
7360
+
7340
7361
  // Phase 1: npm install for each directory
7341
7362
  for (const dir of dirs) {
7342
7363
  const hasModules = existsSync(resolve(dir.path, 'node_modules'));
7343
- if (hasModules) {
7364
+ // If MCP server needs building, reinstall with dev deps even if node_modules exists
7365
+ const isMcp = dir.name === 'MCP Server';
7366
+ const needsDevDeps = isMcp && mcpNeedsBuild && !existsSync(resolve(dir.path, 'node_modules', 'typescript'));
7367
+ if (hasModules && !needsDevDeps) {
7344
7368
  send('install', { dir: dir.name, status: 'skipped', output: 'node_modules already present' });
7345
7369
  continue;
7346
7370
  }
@@ -7361,8 +7385,7 @@ app.post('/api/setup/build', async (req, res) => {
7361
7385
  }
7362
7386
 
7363
7387
  // Phase 2: Build MCP server
7364
- const mcpDist = resolve(PROJECT_ROOT, 'mcp-server', 'dist', 'index.js');
7365
- const mcpSrc = resolve(PROJECT_ROOT, 'mcp-server', 'src', 'index.ts');
7388
+ const mcpSrc = resolve(PACKAGE_ROOT, 'mcp-server', 'src', 'index.ts');
7366
7389
  let needsBuild = !existsSync(mcpDist);
7367
7390
  if (!needsBuild) {
7368
7391
  try { needsBuild = statSync(mcpSrc).mtimeMs > statSync(mcpDist).mtimeMs; } catch { needsBuild = true; }
@@ -7374,7 +7397,7 @@ app.post('/api/setup/build', async (req, res) => {
7374
7397
  send('build', { status: 'start' });
7375
7398
  try {
7376
7399
  const { stdout, stderr } = await execAsync('npm run build', {
7377
- cwd: resolve(PROJECT_ROOT, 'mcp-server'),
7400
+ cwd: resolve(PACKAGE_ROOT, 'mcp-server'),
7378
7401
  timeout: 60_000,
7379
7402
  });
7380
7403
  send('build', { status: 'done', output: (stdout || '').slice(-500) });
@@ -7402,13 +7425,13 @@ app.post('/api/setup/write-mcp-json', (req, res) => {
7402
7425
  const { targetDir } = req.body;
7403
7426
  if (!targetDir) return res.status(400).json({ error: 'targetDir required' });
7404
7427
 
7405
- const mcpIndexPath = resolve(PROJECT_ROOT, 'mcp-server', 'run.mjs').replace(/\\/g, '/');
7406
- const envPath = resolve(PROJECT_ROOT, '.env').replace(/\\/g, '/');
7428
+ const mcpIndexPath = resolve(PACKAGE_ROOT, 'mcp-server', 'run.mjs').replace(/\\/g, '/');
7429
+ const envPath = resolve(DATA_HOME, '.env').replace(/\\/g, '/');
7407
7430
 
7408
7431
  const mcpEntry = {
7409
7432
  command: 'node',
7410
7433
  args: [mcpIndexPath],
7411
- env: { DOTENV_PATH: envPath },
7434
+ env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
7412
7435
  };
7413
7436
 
7414
7437
  const targetPath = resolve(targetDir, '.mcp.json');
@@ -7506,7 +7529,7 @@ function writeClaudeSettings(filePath, data) {
7506
7529
  }
7507
7530
 
7508
7531
  function hookCommandString(scriptName) {
7509
- const scriptPath = resolve(PROJECT_ROOT, 'hooks', 'claude-code', scriptName).replace(/\\/g, '/');
7532
+ const scriptPath = resolve(PACKAGE_ROOT, 'hooks', 'claude-code', scriptName).replace(/\\/g, '/');
7510
7533
  return `node "${scriptPath}"`;
7511
7534
  }
7512
7535
 
@@ -7786,7 +7809,7 @@ function removeHookFromSettings(settings, onlyEvent) {
7786
7809
  }
7787
7810
 
7788
7811
  // Persistent list of project paths the user has registered
7789
- const HOOK_PROJECTS_PATH = resolve(PROJECT_ROOT, 'data', 'claude-code-projects.json');
7812
+ const HOOK_PROJECTS_PATH = resolve(DATA_HOME, 'data', 'claude-code-projects.json');
7790
7813
 
7791
7814
  function loadHookProjects() {
7792
7815
  try {
@@ -7893,7 +7916,7 @@ function countSessionMessages(filePath) {
7893
7916
  // ── Session metadata cache ──
7894
7917
  // Persists discovered session metadata so entries survive even after
7895
7918
  // Claude Code deletes old JSONL files from disk.
7896
- const SESSION_CACHE_DIR = resolve(PROJECT_ROOT, 'data');
7919
+ const SESSION_CACHE_DIR = resolve(DATA_HOME, 'data');
7897
7920
  const _sessionCacheMap = new Map(); // projDir → Map(sessionId → entry)
7898
7921
 
7899
7922
  function getSessionCachePath(projDir) {
@@ -8291,7 +8314,7 @@ app.post('/api/session-indexing/mirror', async (req, res) => {
8291
8314
  app.get('/api/claude-code/integrations', (req, res) => {
8292
8315
  try {
8293
8316
  const hookExists = HOOK_SCRIPTS.every(def =>
8294
- existsSync(resolve(PROJECT_ROOT, 'hooks', 'claude-code', def.script))
8317
+ existsSync(resolve(PACKAGE_ROOT, 'hooks', 'claude-code', def.script))
8295
8318
  );
8296
8319
  const projects = loadHookProjects();
8297
8320
 
@@ -8347,8 +8370,8 @@ app.get('/api/claude-code/integrations', (req, res) => {
8347
8370
  mcpConnected = !!(cj.mcpServers && cj.mcpServers.SynaBun);
8348
8371
  } catch {}
8349
8372
 
8350
- const mcpIndexPath = resolve(PROJECT_ROOT, 'mcp-server', 'run.mjs').replace(/\\/g, '/');
8351
- const envPath = resolve(PROJECT_ROOT, '.env').replace(/\\/g, '/');
8373
+ const mcpIndexPath = resolve(PACKAGE_ROOT, 'mcp-server', 'run.mjs').replace(/\\/g, '/');
8374
+ const envPath = resolve(DATA_HOME, '.env').replace(/\\/g, '/');
8352
8375
  const cliCommand = `claude mcp add SynaBun -s user -e DOTENV_PATH="${envPath}" -- node "${mcpIndexPath}"`;
8353
8376
 
8354
8377
  res.json({
@@ -8413,7 +8436,7 @@ app.post('/api/claude-code/integrations', (req, res) => {
8413
8436
  // Auto-create greeting config entry for the new project
8414
8437
  try {
8415
8438
  const greetingKey = projectLabel.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
8416
- const gcPath = resolve(PROJECT_ROOT, 'data', 'greeting-config.json');
8439
+ const gcPath = resolve(DATA_HOME, 'data', 'greeting-config.json');
8417
8440
  let gc = { version: 1, defaults: {}, projects: {}, global: {} };
8418
8441
  try { if (existsSync(gcPath)) gc = JSON.parse(readFileSync(gcPath, 'utf-8')); } catch {}
8419
8442
  if (!gc.projects) gc.projects = {};
@@ -8498,7 +8521,7 @@ app.delete('/api/claude-code/projects/:index', (req, res) => {
8498
8521
 
8499
8522
  // ── Hook Features (feature flags read by hook scripts) ──
8500
8523
 
8501
- const HOOK_FEATURES_PATH = resolve(PROJECT_ROOT, 'data', 'hook-features.json');
8524
+ const HOOK_FEATURES_PATH = resolve(DATA_HOME, 'data', 'hook-features.json');
8502
8525
 
8503
8526
  function loadHookFeatures() {
8504
8527
  try {
@@ -8652,7 +8675,7 @@ app.put('/api/claude-code/tool-permissions', (req, res) => {
8652
8675
 
8653
8676
  // ── Greeting Config (read/write greeting-config.json for the greeting hook) ──
8654
8677
 
8655
- const GREETING_CONFIG_PATH = resolve(PROJECT_ROOT, 'data', 'greeting-config.json');
8678
+ const GREETING_CONFIG_PATH = resolve(DATA_HOME, 'data', 'greeting-config.json');
8656
8679
 
8657
8680
  function loadGreetingConfig() {
8658
8681
  try {
@@ -8773,8 +8796,8 @@ app.post('/api/claude-code/mcp', (req, res) => {
8773
8796
  try {
8774
8797
  const home = process.env.USERPROFILE || process.env.HOME || '';
8775
8798
  const claudeJsonPath = join(home, '.claude.json');
8776
- const mcpIndexPath = resolve(PROJECT_ROOT, 'mcp-server', 'run.mjs').replace(/\\/g, '/');
8777
- const envPath = resolve(PROJECT_ROOT, '.env').replace(/\\/g, '/');
8799
+ const mcpIndexPath = resolve(PACKAGE_ROOT, 'mcp-server', 'run.mjs').replace(/\\/g, '/');
8800
+ const envPath = resolve(DATA_HOME, '.env').replace(/\\/g, '/');
8778
8801
 
8779
8802
  let data = {};
8780
8803
  try { data = JSON.parse(readFileSync(claudeJsonPath, 'utf-8')); } catch {}
@@ -8783,7 +8806,7 @@ app.post('/api/claude-code/mcp', (req, res) => {
8783
8806
  type: 'stdio',
8784
8807
  command: 'node',
8785
8808
  args: [mcpIndexPath],
8786
- env: { DOTENV_PATH: envPath },
8809
+ env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
8787
8810
  };
8788
8811
  writeFileSync(claudeJsonPath, JSON.stringify(data, null, 2), 'utf-8');
8789
8812
 
@@ -8843,8 +8866,8 @@ function tomlRemoveSection(content, section) {
8843
8866
  // ── Shared MCP path helpers ──
8844
8867
 
8845
8868
  function getMcpPaths() {
8846
- const mcpIndexPath = resolve(PROJECT_ROOT, 'mcp-server', 'dist', 'preload.js').replace(/\\/g, '/');
8847
- const envPath = resolve(PROJECT_ROOT, '.env').replace(/\\/g, '/');
8869
+ const mcpIndexPath = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'preload.js').replace(/\\/g, '/');
8870
+ const envPath = resolve(DATA_HOME, '.env').replace(/\\/g, '/');
8848
8871
  return { mcpIndexPath, envPath };
8849
8872
  }
8850
8873
 
@@ -8878,7 +8901,7 @@ app.post('/api/setup/gemini/mcp', (req, res) => {
8878
8901
  data.mcpServers.SynaBun = {
8879
8902
  command: 'node',
8880
8903
  args: [mcpIndexPath],
8881
- env: { DOTENV_PATH: envPath },
8904
+ env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
8882
8905
  };
8883
8906
  writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
8884
8907
  res.json({ ok: true, message: 'SynaBun MCP registered in Gemini CLI. Restart Gemini to connect.' });
@@ -8928,7 +8951,7 @@ app.post('/api/setup/codex/mcp', (req, res) => {
8928
8951
  content = tomlUpsertSection(content, 'mcp_servers.SynaBun', {
8929
8952
  command: 'node',
8930
8953
  args: [mcpIndexPath],
8931
- env: { DOTENV_PATH: envPath },
8954
+ env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
8932
8955
  });
8933
8956
  writeFileSync(configPath, content, 'utf-8');
8934
8957
  res.json({ ok: true, message: 'SynaBun MCP registered in Codex CLI. Restart Codex to connect.' });
@@ -8990,7 +9013,7 @@ app.get('/api/setup/status', (req, res) => {
8990
9013
  // Claude Code Skill Installation
8991
9014
  // ═══════════════════════════════════════════
8992
9015
 
8993
- const SKILLS_SOURCE_DIR = resolve(PROJECT_ROOT, 'skills');
9016
+ const SKILLS_SOURCE_DIR = resolve(PACKAGE_ROOT, 'skills');
8994
9017
 
8995
9018
  /** Dirent.isDirectory() returns false for symlinks — check both */
8996
9019
  function isDirEntry(entry) {
@@ -9019,7 +9042,7 @@ function listAvailableSkills() {
9019
9042
  const skills = [];
9020
9043
  const seen = new Set();
9021
9044
 
9022
- // 1. Bundled skills (PROJECT_ROOT/skills/)
9045
+ // 1. Bundled skills (PACKAGE_ROOT/skills/)
9023
9046
  if (existsSync(SKILLS_SOURCE_DIR)) {
9024
9047
  for (const entry of readdirSync(SKILLS_SOURCE_DIR, { withFileTypes: true })) {
9025
9048
  if (!isDirEntry(entry)) continue;
@@ -9865,7 +9888,7 @@ app.get('/api/health', (req, res) => {
9865
9888
  // getDb() auto-creates the directory, file, and schema if missing
9866
9889
  getDb();
9867
9890
  const count = countMemories();
9868
- res.json({ ok: true, storage: 'sqlite', memories: count, projectDir: PROJECT_ROOT });
9891
+ res.json({ ok: true, storage: 'sqlite', memories: count, projectDir: DATA_HOME });
9869
9892
  } catch (err) {
9870
9893
  res.json({ ok: false, reason: 'db_error', detail: err.message });
9871
9894
  }
@@ -9877,7 +9900,7 @@ app.post('/api/health/start', (req, res) => {
9877
9900
  });
9878
9901
 
9879
9902
  // --- MCP API Key Management ---
9880
- const MCP_KEY_PATH = resolve(PROJECT_ROOT, 'data', 'mcp-api-key.json');
9903
+ const MCP_KEY_PATH = resolve(DATA_HOME, 'data', 'mcp-api-key.json');
9881
9904
  let activeMcpKey = null;
9882
9905
 
9883
9906
  function loadMcpKey() {
@@ -9893,7 +9916,7 @@ function loadMcpKey() {
9893
9916
  }
9894
9917
 
9895
9918
  function saveMcpKey(key) {
9896
- const dir = resolve(PROJECT_ROOT, 'data');
9919
+ const dir = resolve(DATA_HOME, 'data');
9897
9920
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
9898
9921
  const data = { key, createdAt: new Date().toISOString() };
9899
9922
  writeFileSync(MCP_KEY_PATH, JSON.stringify(data, null, 2));
@@ -9933,10 +9956,10 @@ app.delete('/api/mcp-key', (req, res) => {
9933
9956
  });
9934
9957
 
9935
9958
  // --- Invite Key & Session Management ---
9936
- const INVITE_KEY_PATH = resolve(PROJECT_ROOT, 'data', 'invite-key.json');
9937
- const INVITE_SESSIONS_PATH = resolve(PROJECT_ROOT, 'data', 'invite-sessions.json');
9938
- const INVITE_PROXY_PATH = resolve(PROJECT_ROOT, 'data', 'invite-proxy.json');
9939
- const INVITE_PERMISSIONS_PATH = resolve(PROJECT_ROOT, 'data', 'invite-permissions.json');
9959
+ const INVITE_KEY_PATH = resolve(DATA_HOME, 'data', 'invite-key.json');
9960
+ const INVITE_SESSIONS_PATH = resolve(DATA_HOME, 'data', 'invite-sessions.json');
9961
+ const INVITE_PROXY_PATH = resolve(DATA_HOME, 'data', 'invite-proxy.json');
9962
+ const INVITE_PERMISSIONS_PATH = resolve(DATA_HOME, 'data', 'invite-permissions.json');
9940
9963
 
9941
9964
  const DEFAULT_PERMISSIONS = { terminal: false, whiteboard: false, memories: true, skills: false, cards: true, browser: false };
9942
9965
 
@@ -9956,7 +9979,7 @@ function loadInviteKey() {
9956
9979
  }
9957
9980
 
9958
9981
  function saveInviteKey(key) {
9959
- const dir = resolve(PROJECT_ROOT, 'data');
9982
+ const dir = resolve(DATA_HOME, 'data');
9960
9983
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
9961
9984
  const data = { key, createdAt: new Date().toISOString() };
9962
9985
  writeFileSync(INVITE_KEY_PATH, JSON.stringify(data, null, 2));
@@ -9988,7 +10011,7 @@ function loadInviteSessions() {
9988
10011
  }
9989
10012
 
9990
10013
  function persistInviteSessions() {
9991
- const dir = resolve(PROJECT_ROOT, 'data');
10014
+ const dir = resolve(DATA_HOME, 'data');
9992
10015
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
9993
10016
  const sessions = [...inviteSessions.entries()].map(([token, s]) => ({
9994
10017
  token, createdAt: s.createdAt, lastSeen: s.lastSeen, userAgent: s.userAgent
@@ -10006,7 +10029,7 @@ function loadInviteProxy() {
10006
10029
  }
10007
10030
 
10008
10031
  function saveInviteProxy(config) {
10009
- const dir = resolve(PROJECT_ROOT, 'data');
10032
+ const dir = resolve(DATA_HOME, 'data');
10010
10033
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
10011
10034
  writeFileSync(INVITE_PROXY_PATH, JSON.stringify(config, null, 2));
10012
10035
  }
@@ -10026,7 +10049,7 @@ function loadInvitePermissions() {
10026
10049
  }
10027
10050
 
10028
10051
  function saveInvitePermissions(perms) {
10029
- const dir = resolve(PROJECT_ROOT, 'data');
10052
+ const dir = resolve(DATA_HOME, 'data');
10030
10053
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
10031
10054
  invitePermissions = { ...DEFAULT_PERMISSIONS, ...perms };
10032
10055
  writeFileSync(INVITE_PERMISSIONS_PATH, JSON.stringify(invitePermissions, null, 2));
@@ -10254,7 +10277,7 @@ app.post('/api/tunnel/stop', (req, res) => {
10254
10277
  // Mount the SynaBun MCP server as an HTTP endpoint for Claude web client
10255
10278
  // Auth via URL-embedded key: /mcp/<key> for tunnel access, /mcp for local access
10256
10279
  try {
10257
- const mcpPath = pathToFileURL(resolve(PROJECT_ROOT, 'mcp-server', 'dist', 'http.js')).href;
10280
+ const mcpPath = pathToFileURL(resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'http.js')).href;
10258
10281
  const { createMcpRoutes } = await import(mcpPath);
10259
10282
  const mcpRouter = createMcpRoutes();
10260
10283
 
@@ -10292,7 +10315,7 @@ const TERMINAL_GRACE_PERIOD_MS = 30 * 60 * 1000; // 30 min before orphaned PTY i
10292
10315
 
10293
10316
  // ── CLI Config (custom CLI paths for terminal profiles) ──
10294
10317
 
10295
- const CLI_CONFIG_PATH = resolve(PROJECT_ROOT, 'data', 'cli-config.json');
10318
+ const CLI_CONFIG_PATH = resolve(DATA_HOME, 'data', 'cli-config.json');
10296
10319
 
10297
10320
  const CLI_DEFAULTS = {
10298
10321
  'claude-code': { command: 'claude' },
@@ -10720,7 +10743,7 @@ async function driveNextIteration(session, loopState, filename) {
10720
10743
 
10721
10744
  // ── Last Session (resume after server restart) ──
10722
10745
 
10723
- const LAST_SESSION_PATH = resolve(PROJECT_ROOT, 'data', 'last-session.json');
10746
+ const LAST_SESSION_PATH = resolve(DATA_HOME, 'data', 'last-session.json');
10724
10747
 
10725
10748
  app.get('/api/last-session', (req, res) => {
10726
10749
  try {
@@ -10771,8 +10794,8 @@ app.delete('/api/last-session', (req, res) => {
10771
10794
  const sessionRegistry = new Map(); // claudeSessionId → { cwd, profile, pid, connectedAt, lastActivity, terminalType, terminalSessionId, source }
10772
10795
  const sessionMonitorClients = new Set(); // WebSocket clients for /ws/sessions
10773
10796
  const LEAK_SCAN_INTERVAL_MS = 10_000;
10774
- const PENDING_REMEMBER_DIR = resolve(PROJECT_ROOT, 'data', 'pending-remember');
10775
- const LOOP_DIR_MONITOR = resolve(PROJECT_ROOT, 'data', 'loop');
10797
+ const PENDING_REMEMBER_DIR = resolve(DATA_HOME, 'data', 'pending-remember');
10798
+ const LOOP_DIR_MONITOR = resolve(DATA_HOME, 'data', 'loop');
10776
10799
 
10777
10800
  function broadcastSessionEvent(event) {
10778
10801
  const data = JSON.stringify(event);
@@ -10899,7 +10922,7 @@ function scanForLeaks() {
10899
10922
  }
10900
10923
 
10901
10924
  // 4. Stale precompact flags
10902
- const precompactDir = resolve(PROJECT_ROOT, 'data', 'precompact');
10925
+ const precompactDir = resolve(DATA_HOME, 'data', 'precompact');
10903
10926
  try {
10904
10927
  if (existsSync(precompactDir)) {
10905
10928
  const files = readdirSync(precompactDir).filter(f => f.endsWith('.json'));
@@ -12580,7 +12603,7 @@ function broadcastToWhiteboard(message, excludeWs = null) {
12580
12603
 
12581
12604
  // ── Browser config ──
12582
12605
 
12583
- const BROWSER_CONFIG_PATH = resolve(PROJECT_ROOT, 'data', 'browser-config.json');
12606
+ const BROWSER_CONFIG_PATH = resolve(DATA_HOME, 'data', 'browser-config.json');
12584
12607
 
12585
12608
  function loadBrowserConfig() {
12586
12609
  try {
@@ -12793,9 +12816,9 @@ async function createBrowserSession(options = {}) {
12793
12816
  // the specific profile. Without this split, Chrome creates a NEW Default profile inside
12794
12817
  // the subfolder instead of using the actual selected profile.
12795
12818
  let userDataDir = savedCfg.userDataDir || null;
12796
- // Resolve relative paths to absolute against PROJECT_ROOT (e.g. "data/chrome-profile" → full path)
12819
+ // Resolve relative paths to absolute against DATA_HOME (e.g. "data/chrome-profile" → full path)
12797
12820
  if (userDataDir && !userDataDir.startsWith('/') && !(/^[A-Z]:/i.test(userDataDir))) {
12798
- userDataDir = resolve(PROJECT_ROOT, userDataDir);
12821
+ userDataDir = resolve(DATA_HOME, userDataDir);
12799
12822
  }
12800
12823
  let _profileDirectory = null; // Chrome --profile-directory flag
12801
12824
  let _sourceProfilePath = null; // Original Chrome profile dir (for cookie bootstrap)
@@ -12942,7 +12965,7 @@ async function createBrowserSession(options = {}) {
12942
12965
  if (savedCfg.recordHar?.path) contextOpts.recordHar = savedCfg.recordHar;
12943
12966
 
12944
12967
  // Storage state: restore for clean contexts via contextOpts (persistent contexts restored post-launch)
12945
- const STORAGE_STATE_PATH = resolve(PROJECT_ROOT, savedCfg.storageStatePath || 'data/browser-storage.json');
12968
+ const STORAGE_STATE_PATH = resolve(DATA_HOME, savedCfg.storageStatePath || 'data/browser-storage.json');
12946
12969
  if (!userDataDir && savedCfg.persistStorage !== false && !savedCfg.clearStorageOnStart && existsSync(STORAGE_STATE_PATH)) {
12947
12970
  try {
12948
12971
  contextOpts.storageState = STORAGE_STATE_PATH;
@@ -12977,7 +13000,7 @@ async function createBrowserSession(options = {}) {
12977
13000
  console.log(`[browser] ${_browserLabel} profile detected — using mirror mode`);
12978
13001
 
12979
13002
  const profileHash = createHash('md5').update(_sourceProfilePath || userDataDir).digest('hex').slice(0, 12);
12980
- const mirrorRoot = resolve(PROJECT_ROOT, 'data', 'browser-profiles', profileHash);
13003
+ const mirrorRoot = resolve(DATA_HOME, 'data', 'browser-profiles', profileHash);
12981
13004
  // Wipe mirror dir completely on every launch. Stale GPU/shader caches from a
12982
13005
  // different Chromium version (e.g., Playwright Chromium vs system Chrome) crash
12983
13006
  // the browser. Auth files are re-synced below, so nothing is lost.
@@ -13160,7 +13183,7 @@ async function createBrowserSession(options = {}) {
13160
13183
  // For persistent/mirror contexts, Playwright doesn't support storageState as a launch option,
13161
13184
  // so we inject cookies after launch. For clean contexts, storageState was already set via contextOpts.
13162
13185
  if (_isPersistent && savedCfg.persistStorage !== false && !savedCfg.clearStorageOnStart) {
13163
- const _storageStatePath = resolve(PROJECT_ROOT, savedCfg.storageStatePath || 'data/browser-storage.json');
13186
+ const _storageStatePath = resolve(DATA_HOME, savedCfg.storageStatePath || 'data/browser-storage.json');
13164
13187
  if (existsSync(_storageStatePath)) {
13165
13188
  try {
13166
13189
  const savedState = JSON.parse(readFileSync(_storageStatePath, 'utf-8'));
@@ -13528,7 +13551,7 @@ async function destroyBrowserSession(sessionId) {
13528
13551
  // to preserve sessions (Twitter logins, etc.) across browser restarts.
13529
13552
  const savedCfg = loadBrowserConfig();
13530
13553
  if (savedCfg.persistStorage !== false && session.context) {
13531
- const storagePath = resolve(PROJECT_ROOT, savedCfg.storageStatePath || 'data/browser-storage.json');
13554
+ const storagePath = resolve(DATA_HOME, savedCfg.storageStatePath || 'data/browser-storage.json');
13532
13555
  try {
13533
13556
  const dir = dirname(storagePath);
13534
13557
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -13549,10 +13572,10 @@ async function destroyBrowserSession(sessionId) {
13549
13572
  const _cfg = loadBrowserConfig();
13550
13573
  const dirsToClean = [];
13551
13574
  if (_cfg.userDataDir) {
13552
- dirsToClean.push(_cfg.userDataDir, resolve(PROJECT_ROOT, _cfg.userDataDir));
13575
+ dirsToClean.push(_cfg.userDataDir, resolve(DATA_HOME, _cfg.userDataDir));
13553
13576
  }
13554
13577
  // Also clean mirror profile dirs
13555
- const mirrorsDir = resolve(PROJECT_ROOT, 'data', 'browser-profiles');
13578
+ const mirrorsDir = resolve(DATA_HOME, 'data', 'browser-profiles');
13556
13579
  if (existsSync(mirrorsDir)) {
13557
13580
  try {
13558
13581
  for (const entry of readdirSync(mirrorsDir, { withFileTypes: true })) {
@@ -14314,7 +14337,7 @@ app.put('/api/browser/config', (req, res) => {
14314
14337
  app.get('/api/browser/detect-profiles', (req, res) => {
14315
14338
  try {
14316
14339
  const profiles = detectChromeProfiles();
14317
- const synabunProfile = resolve(PROJECT_ROOT, 'data', 'chrome-profile').replace(/\\/g, '/');
14340
+ const synabunProfile = resolve(DATA_HOME, 'data', 'chrome-profile').replace(/\\/g, '/');
14318
14341
  const synabunExists = existsSync(synabunProfile);
14319
14342
  res.json({ profiles, synabunProfile, synabunProfileExists: synabunExists });
14320
14343
  } catch (err) {