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.
- package/hooks/claude-code/post-plan.mjs +2 -1
- package/hooks/claude-code/post-remember.mjs +1 -2
- package/hooks/claude-code/pre-compact.mjs +7 -4
- package/hooks/claude-code/pre-websearch.mjs +4 -2
- package/hooks/claude-code/prompt-submit.mjs +1 -2
- package/hooks/claude-code/session-start.mjs +5 -5
- package/hooks/claude-code/shared.mjs +5 -3
- package/hooks/claude-code/stop.mjs +1 -2
- package/hooks/claude-code/test-user-learning.mjs +3 -1
- package/lib/paths.js +74 -0
- package/mcp-server/dist/tools/loop.js +3 -3
- package/mcp-server/dist/tools/loop.js.map +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/tools/loop.ts +3 -3
- package/neural-interface/lib/db.js +1 -1
- package/neural-interface/public/shared/styles.css +1 -1
- package/neural-interface/public/variant/3d/graph.js +1 -0
- package/neural-interface/server.js +122 -99
- package/package.json +2 -2
- package/postinstall.js +9 -19
- package/setup.js +315 -337
- package/CLAUDE.md +0 -150
|
@@ -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
|
|
177
|
-
const
|
|
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(
|
|
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(
|
|
215
|
-
const CUSTOM_ICONS_CONFIG = resolve(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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) ||
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
4336
|
-
const LOOP_SCHEDULES_PATH = resolve(
|
|
4337
|
-
const LOOP_DIR = resolve(
|
|
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 ||
|
|
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,
|
|
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(
|
|
5421
|
-
const dotenvPath = resolve(
|
|
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 ||
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
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 →
|
|
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(
|
|
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(
|
|
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(
|
|
6716
|
-
const allowedRoots = [resolve(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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:
|
|
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(
|
|
7337
|
-
{ name: 'MCP Server', path: resolve(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
7406
|
-
const envPath = resolve(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
8351
|
-
const envPath = resolve(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
8777
|
-
const envPath = resolve(
|
|
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(
|
|
8847
|
-
const envPath = resolve(
|
|
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(
|
|
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 (
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
9937
|
-
const INVITE_SESSIONS_PATH = resolve(
|
|
9938
|
-
const INVITE_PROXY_PATH = resolve(
|
|
9939
|
-
const INVITE_PERMISSIONS_PATH = resolve(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
10775
|
-
const LOOP_DIR_MONITOR = resolve(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
13575
|
+
dirsToClean.push(_cfg.userDataDir, resolve(DATA_HOME, _cfg.userDataDir));
|
|
13553
13576
|
}
|
|
13554
13577
|
// Also clean mirror profile dirs
|
|
13555
|
-
const mirrorsDir = resolve(
|
|
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(
|
|
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) {
|