@polderlabs/bizar 5.5.1 → 5.5.2

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.
Files changed (49) hide show
  1. package/bizar-dash/dist/assets/{EnvVarsSection-BUQnyVQD.js → EnvVarsSection-DoIzqrlb.js} +3 -3
  2. package/bizar-dash/dist/assets/{EnvVarsSection-BUQnyVQD.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileChat-8FupFgTv.js → MobileChat-CoUCUsbr.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileChat-8FupFgTv.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
  5. package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
  6. package/bizar-dash/dist/assets/{MobileSettings-BkXtglA6.js.map → MobileSettings-BeO04g52.js.map} +1 -1
  7. package/bizar-dash/dist/assets/{Toast-CUwkLRms.js → Toast-D9nv5N6m.js} +1 -1
  8. package/bizar-dash/dist/assets/{Toast-CUwkLRms.js.map → Toast-D9nv5N6m.js.map} +1 -1
  9. package/bizar-dash/dist/assets/{icons-COpQr35l.js → icons-Btipv2pp.js} +65 -70
  10. package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +1 -0
  11. package/bizar-dash/dist/assets/main-DHXPuddY.js +18 -0
  12. package/bizar-dash/dist/assets/main-DHXPuddY.js.map +1 -0
  13. package/bizar-dash/dist/assets/{main-Bz_zWFCw.css → main-UG4jKK-F.css} +1 -1
  14. package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
  15. package/bizar-dash/dist/assets/{mobile-B7dAkmYd.js.map → mobile-H5KH9cTL.js.map} +1 -1
  16. package/bizar-dash/dist/assets/{mobile-layout-ClC6Qa42.js → mobile-layout-BqHxVb6_.js} +2 -2
  17. package/bizar-dash/dist/assets/{mobile-layout-ClC6Qa42.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
  18. package/bizar-dash/dist/assets/{useSlashCommands-B3MZ1JXa.js → useSlashCommands-C24-O1e7.js} +2 -2
  19. package/bizar-dash/dist/assets/{useSlashCommands-B3MZ1JXa.js.map → useSlashCommands-C24-O1e7.js.map} +1 -1
  20. package/bizar-dash/dist/index.html +7 -7
  21. package/bizar-dash/dist/mobile.html +3 -3
  22. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  23. package/bizar-dash/src/server/memory-store.mjs +74 -28
  24. package/bizar-dash/src/server/routes/memory.mjs +91 -38
  25. package/bizar-dash/src/web/App.tsx +36 -43
  26. package/bizar-dash/src/web/components/SettingsNav.tsx +92 -0
  27. package/bizar-dash/src/web/components/Sidebar.tsx +26 -7
  28. package/bizar-dash/src/web/components/Topbar.tsx +11 -48
  29. package/bizar-dash/src/web/styles/main.css +18 -3
  30. package/bizar-dash/src/web/styles/settings.css +115 -3
  31. package/bizar-dash/src/web/views/Settings.tsx +184 -205
  32. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +4 -4
  33. package/bizar-dash/tests/memory-namespace.test.mjs +2 -2
  34. package/bizar-dash/tests/memory-status-root.test.mjs +144 -0
  35. package/bizar-dash/tests/memory-store.test.mjs +5 -4
  36. package/bizar-dash/tests/memory-vault-config-endpoint.test.mjs +173 -0
  37. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  38. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  39. package/bizar-dash/tests/settings-nav.test.tsx +127 -0
  40. package/bizar-dash/tests/views/Memory.test.tsx +40 -1
  41. package/bizar-dash/tests/views/topbar.test.tsx +35 -50
  42. package/cli/memory.mjs +1 -1
  43. package/package.json +1 -1
  44. package/bizar-dash/dist/assets/MobileSettings-BkXtglA6.js +0 -1
  45. package/bizar-dash/dist/assets/icons-COpQr35l.js.map +0 -1
  46. package/bizar-dash/dist/assets/main-gqVwliHz.js +0 -18
  47. package/bizar-dash/dist/assets/main-gqVwliHz.js.map +0 -1
  48. package/bizar-dash/dist/assets/mobile-B7dAkmYd.js +0 -1
  49. package/bizar-dash/tests/views/Settings.test.tsx +0 -160
@@ -207,8 +207,14 @@ export function saveConfig(projectRoot, config) {
207
207
  /**
208
208
  * Resolve the effective vault root, namespaces, and related paths based on mode.
209
209
  *
210
+ * v6.x — `vaultRoot` is now always the GENERAL vault root (e.g. `~/.bizar_memory`).
211
+ * A new `projectVaultRoot` field carries the project-specific subdirectory for
212
+ * note storage in managed/linked mode (`~/.bizar_memory/projects/<projectId>`).
213
+ * This distinction lets the UI display the general root while the store still
214
+ * writes notes to the correct project subdirectory.
215
+ *
210
216
  * @param {string} projectRoot
211
- * @returns {{ mode: string, projectId: string, vaultRoot: string, repoPath: string | null, configDir: string, gitRemote: string | null, branch: string, lightragDir: string, namespaces: object }}
217
+ * @returns {{ mode: string, projectId: string, vaultRoot: string, projectVaultRoot: string, repoPath: string | null, configDir: string, gitRemote: string | null, branch: string, lightragDir: string, namespaces: object }}
212
218
  */
213
219
  export function resolveVault(projectRoot) {
214
220
  const { config } = loadConfig(projectRoot);
@@ -219,16 +225,19 @@ export function resolveVault(projectRoot) {
219
225
  const gitRemote = mr.remote || config.gitRemote || null;
220
226
 
221
227
  let vaultRoot;
228
+ let projectVaultRoot;
222
229
  let repoPath;
223
230
  if (mode === 'local-only') {
224
231
  vaultRoot = join(projectRoot, '.obsidian');
232
+ projectVaultRoot = vaultRoot;
225
233
  repoPath = null;
226
234
  } else {
227
235
  // managed or linked — path may be absolute, ~-relative, or a sibling of projectRoot
228
236
  const rawPath = mr.path || config.repoName || join(projectRoot, '.bizar', 'memory');
229
237
  const expanded = rawPath.startsWith('~') ? join(HOME, rawPath.slice(1)) : rawPath;
230
238
  repoPath = expanded;
231
- vaultRoot = join(expanded, 'projects', projectId);
239
+ vaultRoot = expanded; // v6.x — general root, e.g. ~/.bizar_memory
240
+ projectVaultRoot = join(expanded, 'projects', projectId); // project-specific subdirectory
232
241
  }
233
242
 
234
243
  const configDir = join(projectRoot, '.bizar');
@@ -239,29 +248,28 @@ export function resolveVault(projectRoot) {
239
248
  user: `users/${process.env.USER || process.env.USERNAME || 'local'}`,
240
249
  };
241
250
 
242
- return { mode, projectId, vaultRoot, repoPath, configDir, gitRemote, branch, lightragDir, namespaces };
251
+ return { mode, projectId, vaultRoot, projectVaultRoot, repoPath, configDir, gitRemote, branch, lightragDir, namespaces };
243
252
  }
244
253
 
245
254
  /**
246
255
  * Resolve the absolute path of a namespace root.
247
256
  *
248
- * For `project`, returns vaultRoot (the existing default). For `global` and
249
- * `user`, returns `<vaultRoot>/<namespaces.global|user>` in both modes
250
- * `writeNote('global/bizar/foo.md', …)` already places files there in both
251
- * local-only and managed mode, so this matches the on-disk layout that
252
- * existing callers rely on.
257
+ * For `project`, returns projectVaultRoot (the directory where project notes
258
+ * are stored: `<vaultRoot>/projects/<projectId>` in managed mode, or
259
+ * `<projectRoot>/.obsidian` in local-only mode). For `global` and `user`,
260
+ * returns `<vaultRoot>/<namespaces.X>` these namespaces live at the vault
261
+ * root level, not inside the project subdirectory.
253
262
  *
254
- * (Note: an earlier draft distinguished local-only vs managed, putting
255
- * global under `<repoPath>/global/bizar` in managed mode. That diverged from
256
- * where writeNote actually creates the file, which is always under
257
- * vaultRoot. This implementation matches the on-disk reality.)
263
+ * Routing: writeNote/readNote/deleteNote use this to determine where files
264
+ * live. Project relPaths go to projectVaultRoot; global/ and users/ relPaths
265
+ * go to vaultRoot-based paths.
258
266
  *
259
- * @param {{ mode: string, vaultRoot: string, namespaces: object }} vaultInfo
267
+ * @param {{ mode: string, vaultRoot: string, projectVaultRoot: string, namespaces: object }} vaultInfo
260
268
  * @param {'project'|'global'|'user'} namespace
261
269
  * @returns {string|null} absolute path, or null if the namespace is unknown
262
270
  */
263
271
  export function resolveNamespaceRoot(vaultInfo, namespace) {
264
- if (namespace === 'project') return vaultInfo.vaultRoot;
272
+ if (namespace === 'project') return vaultInfo.projectVaultRoot;
265
273
  const ns = vaultInfo.namespaces?.[namespace];
266
274
  if (!ns) return null;
267
275
  return join(vaultInfo.vaultRoot, ns);
@@ -381,8 +389,8 @@ export function initVault(projectRoot) {
381
389
  * @returns {Array<{ relPath: string, mtime: number, size: number, frontmatter: Record<string, unknown>, body: string, schemaValid: boolean }>}
382
390
  */
383
391
  export function listNotes(projectRoot, opts = {}) {
384
- const { vaultRoot } = resolveVault(projectRoot);
385
- const root = opts.root || vaultRoot;
392
+ const { projectVaultRoot } = resolveVault(projectRoot);
393
+ const root = opts.root || projectVaultRoot;
386
394
  if (!existsSync(root)) return [];
387
395
 
388
396
  const namespace = opts.namespace || '';
@@ -425,8 +433,16 @@ export function listNotes(projectRoot, opts = {}) {
425
433
  * @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean } | null}
426
434
  */
427
435
  export function readNote(projectRoot, relPath, opts = {}) {
428
- const vaultInfo = resolveVault(projectRoot);
429
- const root = opts.root || vaultInfo.vaultRoot;
436
+ const { vaultRoot, projectVaultRoot } = resolveVault(projectRoot);
437
+ // When opts.root is explicitly provided, use it. Otherwise route based on
438
+ // namespace so reads find notes where writeNote placed them.
439
+ let root;
440
+ if (opts.root) {
441
+ root = opts.root;
442
+ } else {
443
+ const isProjectNamespace = !relPath.startsWith('global/') && !relPath.startsWith('users/');
444
+ root = isProjectNamespace ? projectVaultRoot : vaultRoot;
445
+ }
430
446
  const filePath = resolveSafe(root, relPath);
431
447
  if (!filePath || !existsSync(filePath)) return null;
432
448
  try {
@@ -450,7 +466,7 @@ export function readNote(projectRoot, relPath, opts = {}) {
450
466
  * @throws {Error} if schema validation fails or HIGH-severity secret found
451
467
  */
452
468
  export function writeNote(projectRoot, relPath, { frontmatter, body }) {
453
- const { vaultRoot } = resolveVault(projectRoot);
469
+ const { vaultRoot, projectVaultRoot } = resolveVault(projectRoot);
454
470
 
455
471
  // Validate frontmatter
456
472
  const { valid, errors, warnings } = validateNote(frontmatter, body);
@@ -473,13 +489,20 @@ export function writeNote(projectRoot, relPath, { frontmatter, body }) {
473
489
  throw err;
474
490
  }
475
491
 
492
+ // Determine the effective vault root based on namespace.
493
+ // Project notes (relPaths that don't start with global/ or users/) go under
494
+ // projectVaultRoot. Global and user notes go under vaultRoot.
495
+ // This matches the layout resolveNamespaceRoot returns for each namespace.
496
+ const isProjectNamespace = !relPath.startsWith('global/') && !relPath.startsWith('users/');
497
+ const noteVaultRoot = isProjectNamespace ? projectVaultRoot : vaultRoot;
498
+
476
499
  // Build file content
477
- const filePath = resolveSafe(vaultRoot, relPath);
500
+ const filePath = resolveSafe(noteVaultRoot, relPath);
478
501
  if (!filePath) {
479
502
  throw new Error(`Invalid note path: ${relPath}`);
480
503
  }
481
504
 
482
- // Lazily create the project namespace directory (F7 — not created at initVault)
505
+ // Lazily create the namespace directory (not created at initVault)
483
506
  const nsDir = dirname(filePath);
484
507
  if (!existsSync(nsDir)) {
485
508
  mkdirSync(nsDir, { recursive: true });
@@ -538,8 +561,16 @@ export function writeNote(projectRoot, relPath, { frontmatter, body }) {
538
561
  * @returns {boolean}
539
562
  */
540
563
  export function deleteNote(projectRoot, relPath, opts = {}) {
541
- const vaultInfo = resolveVault(projectRoot);
542
- const root = opts.root || vaultInfo.vaultRoot;
564
+ const { vaultRoot, projectVaultRoot } = resolveVault(projectRoot);
565
+ // When opts.root is explicitly provided, use it. Otherwise route based on
566
+ // namespace so deletes find notes where writeNote placed them.
567
+ let root;
568
+ if (opts.root) {
569
+ root = opts.root;
570
+ } else {
571
+ const isProjectNamespace = !relPath.startsWith('global/') && !relPath.startsWith('users/');
572
+ root = isProjectNamespace ? projectVaultRoot : vaultRoot;
573
+ }
543
574
  const filePath = resolveSafe(root, relPath);
544
575
  if (!filePath || !existsSync(filePath)) return false;
545
576
  unlinkSync(filePath);
@@ -560,8 +591,23 @@ export function searchVault(projectRoot, query, { limit = 25 } = {}) {
560
591
  const tokens = q.split(/\s+/).filter((t) => t.length >= 2);
561
592
  if (tokens.length === 0) return [];
562
593
 
594
+ const { vaultRoot, projectVaultRoot } = resolveVault(projectRoot);
595
+
596
+ // Collect notes from all namespaces by walking vaultRoot (which contains
597
+ // projects/<id>/, global/, and users/ as siblings). This finds project,
598
+ // global, and user notes in a single scan. relPaths returned are vaultRoot-
599
+ // relative (e.g. "projects/<id>/decisions/foo.md" or "global/bizar/foo.md").
600
+ // For project notes, strip the "projects/<id>/" prefix so the relPath matches
601
+ // what writeNote uses (projectVaultRoot-relative).
602
+ const projectPrefix = join('projects', projectVaultRoot.split('/').pop());
603
+ const allNotes = listNotes(projectRoot, { root: vaultRoot });
604
+
563
605
  const results = [];
564
- for (const note of listNotes(projectRoot)) {
606
+ for (const note of allNotes) {
607
+ let relPath = note.relPath;
608
+ if (relPath.startsWith(projectPrefix)) {
609
+ relPath = relPath.slice(projectPrefix.length + 1);
610
+ }
565
611
  const lower = (note.body + ' ' + JSON.stringify(note.frontmatter)).toLowerCase();
566
612
  let score = 0;
567
613
  for (const t of tokens) {
@@ -572,7 +618,7 @@ export function searchVault(projectRoot, query, { limit = 25 } = {}) {
572
618
  const start = Math.max(0, idx - 60);
573
619
  const end = Math.min(lower.length, idx + 160);
574
620
  const snippet = (start > 0 ? '…' : '') + lower.slice(start, end).replace(/\s+/g, ' ').trim();
575
- results.push({ relPath: note.relPath, snippet, score, mtime: note.mtime });
621
+ results.push({ relPath, snippet, score, mtime: note.mtime });
576
622
  }
577
623
 
578
624
  return results.sort((a, b) => b.score - a.score).slice(0, limit);
@@ -707,8 +753,8 @@ export function vaultStats(projectRoot) {
707
753
  };
708
754
  }
709
755
 
710
- const { vaultRoot, mode, branch } = resolveVault(projectRoot);
711
- if (!vaultRoot || !existsSync(vaultRoot)) {
756
+ const { vaultRoot, projectVaultRoot, mode, branch } = resolveVault(projectRoot);
757
+ if (!projectVaultRoot || !existsSync(projectVaultRoot)) {
712
758
  return {
713
759
  exists: true,
714
760
  vaultRoot: vaultRoot || '',
@@ -737,7 +783,7 @@ export function vaultStats(projectRoot) {
737
783
  try {
738
784
  const { isGitInstalled, status: gitStatus } = memoryGit;
739
785
  if (isGitInstalled()) {
740
- const gs = gitStatus(vaultRoot);
786
+ const gs = gitStatus(projectVaultRoot);
741
787
  gitClean = gs.clean;
742
788
  gitBranch = gs.branch;
743
789
  }
@@ -10,8 +10,9 @@
10
10
  */
11
11
 
12
12
  import { Router } from 'express';
13
- import { join, dirname } from 'node:path';
13
+ import { join, dirname, resolve as pathResolve } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { homedir } from 'node:os';
15
16
  import { existsSync, mkdirSync, readFileSync, statSync, readdirSync } from 'node:fs';
16
17
  import { execFileSync } from 'node:child_process';
17
18
  import { warn as logWarn } from '../logger.mjs';
@@ -60,17 +61,19 @@ export function createMemoryRouter({ projectRoot }) {
60
61
 
61
62
  const { config, exists } = loadConfig(projectRoot);
62
63
  if (!exists) {
63
- res.json({ initialized: false, mode: null, projectId: null, vaultRoot: null });
64
+ res.json({ initialized: false, mode: null, projectId: null, vaultRoot: null, projectVaultRoot: null });
64
65
  return;
65
66
  }
66
67
 
67
- const { vaultRoot, mode, projectId, branch } = resolveVault(projectRoot);
68
- const notes = existsSync(vaultRoot) ? listNotes(projectRoot) : [];
68
+ // v6.x vaultRoot is now the GENERAL vault root (e.g. ~/.bizar_memory).
69
+ // projectVaultRoot is the project-specific subdirectory for note storage.
70
+ const { vaultRoot, projectVaultRoot, mode, projectId, branch } = resolveVault(projectRoot);
71
+ const notes = existsSync(projectVaultRoot) ? listNotes(projectRoot) : [];
69
72
 
70
73
  let gitClean = null;
71
74
  let gitBranch = null;
72
- if ((mode === 'managed' || mode === 'linked') && existsSync(vaultRoot) && isGitInstalled()) {
73
- const gs = gitStatus(vaultRoot);
75
+ if ((mode === 'managed' || mode === 'linked') && existsSync(projectVaultRoot) && isGitInstalled()) {
76
+ const gs = gitStatus(projectVaultRoot);
74
77
  gitClean = gs.clean;
75
78
  gitBranch = gs.branch;
76
79
  }
@@ -91,7 +94,8 @@ export function createMemoryRouter({ projectRoot }) {
91
94
  initialized: true,
92
95
  mode,
93
96
  projectId,
94
- vaultRoot,
97
+ vaultRoot, // v6.x — general vault root (e.g. ~/.bizar_memory)
98
+ projectVaultRoot, // v6.x — project-specific subdirectory for managed/linked mode
95
99
  branch: gitBranch || branch,
96
100
  gitClean,
97
101
  noteCount: notes.length,
@@ -277,7 +281,7 @@ export function createMemoryRouter({ projectRoot }) {
277
281
  router.get('/memory/git/status', wrap(async (_req, res) => {
278
282
  const { resolveVault } = memoryStore;
279
283
  const { isGitInstalled, status: gitStatus } = memoryGit;
280
- const { vaultRoot, mode } = resolveVault(projectRoot);
284
+ const { projectVaultRoot, mode } = resolveVault(projectRoot);
281
285
 
282
286
  if (mode === 'local-only') {
283
287
  res.json({ ok: true, mode: 'local-only' });
@@ -289,7 +293,7 @@ export function createMemoryRouter({ projectRoot }) {
289
293
  return;
290
294
  }
291
295
 
292
- const gs = gitStatus(vaultRoot);
296
+ const gs = gitStatus(projectVaultRoot);
293
297
  res.json({ ok: true, mode, ...gs });
294
298
  }));
295
299
 
@@ -297,7 +301,7 @@ export function createMemoryRouter({ projectRoot }) {
297
301
  router.post('/memory/git/pull', wrap(async (_req, res) => {
298
302
  const { resolveVault } = memoryStore;
299
303
  const { pull, isGitInstalled } = memoryGit;
300
- const { vaultRoot, mode } = resolveVault(projectRoot);
304
+ const { projectVaultRoot, mode } = resolveVault(projectRoot);
301
305
 
302
306
  if (mode === 'local-only') {
303
307
  res.status(400).json({ error: 'local_only_mode' });
@@ -308,7 +312,7 @@ export function createMemoryRouter({ projectRoot }) {
308
312
  return;
309
313
  }
310
314
 
311
- const result = pull(vaultRoot);
315
+ const result = pull(projectVaultRoot);
312
316
  if (!result.ok) {
313
317
  res.status(500).json({ error: 'pull_failed', message: result.error });
314
318
  return;
@@ -320,7 +324,7 @@ export function createMemoryRouter({ projectRoot }) {
320
324
  router.post('/memory/git/commit', wrap(async (req, res) => {
321
325
  const { resolveVault } = memoryStore;
322
326
  const { commit: gitCommit, addAll, isGitInstalled } = memoryGit;
323
- const { vaultRoot, mode } = resolveVault(projectRoot);
327
+ const { projectVaultRoot, mode } = resolveVault(projectRoot);
324
328
 
325
329
  if (mode === 'local-only') {
326
330
  res.status(400).json({ error: 'local_only_mode' });
@@ -331,14 +335,14 @@ export function createMemoryRouter({ projectRoot }) {
331
335
  return;
332
336
  }
333
337
 
334
- const addResult = addAll(vaultRoot);
338
+ const addResult = addAll(projectVaultRoot);
335
339
  if (!addResult.ok) {
336
340
  res.status(500).json({ error: 'git_add_failed', message: addResult.error });
337
341
  return;
338
342
  }
339
343
 
340
344
  const message = req.body?.message || `[memory-sync] ${new Date().toISOString().replace(/T.*/, '')} vault sync`;
341
- const result = gitCommit(vaultRoot, message);
345
+ const result = gitCommit(projectVaultRoot, message);
342
346
  if (!result.ok) {
343
347
  res.status(500).json({ error: 'commit_failed', message: result.error });
344
348
  return;
@@ -350,7 +354,7 @@ export function createMemoryRouter({ projectRoot }) {
350
354
  router.post('/memory/git/push', wrap(async (_req, res) => {
351
355
  const { resolveVault, loadConfig } = memoryStore;
352
356
  const { push: gitPush, isGitInstalled } = memoryGit;
353
- const { vaultRoot, mode, branch } = resolveVault(projectRoot);
357
+ const { projectVaultRoot, mode, branch } = resolveVault(projectRoot);
354
358
  const { config } = loadConfig(projectRoot);
355
359
 
356
360
  if (mode === 'local-only') {
@@ -362,7 +366,7 @@ export function createMemoryRouter({ projectRoot }) {
362
366
  return;
363
367
  }
364
368
 
365
- const result = gitPush(vaultRoot, { remote: config.gitRemote || 'origin', branch });
369
+ const result = gitPush(projectVaultRoot, { remote: config.gitRemote || 'origin', branch });
366
370
  if (!result.ok) {
367
371
  res.status(500).json({ error: 'push_failed', message: result.error });
368
372
  return;
@@ -374,7 +378,7 @@ export function createMemoryRouter({ projectRoot }) {
374
378
  router.post('/memory/git/sync', wrap(async (req, res) => {
375
379
  const { resolveVault, listNotes, validateAll, scanForSecrets } = memoryStore;
376
380
  const { pull, addAll, commit: gitCommit, push: gitPush, status: gitStatus, acquireLock, isGitInstalled } = memoryGit;
377
- const { vaultRoot, mode, branch } = resolveVault(projectRoot);
381
+ const { projectVaultRoot, mode, branch } = resolveVault(projectRoot);
378
382
  const { config } = memoryStore.loadConfig(projectRoot);
379
383
 
380
384
  if (mode === 'local-only') {
@@ -386,7 +390,7 @@ export function createMemoryRouter({ projectRoot }) {
386
390
  return;
387
391
  }
388
392
 
389
- const lock = acquireLock(vaultRoot);
393
+ const lock = acquireLock(projectVaultRoot);
390
394
  if (lock.error) {
391
395
  res.status(423).json({ error: 'locked', message: 'vault is locked by another process' });
392
396
  return;
@@ -396,13 +400,13 @@ export function createMemoryRouter({ projectRoot }) {
396
400
 
397
401
  try {
398
402
  // Pull
399
- const pullResult = pull(vaultRoot);
403
+ const pullResult = pull(projectVaultRoot);
400
404
  if (!pullResult.ok) {
401
405
  // Non-fatal — may be up to date or have no remote
402
406
  }
403
407
 
404
408
  // Check status
405
- const gs = gitStatus(vaultRoot);
409
+ const gs = gitStatus(projectVaultRoot);
406
410
  if (gs.clean) {
407
411
  res.json({ ok: true, message: 'nothing to sync', clean: true });
408
412
  return;
@@ -437,7 +441,7 @@ export function createMemoryRouter({ projectRoot }) {
437
441
  }
438
442
 
439
443
  // Add all
440
- const addResult = addAll(vaultRoot);
444
+ const addResult = addAll(projectVaultRoot);
441
445
  if (!addResult.ok) {
442
446
  res.status(500).json({ error: 'git_add_failed' });
443
447
  return;
@@ -447,7 +451,7 @@ export function createMemoryRouter({ projectRoot }) {
447
451
  const date = new Date().toISOString().replace(/T.*/, '');
448
452
  const summary = allNotes[0]?.relPath?.slice(0, 60) || 'vault sync';
449
453
  const message = `[memory-sync] ${date} ${summary}`;
450
- const commitResult = gitCommit(vaultRoot, message);
454
+ const commitResult = gitCommit(projectVaultRoot, message);
451
455
  if (!commitResult.ok) {
452
456
  res.status(500).json({ error: 'commit_failed', message: commitResult.error });
453
457
  return;
@@ -455,7 +459,7 @@ export function createMemoryRouter({ projectRoot }) {
455
459
 
456
460
  // Push if requested
457
461
  if (pushRequested && config.gitRemote) {
458
- const pushResult = gitPush(vaultRoot, { remote: config.gitRemote, branch });
462
+ const pushResult = gitPush(projectVaultRoot, { remote: config.gitRemote, branch });
459
463
  if (!pushResult.ok) {
460
464
  res.status(500).json({ error: 'push_failed', message: pushResult.error });
461
465
  return;
@@ -476,7 +480,7 @@ export function createMemoryRouter({ projectRoot }) {
476
480
  // GET /memory/conflicts
477
481
  router.get('/memory/conflicts', wrap(async (_req, res) => {
478
482
  const { listNotes, readNote } = memoryStore;
479
- const { vaultRoot } = memoryStore.resolveVault(projectRoot);
483
+ const { projectVaultRoot } = memoryStore.resolveVault(projectRoot);
480
484
  const notes = listNotes(projectRoot);
481
485
  const conflicts = [];
482
486
 
@@ -490,7 +494,7 @@ export function createMemoryRouter({ projectRoot }) {
490
494
  try {
491
495
  const { readFileSync: rf } = await import('node:fs');
492
496
  for (const note of notes) {
493
- const filePath = join(vaultRoot, note.relPath);
497
+ const filePath = join(projectVaultRoot, note.relPath);
494
498
  const content = rf(filePath, 'utf8');
495
499
  if (/^<{7}\s|^={7}\s|>{7}\s/.test(content)) {
496
500
  conflicts.push({ relPath: note.relPath, reason: 'git conflict markers' });
@@ -849,6 +853,49 @@ export function createMemoryRouter({ projectRoot }) {
849
853
  res.json({ ok: true, exists: true, path: GLOBAL_MEMORY_CONFIG_PATH, config: next });
850
854
  }));
851
855
 
856
+ // POST /memory/config/vault — v6.x — update the vault root path.
857
+ // Accepts { vaultRoot: string }
858
+ // Persists to ~/.config/bizar/memory-config.json so it survives restarts,
859
+ // and sets process.env.BIZAR_MEMORY_VAULT for the current process.
860
+ router.post('/memory/config/vault', wrap(async (req, res) => {
861
+ const { vaultRoot: newVaultRoot } = req.body || {};
862
+ const trimmed = typeof newVaultRoot === 'string' ? newVaultRoot.trim() : '';
863
+ if (!trimmed || typeof newVaultRoot !== 'string') {
864
+ res.status(400).json({ error: 'vaultRoot required' });
865
+ return;
866
+ }
867
+
868
+ // Expand ~ to home directory
869
+ const expanded = trimmed.startsWith('~')
870
+ ? join(homedir(), trimmed.slice(1))
871
+ : trimmed;
872
+ const resolved = pathResolve(expanded);
873
+
874
+ // Validate: try to create the directory if it doesn't exist
875
+ try {
876
+ mkdirSync(resolved, { recursive: true, mode: 0o700 });
877
+ } catch (err) {
878
+ res.status(400).json({ error: `Cannot create vault at ${resolved}: ${err.message}` });
879
+ return;
880
+ }
881
+
882
+ // Persist to the global memory config so it survives restarts.
883
+ // Also update process.env so the change takes effect immediately.
884
+ process.env.BIZAR_MEMORY_VAULT = resolved;
885
+ const { config: existing } = loadGlobalMemoryConfig();
886
+ const next = JSON.parse(JSON.stringify(existing));
887
+ next.git = next.git || {};
888
+ next.git.repoPath = resolved; // v6.x — also set git.repoPath to the vault root
889
+ try {
890
+ saveGlobalMemoryConfig(next);
891
+ } catch (err) {
892
+ res.status(500).json({ error: 'write_failed', message: err.message });
893
+ return;
894
+ }
895
+
896
+ res.json({ ok: true, vaultRoot: resolved });
897
+ }));
898
+
852
899
  // POST /memory/test-git — test the configured git repo. Returns:
853
900
  // { ok: bool, checks: [...], message?: string }
854
901
  // Steps performed:
@@ -857,15 +904,20 @@ export function createMemoryRouter({ projectRoot }) {
857
904
  // 3. Is a valid git repo (has .git/)?
858
905
  // 4. (Optional) Has a configured remote?
859
906
  // 5. (Optional) Can we push (skipped unless ?push=1)?
907
+ // v6.x — falls back to the general vault root (BIZAR_MEMORY_VAULT or ~/.bizar_memory)
908
+ // when git.repoPath is not explicitly configured.
860
909
  router.post('/memory/test-git', wrap(async (req, res) => {
861
910
  const { config } = loadGlobalMemoryConfig();
862
- const repoPath = config?.git?.repoPath || '';
911
+ // v6.x: fall back to the general vault root when not explicitly set
912
+ const configuredPath = config?.git?.repoPath || '';
913
+ const repoPath = configuredPath || memoryStore.currentVault();
863
914
  const remoteUrl = config?.git?.remoteUrl || '';
864
915
  const wantPush = req.query.push === '1' || req.body?.push === true;
865
916
 
866
917
  const checks = [];
867
- if (!repoPath) {
868
- checks.push({ name: 'repo_path_set', pass: false, detail: 'git.repoPath is empty — set it in Settings first' });
918
+ // Only fail if the fallback path also doesn't exist
919
+ if (!configuredPath && !existsSync(repoPath)) {
920
+ checks.push({ name: 'repo_path_set', pass: false, detail: `git.repoPath not set — vault at ${repoPath} does not exist yet` });
869
921
  res.json({ ok: false, checks, message: 'repo path not configured' });
870
922
  return;
871
923
  }
@@ -948,8 +1000,8 @@ export function createMemoryRouter({ projectRoot }) {
948
1000
  return;
949
1001
  }
950
1002
 
951
- const { vaultRoot, mode } = resolveVault(projectRoot);
952
- const vaultExists = existsSync(vaultRoot);
1003
+ const { vaultRoot, projectVaultRoot, mode } = resolveVault(projectRoot);
1004
+ const vaultExists = existsSync(projectVaultRoot);
953
1005
  checks.push({
954
1006
  name: 'vault_exists',
955
1007
  pass: vaultExists,
@@ -961,7 +1013,7 @@ export function createMemoryRouter({ projectRoot }) {
961
1013
  let writable = false;
962
1014
  if (vaultExists) {
963
1015
  try {
964
- const probe = join(vaultRoot, '.health-probe.tmp');
1016
+ const probe = join(projectVaultRoot, '.health-probe.tmp');
965
1017
  writeFileSync(probe, 'ok');
966
1018
  try {
967
1019
  const { unlinkSync } = await import('node:fs');
@@ -978,7 +1030,7 @@ export function createMemoryRouter({ projectRoot }) {
978
1030
  // Git clean?
979
1031
  let gitClean = null;
980
1032
  if ((mode === 'managed' || mode === 'linked') && vaultExists && isGitInstalled()) {
981
- const gs = gitStatus(vaultRoot);
1033
+ const gs = gitStatus(projectVaultRoot);
982
1034
  gitClean = gs.clean;
983
1035
  checks.push({
984
1036
  name: 'git_clean',
@@ -1058,10 +1110,11 @@ export function createMemoryRouter({ projectRoot }) {
1058
1110
  // Walks the vault + .bizar/memory-cache + lightrag working dir.
1059
1111
  router.get('/memory/storage', wrap(async (_req, res) => {
1060
1112
  const { resolveVault } = memoryStore;
1061
- const { vaultRoot, mode } = resolveVault(projectRoot);
1113
+ const { vaultRoot, projectVaultRoot, mode } = resolveVault(projectRoot);
1062
1114
 
1063
1115
  const targets = [];
1064
- if (existsSync(vaultRoot)) targets.push({ name: 'vault', path: vaultRoot });
1116
+ // Use projectVaultRoot for the actual note storage path
1117
+ if (existsSync(projectVaultRoot)) targets.push({ name: 'vault', path: projectVaultRoot });
1065
1118
  const cacheDir = join(projectRoot, '.bizar', 'memory-cache');
1066
1119
  if (existsSync(cacheDir)) targets.push({ name: 'memory-cache', path: cacheDir });
1067
1120
  const lightragDir = join(projectRoot, '.bizar', 'lightrag');
@@ -1078,7 +1131,7 @@ export function createMemoryRouter({ projectRoot }) {
1078
1131
  total,
1079
1132
  breakdown,
1080
1133
  mode,
1081
- vaultRoot,
1134
+ vaultRoot, // general vault root for display
1082
1135
  message: total === 0 ? 'no memory data on disk yet' : `${formatBytes(total)} on disk`,
1083
1136
  });
1084
1137
  }));
@@ -1088,7 +1141,7 @@ export function createMemoryRouter({ projectRoot }) {
1088
1141
  router.get('/memory/git/diff', wrap(async (_req, res) => {
1089
1142
  const { resolveVault } = memoryStore;
1090
1143
  const { isGitInstalled } = memoryGit;
1091
- const { vaultRoot, mode } = resolveVault(projectRoot);
1144
+ const { projectVaultRoot, mode } = resolveVault(projectRoot);
1092
1145
 
1093
1146
  if (mode === 'local-only') {
1094
1147
  res.json({ hasDiff: false, lines: [], files: [], mode: 'local-only' });
@@ -1102,7 +1155,7 @@ export function createMemoryRouter({ projectRoot }) {
1102
1155
  let raw = '';
1103
1156
  try {
1104
1157
  raw = execFileSync('git', ['diff', '--no-color', '--no-ext-diff'], {
1105
- cwd: vaultRoot,
1158
+ cwd: projectVaultRoot,
1106
1159
  encoding: 'utf8',
1107
1160
  maxBuffer: 8 * 1024 * 1024,
1108
1161
  });
@@ -1115,7 +1168,7 @@ export function createMemoryRouter({ projectRoot }) {
1115
1168
  let untracked = [];
1116
1169
  try {
1117
1170
  const statusRaw = execFileSync('git', ['status', '--porcelain'], {
1118
- cwd: vaultRoot,
1171
+ cwd: projectVaultRoot,
1119
1172
  encoding: 'utf8',
1120
1173
  });
1121
1174
  for (const line of statusRaw.split('\n')) {