@polderlabs/bizar 4.0.0 → 4.2.1

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 (44) hide show
  1. package/README.md +11 -14
  2. package/bizar-dash/CHANGELOG.md +1 -1
  3. package/bizar-dash/src/server/api.mjs +2 -2
  4. package/bizar-dash/src/server/artifacts-store.mjs +4 -4
  5. package/bizar-dash/src/server/memory-git.mjs +142 -0
  6. package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
  7. package/bizar-dash/src/server/memory-store.mjs +129 -10
  8. package/bizar-dash/src/server/mod-security.mjs +2 -2
  9. package/bizar-dash/src/server/routes/memory.mjs +174 -15
  10. package/bizar-dash/src/server/server.mjs +1 -1
  11. package/bizar-dash/src/server/state.mjs +2 -2
  12. package/bizar-dash/src/web/views/Config.tsx +461 -1
  13. package/bizar-dash/tests/memory-cli-readlistdelete.test.mjs +405 -0
  14. package/bizar-dash/tests/memory-cli-setup.test.mjs +382 -0
  15. package/bizar-dash/tests/memory-cli.test.mjs +542 -0
  16. package/bizar-dash/tests/memory-config.test.mjs +422 -0
  17. package/bizar-dash/tests/memory-conflicts.test.mjs +229 -0
  18. package/bizar-dash/tests/memory-git.test.mjs +109 -1
  19. package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
  20. package/bizar-dash/tests/memory-namespace.test.mjs +404 -0
  21. package/bizar-dash/tests/memory-path-safety.test.mjs +427 -0
  22. package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
  23. package/bizar-dash/tests/memory-roundtrip.test.mjs +219 -0
  24. package/cli/banner.mjs +1 -1
  25. package/cli/bin.mjs +4 -4
  26. package/cli/bootstrap.mjs +1 -1
  27. package/cli/copy.mjs +22 -16
  28. package/cli/doctor.mjs +4 -4
  29. package/cli/doctor.test.mjs +2 -2
  30. package/cli/init.mjs +2 -2
  31. package/cli/install.mjs +21 -16
  32. package/cli/memory.mjs +952 -37
  33. package/cli/utils.mjs +6 -3
  34. package/config/AGENTS.md +7 -7
  35. package/config/agents/_shared/AGENT_BASELINE.md +59 -61
  36. package/config/opencode.json +13 -38
  37. package/config/skills/memory-protocol/SKILL.md +105 -0
  38. package/config/skills/obsidian/SKILL.md +58 -1
  39. package/install.sh +11 -1
  40. package/package.json +2 -2
  41. package/plugins/bizar/index.ts +7 -0
  42. package/plugins/bizar/src/commands.ts +42 -1
  43. package/plugins/bizar/src/tools/open-kb.ts +191 -0
  44. package/plugins/bizar/tests/commands.test.ts +36 -0
package/cli/memory.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
  * cli/memory.mjs
3
3
  *
4
4
  * `bizar memory` subcommands. Delegates to memory-store.mjs and memory-git.mjs.
5
- * Supports: init, status, link, unlink, pull, commit, push, sync, reindex,
6
- * conflicts, doctor.
5
+ * Supports: init, status, link, unlink, write, pull, commit, push, sync,
6
+ * reindex, conflicts, doctor.
7
7
  */
8
8
 
9
9
  import chalk from 'chalk';
@@ -18,6 +18,7 @@ const memoryStore = await import(`${SERVER_ROOT}/memory-store.mjs`).then((m) =>
18
18
  const memorySchema = await import(`${SERVER_ROOT}/memory-schema.mjs`).then((m) => m);
19
19
  const memorySecrets = await import(`${SERVER_ROOT}/memory-secrets.mjs`).then((m) => m);
20
20
  const memoryGit = await import(`${SERVER_ROOT}/memory-git.mjs`).then((m) => m);
21
+ const memoryLightrag = await import(`${SERVER_ROOT}/memory-lightrag.mjs`).then((m) => m);
21
22
 
22
23
  /**
23
24
  * Get the project root. Assumes CWD is the project root.
@@ -26,6 +27,33 @@ function getProjectRoot() {
26
27
  return process.cwd();
27
28
  }
28
29
 
30
+ /**
31
+ * Validate a git remote URL. Accepts HTTPS, SSH (scp-style), and SSH URL form.
32
+ * Rejects file://, plain paths, and other schemes.
33
+ *
34
+ * @param {string} url
35
+ * @returns {{ valid: true, kind: 'ssh'|'https'|'ssh-url' } | { valid: false, error: string }}
36
+ */
37
+ export function validateRemoteUrl(url) {
38
+ if (typeof url !== 'string' || url.length === 0) {
39
+ return { valid: false, error: 'URL must be a non-empty string' };
40
+ }
41
+ const trimmed = url.trim();
42
+ // SSH scp-style: git@host:path
43
+ if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:.+$/.test(trimmed)) {
44
+ return { valid: true, kind: 'ssh' };
45
+ }
46
+ // SSH URL form: ssh://[user@]host[:port]/path
47
+ if (/^ssh:\/\/(?:[a-zA-Z0-9._-]+@)?[a-zA-Z0-9._-]+(?::\d+)?\/.+$/.test(trimmed)) {
48
+ return { valid: true, kind: 'ssh-url' };
49
+ }
50
+ // HTTPS: https://host/path
51
+ if (/^https:\/\/[a-zA-Z0-9._-]+(?::\d+)?\/.+$/.test(trimmed)) {
52
+ return { valid: true, kind: 'https' };
53
+ }
54
+ return { valid: false, error: 'URL must be ssh://, https://, or git@host:path form' };
55
+ }
56
+
29
57
  // ─── Helper formatters ────────────────────────────────────────────────────────
30
58
 
31
59
  function success(msg) { console.log(chalk.green('✓'), msg); }
@@ -44,10 +72,178 @@ function kv(key, value) {
44
72
 
45
73
  // ─── Subcommand implementations ───────────────────────────────────────────────
46
74
 
75
+ /**
76
+ * `bizar memory write <relpath> [--type …] [--status …] [--confidence …]`
77
+ * `[--tag <tag>]… [--body <body>|--body-file <file>]`
78
+ * `[--title <title>] [--json]`
79
+ *
80
+ * Write a single note to the vault. Validates the path, type, status, and
81
+ * confidence against the schema, builds frontmatter via `defaultFrontmatter`,
82
+ * then delegates to `memoryStore.writeNote`. Designed for agents (and humans)
83
+ * that want to write memory notes without going through the dashboard UI.
84
+ */
85
+ async function cmdWrite(args) {
86
+ const projectRoot = getProjectRoot();
87
+ const { writeNote, loadConfig } = memoryStore;
88
+ const { defaultFrontmatter, VALID_TYPES, VALID_STATUSES, VALID_CONFIDENCES } = memorySchema;
89
+
90
+ if (args.includes('--help') || args.includes('-h')) {
91
+ console.log(`
92
+ Usage: bizar memory write <relpath> [options]
93
+
94
+ Write a single note to the vault. <relpath> must end in .md and is
95
+ resolved relative to the project namespace root (e.g.
96
+ decisions/0001-foo.md), NOT prefixed with the namespace.
97
+
98
+ Options:
99
+ --type <type> One of: ${VALID_TYPES.join(', ')}
100
+ (default: project_overview)
101
+ --status <status> One of: ${VALID_STATUSES.join(', ')}
102
+ (default: active)
103
+ --confidence <conf> One of: ${VALID_CONFIDENCES.join(', ')}
104
+ (default: verified)
105
+ --tag <tag> Tag to attach (may be passed multiple times)
106
+ --title <title> Frontmatter title field
107
+ --body <text> Note body as a string
108
+ --body-file <path> Path to a file containing the body
109
+ (exactly one of --body / --body-file is allowed)
110
+ --json Emit the full returned note as JSON
111
+ --help, -h Show this help
112
+
113
+ Exactly one of --body or --body-file is required, unless you intend
114
+ to write an empty body (in which case omit both).
115
+ `.trim());
116
+ return;
117
+ }
118
+
119
+ // Parse positional relpath (first non-flag arg)
120
+ const positional = args.filter((a) => !a.startsWith('-'));
121
+ const relpath = positional[0];
122
+ if (!relpath) {
123
+ error('relpath is required: `bizar memory write <relpath>`');
124
+ info('run `bizar memory write --help` for usage');
125
+ process.exit(1);
126
+ }
127
+ if (!relpath.endsWith('.md')) {
128
+ error(`relpath must end in .md: ${relpath}`);
129
+ process.exit(1);
130
+ }
131
+
132
+ // ── Parse flags ──────────────────────────────────────────────────────────
133
+ const optVal = (name) => {
134
+ const i = args.indexOf(name);
135
+ if (i === -1) return undefined;
136
+ const v = args[i + 1];
137
+ if (!v || v.startsWith('-')) return undefined;
138
+ return v;
139
+ };
140
+
141
+ const type = optVal('--type') ?? 'project_overview';
142
+ if (!VALID_TYPES.includes(type)) {
143
+ error(`invalid --type: '${type}'`);
144
+ info(`must be one of: ${VALID_TYPES.join(', ')}`);
145
+ process.exit(1);
146
+ }
147
+
148
+ const status = optVal('--status') ?? 'active';
149
+ if (!VALID_STATUSES.includes(status)) {
150
+ error(`invalid --status: '${status}'`);
151
+ info(`must be one of: ${VALID_STATUSES.join(', ')}`);
152
+ process.exit(1);
153
+ }
154
+
155
+ const confidence = optVal('--confidence') ?? 'verified';
156
+ if (!VALID_CONFIDENCES.includes(confidence)) {
157
+ error(`invalid --confidence: '${confidence}'`);
158
+ info(`must be one of: ${VALID_CONFIDENCES.join(', ')}`);
159
+ process.exit(1);
160
+ }
161
+
162
+ const title = optVal('--title');
163
+
164
+ // Multi-value --tag
165
+ const tags = [];
166
+ for (let i = 0; i < args.length; i++) {
167
+ if (args[i] === '--tag' && args[i + 1] && !args[i + 1].startsWith('-')) {
168
+ tags.push(args[i + 1]);
169
+ i++;
170
+ }
171
+ }
172
+
173
+ const inlineBody = optVal('--body');
174
+ const bodyFile = optVal('--body-file');
175
+ if (inlineBody !== undefined && bodyFile !== undefined) {
176
+ error('use either --body or --body-file, not both');
177
+ process.exit(1);
178
+ }
179
+
180
+ let body = '';
181
+ if (bodyFile) {
182
+ try {
183
+ body = readFileSync(bodyFile, 'utf8');
184
+ } catch (err) {
185
+ error(`failed to read --body-file ${bodyFile}: ${err.message}`);
186
+ process.exit(1);
187
+ }
188
+ } else if (inlineBody !== undefined) {
189
+ body = inlineBody;
190
+ }
191
+
192
+ // ── Build frontmatter ───────────────────────────────────────────────────
193
+ const { config } = loadConfig(projectRoot);
194
+ const projectId = config.projectId || projectRoot.split('/').pop() || 'unknown';
195
+
196
+ const frontmatter = defaultFrontmatter({
197
+ type,
198
+ project_id: projectId,
199
+ status,
200
+ confidence,
201
+ tags,
202
+ });
203
+ if (title) frontmatter.title = title;
204
+
205
+ // ── Write ───────────────────────────────────────────────────────────────
206
+ let result;
207
+ try {
208
+ result = writeNote(projectRoot, relpath, { frontmatter, body });
209
+ } catch (err) {
210
+ if (err.code === 'SCHEMA_VALIDATION_FAILED') {
211
+ error(`schema validation failed: ${err.message.replace(/^schema validation failed: /, '')}`);
212
+ } else if (err.code === 'SECRET_DETECTED') {
213
+ error(err.message);
214
+ } else {
215
+ error(err.message);
216
+ }
217
+ process.exit(1);
218
+ }
219
+
220
+ // ── Output ──────────────────────────────────────────────────────────────
221
+ if (args.includes('--json')) {
222
+ console.log(JSON.stringify(result, null, 2));
223
+ return;
224
+ }
225
+
226
+ success(`wrote ${result.relPath}`);
227
+ console.log();
228
+ console.log(chalk.bold(' Note'));
229
+ console.log(' ─────────────────────────────');
230
+ kv('relpath', result.relPath);
231
+ kv('type', String(result.frontmatter.type));
232
+ kv('status', String(result.frontmatter.status));
233
+ kv('confidence', String(result.frontmatter.confidence));
234
+ kv('memory_id', String(result.frontmatter.memory_id));
235
+ if (Array.isArray(result.frontmatter.tags) && result.frontmatter.tags.length > 0) {
236
+ kv('tags', result.frontmatter.tags.join(', '));
237
+ }
238
+ if (title) kv('title', title);
239
+ kv('size', `${result.size} bytes`);
240
+ console.log();
241
+ }
242
+
47
243
  /**
48
244
  * `bizar memory init`
49
- * Creates .bizar/memory.json with mode=local-only.
50
- * If mode=managed is desired, user passes --managed or --repo <name>.
245
+ * Creates .bizar/memory.json with mode=managed (default).
246
+ * Pass --memory-mode local-only to use local vault storage instead.
51
247
  *
52
248
  * Flags (added for install.sh bootstrap):
53
249
  * --yes Accept all defaults (skip prompts, skip if already initialized)
@@ -58,7 +254,25 @@ async function cmdInit(args) {
58
254
  const projectRoot = getProjectRoot();
59
255
  const { loadConfig, saveConfig } = memoryStore;
60
256
 
61
- const yesFlag = args.includes('--yes');
257
+ // ── Help (must come before any work — fixes gap in cmdInit --help coverage)
258
+ if (args.includes('--help') || args.includes('-h')) {
259
+ console.log(`
260
+ Usage: bizar memory init [options]
261
+
262
+ Initialize memory config for this project.
263
+
264
+ Options:
265
+ --memory-mode <managed|local-only> Vault mode (default: managed)
266
+ --memory-repo-name <name> Vault directory name (managed mode)
267
+ --yes Skip prompts; non-interactive
268
+ --non-interactive Alias for --yes
269
+ --help, -h Show this help
270
+ `.trim());
271
+ return;
272
+ }
273
+
274
+ // --non-interactive is the canonical alias; --yes kept for backward compat.
275
+ const yesFlag = args.includes('--yes') || args.includes('--non-interactive');
62
276
 
63
277
  const existing = loadConfig(projectRoot);
64
278
  if (existing.exists) {
@@ -73,7 +287,7 @@ async function cmdInit(args) {
73
287
  }
74
288
 
75
289
  // --memory-mode takes precedence over --managed
76
- let mode = 'local-only';
290
+ let mode = 'managed';
77
291
  const modeIdx = args.indexOf('--memory-mode');
78
292
  if (modeIdx !== -1 && args[modeIdx + 1]) {
79
293
  mode = args[modeIdx + 1];
@@ -157,6 +371,356 @@ async function cmdInit(args) {
157
371
  }
158
372
  }
159
373
 
374
+ /**
375
+ * `bizar memory setup` — configure or reconfigure the memory vault.
376
+ *
377
+ * v4.2.0 — first-class bootstrap entry point. Two flows:
378
+ * - No existing config → behaves like `init` + adds the remote (managed mode).
379
+ * - Existing config → optionally updates the remote URL on a managed vault.
380
+ *
381
+ * Flags:
382
+ * --mode <managed|local-only> Vault mode (default: managed)
383
+ * --remote <url> Git remote URL (required for managed)
384
+ * --repo-name <name> Vault directory name (default: bizar-memory)
385
+ * --non-interactive No prompts; use flags or defaults
386
+ * --yes Same as --non-interactive (backward compat)
387
+ *
388
+ * The remote URL is written to BOTH `memoryRepo.remote` (canonical,
389
+ * read by resolveVault) and the top-level `gitRemote` (read by cmdPush).
390
+ * Both must agree or `bizar memory push` will fail silently.
391
+ *
392
+ * Connectivity is checked via `memoryGit.lsRemote` after `addRemote`. A
393
+ * failure here is informational only — credentials may not be configured
394
+ * yet — and does NOT abort the setup.
395
+ *
396
+ * @param {string[]} argv
397
+ */
398
+ export async function cmdSetup(argv) {
399
+ const projectRoot = getProjectRoot();
400
+ const { loadConfig, saveConfig, initVault, resolveVault } = memoryStore;
401
+
402
+ // ── Parse flags ──────────────────────────────────────────────────────────
403
+ const optVal = (name) => {
404
+ const i = argv.indexOf(name);
405
+ if (i === -1) return undefined;
406
+ const v = argv[i + 1];
407
+ if (!v || v.startsWith('-')) return undefined;
408
+ return v;
409
+ };
410
+ const hasFlag = (name) => argv.includes(name);
411
+
412
+ const nonInteractive = hasFlag('--non-interactive') || hasFlag('--yes');
413
+
414
+ // --mode (default managed). Accept --memory-mode alias too.
415
+ let mode = 'managed';
416
+ const modeRaw = optVal('--mode') ?? optVal('--memory-mode');
417
+ if (modeRaw) mode = modeRaw;
418
+ else if (hasFlag('--local-only')) mode = 'local-only';
419
+ else if (hasFlag('--managed')) mode = 'managed';
420
+ if (mode !== 'managed' && mode !== 'local-only') {
421
+ error(`invalid --mode: '${mode}' (must be 'managed' or 'local-only')`);
422
+ process.exit(1);
423
+ }
424
+
425
+ // --remote
426
+ let remote = optVal('--remote');
427
+ if (remote !== undefined) {
428
+ const v = validateRemoteUrl(remote);
429
+ if (!v.valid) {
430
+ error(`invalid --remote URL: ${v.error}`);
431
+ info('examples:');
432
+ info(' --remote git@github.com:user/repo.git');
433
+ info(' --remote ssh://git@github.com/user/repo.git');
434
+ info(' --remote https://github.com/user/repo.git');
435
+ process.exit(1);
436
+ }
437
+ // Store the trimmed canonical form
438
+ remote = remote.trim();
439
+ }
440
+
441
+ // --repo-name (or --memory-repo-name, --repo)
442
+ const repoName = optVal('--repo-name') ?? optVal('--memory-repo-name') ?? optVal('--repo') ?? 'bizar-memory';
443
+
444
+ // ── Help ────────────────────────────────────────────────────────────────
445
+ if (hasFlag('--help') || hasFlag('-h')) {
446
+ console.log(`
447
+ Usage: bizar memory setup [--remote <url>] [--mode <mode>] [--non-interactive]
448
+
449
+ Configure or reconfigure this project's memory vault.
450
+
451
+ Options:
452
+ --mode <managed|local-only> Vault mode (default: managed)
453
+ --remote <url> Git remote URL (required for managed)
454
+ Accepts: ssh://, https://, git@host:path
455
+ --repo-name <name> Vault directory name (default: bizar-memory)
456
+ --local-only Shortcut for --mode local-only
457
+ --non-interactive Skip prompts; use flags or defaults
458
+ --yes Alias for --non-interactive
459
+ --help, -h Show this help
460
+
461
+ Examples:
462
+ # Bootstrap: first-time init with a remote
463
+ bizar memory setup --non-interactive \\
464
+ --remote git@github.com:org/bizar-memory.git
465
+
466
+ # Reconfigure the remote on an existing managed vault
467
+ bizar memory setup --non-interactive \\
468
+ --remote https://github.com/org/bizar-memory.git
469
+
470
+ # Convert to local-only (removes remote but keeps notes)
471
+ bizar memory setup --non-interactive --local-only
472
+ `.trim());
473
+ return;
474
+ }
475
+
476
+ // ── Read existing config ────────────────────────────────────────────────
477
+ const { config: existingCfg, exists: configExists } = loadConfig(projectRoot);
478
+
479
+ // Decide whether this is a bootstrap (no config) or reconfigure (config
480
+ // already exists). The branch shape drives the rest of the function.
481
+ const isBootstrap = !configExists;
482
+
483
+ // Reconfigure path: if no --remote was passed AND --mode wasn't changed,
484
+ // just print status and exit. Otherwise apply the change.
485
+ if (!isBootstrap) {
486
+ const existingMode = existingCfg.memoryRepo?.mode || existingCfg.mode || 'local-only';
487
+ const existingRemote = existingCfg.memoryRepo?.remote || existingCfg.gitRemote || '';
488
+ const modeChanged = mode !== existingMode;
489
+ const remoteChanged = remote !== undefined && remote !== existingRemote;
490
+
491
+ if (!modeChanged && !remoteChanged) {
492
+ // Nothing to do — print status and exit
493
+ console.log(chalk.bold('\n Memory setup (no changes)'));
494
+ console.log(' ─────────────────────────────');
495
+ success('already configured');
496
+ kv('mode', existingMode);
497
+ kv('remote', existingRemote || '(none)');
498
+ kv('vault', existingCfg.memoryRepo?.path || existingCfg.repoName || '(unknown)');
499
+ console.log();
500
+ info(`run \`bizar memory setup --remote <url>\` to change the remote`);
501
+ return;
502
+ }
503
+
504
+ // ── Reconfigure path — apply in place ───────────────────────────────
505
+ const updatedCfg = { ...existingCfg };
506
+
507
+ if (modeChanged && remote === undefined) {
508
+ // Switching modes without a new remote is fine, but warn if going
509
+ // managed without a URL to push to.
510
+ if (mode === 'managed' && !existingRemote) {
511
+ warn('switching to managed mode with no remote configured — push will fail');
512
+ info('pass --remote <url> to set one');
513
+ }
514
+ }
515
+
516
+ if (remote !== undefined) {
517
+ // Dual-write: top-level + memoryRepo.*
518
+ updatedCfg.gitRemote = remote;
519
+ updatedCfg.memoryRepo = {
520
+ ...(existingCfg.memoryRepo || {}),
521
+ mode: mode === 'managed' ? 'managed' : (updatedCfg.memoryRepo?.mode || 'local-only'),
522
+ remote,
523
+ };
524
+ }
525
+ if (modeChanged) {
526
+ updatedCfg.mode = mode;
527
+ if (updatedCfg.memoryRepo) {
528
+ updatedCfg.memoryRepo.mode = mode;
529
+ }
530
+ }
531
+
532
+ const saveResult = saveConfig(projectRoot, updatedCfg);
533
+ if (!saveResult.ok) {
534
+ error(`failed to save config: ${saveResult.error}`);
535
+ process.exit(1);
536
+ }
537
+
538
+ // If we now have a remote on a managed vault, register it.
539
+ if (mode === 'managed' && remote) {
540
+ addRemoteToVault(projectRoot, remote);
541
+ }
542
+
543
+ printSetupResult({ mode, remote, vaultPath: updatedCfg.memoryRepo?.path });
544
+ return;
545
+ }
546
+
547
+ // ── Bootstrap path (no existing config) ─────────────────────────────────
548
+
549
+ // For managed bootstrap, we need a remote. If missing, prompt or error.
550
+ if (mode === 'managed' && !remote) {
551
+ if (nonInteractive) {
552
+ error('--remote is required for --mode managed in non-interactive setup');
553
+ info('pass --remote <url> (or run without --non-interactive to be prompted)');
554
+ process.exit(1);
555
+ }
556
+ // Prompt via readline
557
+ const readline = await import('node:readline');
558
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
559
+ const answer = await new Promise((resolve) => {
560
+ rl.question(chalk.cyan(' remote URL (ssh://, https://, or git@host:path): '), resolve);
561
+ });
562
+ rl.close();
563
+ const trimmed = (answer || '').trim();
564
+ if (!trimmed) {
565
+ error('a remote URL is required for managed mode');
566
+ process.exit(1);
567
+ }
568
+ const v = validateRemoteUrl(trimmed);
569
+ if (!v.valid) {
570
+ error(`invalid remote URL: ${v.error}`);
571
+ process.exit(1);
572
+ }
573
+ remote = trimmed;
574
+ }
575
+
576
+ // Build a config object mirroring cmdInit's shape, with the remote set.
577
+ const home = homedir();
578
+ const projectId = projectRoot.split('/').pop() || 'project';
579
+ const vaultPath = mode === 'managed'
580
+ ? join(home, '.local', 'share', 'bizar', 'memory', repoName)
581
+ : join(projectRoot, '.obsidian');
582
+
583
+ const cfg = {
584
+ version: 1,
585
+ backend: 'bizar-local',
586
+ projectId,
587
+ memoryRepo: {
588
+ mode,
589
+ path: vaultPath,
590
+ remote: mode === 'managed' ? (remote || null) : null,
591
+ branch: 'main',
592
+ namespace: mode === 'managed' ? `projects/${projectId}` : null,
593
+ },
594
+ namespaces: {
595
+ project: `projects/${projectId}`,
596
+ global: 'global/bizar',
597
+ user: `users/${process.env.USER || process.env.USERNAME || 'local'}`,
598
+ },
599
+ lightrag: {
600
+ enabled: false,
601
+ host: '127.0.0.1',
602
+ port: 9621,
603
+ workingDir: join(projectRoot, '.bizar', 'lightrag'),
604
+ },
605
+ git: {
606
+ autoPullOnSessionStart: false,
607
+ autoCommitOnMemoryWrite: false,
608
+ autoPushOnSessionEnd: false,
609
+ commitAuthor: 'Bizar Memory <bizar-memory@local>',
610
+ commitMessageTemplate: `memory(${projectId}): {summary}`,
611
+ },
612
+ };
613
+
614
+ // Top-level gitRemote mirrors memoryRepo.remote so cmdPush works.
615
+ if (mode === 'managed' && remote) {
616
+ cfg.gitRemote = remote;
617
+ }
618
+
619
+ const saveResult = saveConfig(projectRoot, cfg);
620
+ if (!saveResult.ok) {
621
+ error(`failed to write config: ${saveResult.error}`);
622
+ process.exit(1);
623
+ }
624
+
625
+ // Initialize the vault on disk.
626
+ const vaultResult = initVault(projectRoot);
627
+ if (!vaultResult.ok) {
628
+ error(`vault init failed: ${vaultResult.error || 'unknown'}`);
629
+ process.exit(1);
630
+ }
631
+ if (vaultResult.created.length > 0) {
632
+ for (const c of vaultResult.created) info(` created: ${c}`);
633
+ }
634
+
635
+ // For managed bootstrap with a remote, register it on the new vault.
636
+ if (mode === 'managed' && remote) {
637
+ addRemoteToVault(projectRoot, remote);
638
+ }
639
+
640
+ printSetupResult({ mode, remote, vaultPath });
641
+ }
642
+
643
+ /**
644
+ * Register `remote` as `origin` on the vault's git repo, then probe
645
+ * connectivity via `lsRemote`. Both operations are best-effort: we
646
+ * warn on failure but never abort the setup.
647
+ *
648
+ * @param {string} projectRoot
649
+ * @param {string} remote
650
+ */
651
+ function addRemoteToVault(projectRoot, remote) {
652
+ const { resolveVault } = memoryStore;
653
+ const { vaultRoot } = resolveVault(projectRoot);
654
+
655
+ // Sanity check: is the vault a git repo? initVault should have ensured
656
+ // this, but double-check before calling git remote.
657
+ const gitDir = join(vaultRoot, '.git');
658
+ if (!existsSync(gitDir)) {
659
+ warn('vault is not a git repo — skipping remote registration');
660
+ info('run `bizar memory init` or `bizar memory sync` to repair');
661
+ return;
662
+ }
663
+
664
+ if (typeof memoryGit.addRemote !== 'function') {
665
+ warn('memoryGit.addRemote is not available in this build');
666
+ info('(waiting for the addRemote/lsRemote stream to merge) — remote URL saved to config');
667
+ return;
668
+ }
669
+
670
+ const addResult = memoryGit.addRemote(vaultRoot, 'origin', remote);
671
+ if (!addResult || addResult.ok === false) {
672
+ warn(`failed to register remote: ${addResult?.error || 'unknown error'}`);
673
+ info('the remote URL is still saved in .bizar/memory.json');
674
+ return;
675
+ }
676
+ if (addResult.action === 'unchanged') {
677
+ info(`remote 'origin' already set to ${remote}`);
678
+ } else if (addResult.action === 'updated') {
679
+ success(`updated remote 'origin' → ${remote}`);
680
+ } else {
681
+ success(`registered remote 'origin' → ${remote}`);
682
+ }
683
+
684
+ if (typeof memoryGit.lsRemote !== 'function') {
685
+ info('skipping connectivity probe (lsRemote not available yet)');
686
+ return;
687
+ }
688
+
689
+ const probe = memoryGit.lsRemote(vaultRoot, 'origin', { timeoutMs: 5000 });
690
+ if (typeof probe === 'string' && probe.length > 0) {
691
+ const refCount = probe.split('\n').filter(Boolean).length;
692
+ success(`connectivity ok (${refCount} refs reachable)`);
693
+ } else {
694
+ warn('could not reach remote (auth, network, or unknown host)');
695
+ info('the vault is set up; configure credentials and run `bizar memory pull`');
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Render the standard "setup complete" report using the existing `kv`
701
+ * style. The connectivity line is omitted if not relevant.
702
+ *
703
+ * @param {{ mode: string, remote: string|undefined|null, vaultPath: string }} opts
704
+ */
705
+ function printSetupResult({ mode, remote, vaultPath }) {
706
+ console.log();
707
+ success('memory setup complete');
708
+ console.log(' ─────────────────────────────');
709
+ kv('mode', mode);
710
+ kv('vault', String(vaultPath || ''));
711
+ if (remote) kv('remote', remote);
712
+ console.log();
713
+ if (!remote) {
714
+ info('local-only mode — notes are stored in this project only');
715
+ info('run `bizar memory setup --remote <url>` to attach a remote later');
716
+ } else {
717
+ info('next steps:');
718
+ info(' 1. `bizar memory pull` to fetch the existing vault (if any)');
719
+ info(' 2. `bizar memory write <relpath>` to start adding notes');
720
+ info(' 3. `bizar memory push` to publish');
721
+ }
722
+ }
723
+
160
724
  /**
161
725
  * `bizar memory status`
162
726
  */
@@ -204,15 +768,28 @@ async function cmdStatus(_args) {
204
768
  }
205
769
  }
206
770
 
207
- // LightRAG stub
208
- kv('lightrag', chalk.gray('stub (Phase 2)'));
771
+ // LightRAG status
772
+ try {
773
+ const { isLightRAGRunning, isLightRAGInstalled, resolveLightRAGConfig } = memoryStore;
774
+ const cfg = resolveLightRAGConfig(projectRoot);
775
+ if (!cfg.enabled) {
776
+ kv('lightrag', chalk.gray('disabled'));
777
+ } else if (!(await isLightRAGInstalled())) {
778
+ kv('lightrag', chalk.yellow('not installed — `uv tool install "lightrag-hku[api]"`'));
779
+ } else {
780
+ const running = await isLightRAGRunning(cfg);
781
+ kv('lightrag', running ? chalk.green(`${cfg.host}:${cfg.port} (running)`) : chalk.yellow(`${cfg.host}:${cfg.port} (not running — run \`bizar memory reindex\`)`));
782
+ }
783
+ } catch (err) {
784
+ kv('lightrag', chalk.gray(`status unavailable (${err.message})`));
785
+ }
209
786
 
210
787
  // Last reindex attempt
211
- const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex-attempt.json');
788
+ const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex.json');
212
789
  if (existsSync(reindexMarker)) {
213
790
  try {
214
- const { attemptedAt, ok } = JSON.parse(readFileSync(reindexMarker, 'utf8'));
215
- kv('lastReindex', `${new Date(attemptedAt).toLocaleString()} — ${ok ? chalk.green('ok') : chalk.red('failed')}`);
791
+ const { finishedAt, ok, inserted, failed, noteCount } = JSON.parse(readFileSync(reindexMarker, 'utf8'));
792
+ kv('lastReindex', `${new Date(finishedAt).toLocaleString()} — ${ok ? chalk.green(`${inserted}/${noteCount} ok`) : chalk.red(`${failed} failed`)}`);
216
793
  } catch { /* ignore */ }
217
794
  }
218
795
 
@@ -320,7 +897,7 @@ async function cmdUnlink(_args) {
320
897
  async function cmdPull(_args) {
321
898
  const projectRoot = getProjectRoot();
322
899
  const { loadConfig, resolveVault } = memoryStore;
323
- const { pull, isGitInstalled } = memoryGit;
900
+ const { pull, isGitInstalled, ensureUpstream } = memoryGit;
324
901
 
325
902
  const { config } = loadConfig(projectRoot);
326
903
  if (config.mode === 'local-only') {
@@ -333,7 +910,18 @@ async function cmdPull(_args) {
333
910
  process.exit(1);
334
911
  }
335
912
 
336
- const { vaultRoot } = resolveVault(projectRoot);
913
+ const { vaultRoot, branch } = resolveVault(projectRoot);
914
+ const remoteName = config.gitRemote ? 'origin' : 'origin';
915
+
916
+ // Pre-flight: ensure upstream is set. Without this, `git pull` fails with
917
+ // "no tracking information" on freshly-cloned or locally-initialized vaults.
918
+ const upstreamResult = ensureUpstream(vaultRoot, config.branch || branch || 'main', remoteName);
919
+ if (!upstreamResult.ok) {
920
+ info(`upstream not set (${upstreamResult.error}); pull may fail`);
921
+ } else if (upstreamResult.action === 'set') {
922
+ success(`set upstream to ${remoteName}/${config.branch || branch || 'main'}`);
923
+ }
924
+
337
925
  info(`pulling into ${vaultRoot}`);
338
926
  const result = pull(vaultRoot);
339
927
  if (!result.ok) {
@@ -426,7 +1014,7 @@ async function cmdPush(_args) {
426
1014
  async function cmdSync(args) {
427
1015
  const projectRoot = getProjectRoot();
428
1016
  const { loadConfig, resolveVault, validateAll, scanForSecrets, listNotes } = memoryStore;
429
- const { pull, addAll, commit: gitCommit, push: gitPush, status: gitStatus, acquireLock, isGitInstalled } = memoryGit;
1017
+ const { pull, addAll, commit: gitCommit, push: gitPush, status: gitStatus, acquireLock, isGitInstalled, ensureUpstream } = memoryGit;
430
1018
 
431
1019
  const { config } = loadConfig(projectRoot);
432
1020
  if (config.mode === 'local-only') {
@@ -439,7 +1027,19 @@ async function cmdSync(args) {
439
1027
  process.exit(1);
440
1028
  }
441
1029
 
442
- const { vaultRoot } = resolveVault(projectRoot);
1030
+ const { vaultRoot, branch } = resolveVault(projectRoot);
1031
+ const branchName = config.branch || branch || 'main';
1032
+
1033
+ // Pre-flight: ensure upstream is set. Some workflows create a vault before
1034
+ // origin exists; without upstream, `git pull` fails with "no tracking".
1035
+ if (config.gitRemote) {
1036
+ const upstreamResult = ensureUpstream(vaultRoot, branchName, 'origin');
1037
+ if (!upstreamResult.ok) {
1038
+ info(`upstream not set (${upstreamResult.error}); pull may fail`);
1039
+ } else if (upstreamResult.action === 'set') {
1040
+ success(`set upstream to origin/${branchName}`);
1041
+ }
1042
+ }
443
1043
 
444
1044
  // Acquire lock
445
1045
  const lock = acquireLock(vaultRoot);
@@ -543,24 +1143,115 @@ async function cmdSync(args) {
543
1143
 
544
1144
  /**
545
1145
  * `bizar memory reindex`
546
- * STUBPhase 2 will implement LightRAG integration.
1146
+ * v4.1.0populates the LightRAG server with every note in the vault.
1147
+ *
1148
+ * Steps:
1149
+ * 1. Resolve LightRAG config from `.bizar/memory.json`.
1150
+ * 2. Ensure `lightrag-server` is installed (searches common paths).
1151
+ * 3. Ensure server is running (auto-starts if not).
1152
+ * 4. List all notes from the vault.
1153
+ * 5. Insert each note via `POST /documents` with stable id
1154
+ * `bizar://<projectId>/<relPath>`.
1155
+ * 6. Write marker file with stats.
1156
+ *
1157
+ * The reindex is idempotent — re-running re-inserts the same notes under
1158
+ * the same ids; LightRAG's id-keyed storage handles this gracefully.
547
1159
  */
548
- async function cmdReindex(_args) {
1160
+ async function cmdReindex(args) {
549
1161
  const projectRoot = getProjectRoot();
550
- const { atomicWriteJson } = await import('./atomic.mjs').then((m) => m);
1162
+ const _atomic = await import('./atomic.mjs').then((m) => m);
551
1163
  const cacheDir = join(projectRoot, '.bizar', 'memory-cache');
552
- if (!existsSync(cacheDir)) {
553
- mkdirSync(cacheDir, { recursive: true });
554
- }
555
- const marker = join(cacheDir, 'last-reindex-attempt.json');
556
- const result = {
557
- attemptedAt: new Date().toISOString(),
558
- ok: false,
559
- reason: 'lightrag-not-implemented',
560
- };
561
- atomicWriteJson(marker, result);
562
- console.log(chalk.yellow('reindex: LightRAG runtime not yet implemented — Phase 2'));
563
- console.log(` marker written to ${marker}`);
1164
+ mkdirSync(cacheDir, { recursive: true });
1165
+
1166
+ const { isLightRAGInstalled, reindexVault, resolveLightRAGConfig } = memoryStore;
1167
+
1168
+ // Preflight: is lightrag-server installed?
1169
+ if (!(await isLightRAGInstalled())) {
1170
+ error('lightrag-server not installed');
1171
+ info('install it with: uv tool install "lightrag-hku[api]"');
1172
+ info('then run `bizar memory reindex` again');
1173
+ process.exit(1);
1174
+ }
1175
+
1176
+ // Preflight: is lightrag enabled in config?
1177
+ const config = resolveLightRAGConfig(projectRoot);
1178
+ if (!config.enabled) {
1179
+ warn('lightrag is disabled in .bizar/memory.json (lightrag.enabled = false)');
1180
+ info('set lightrag.enabled = true and run `bizar memory reindex` again');
1181
+ return;
1182
+ }
1183
+
1184
+ info(`reindexing vault into LightRAG at http://${config.host}:${config.port}…`);
1185
+ const result = await reindexVault(projectRoot, { logger: console });
1186
+
1187
+ if (!result.ok && result.inserted === 0) {
1188
+ error(`reindex failed: ${result.error || 'unknown'}`);
1189
+ if (result.failures?.length > 0) {
1190
+ info(`first failure: ${result.failures[0].relPath} — ${result.failures[0].error}`);
1191
+ }
1192
+ process.exit(1);
1193
+ }
1194
+
1195
+ if (result.started) {
1196
+ success(`started LightRAG server (pid ${result.pid})`);
1197
+ }
1198
+ success(`reindexed ${result.inserted}/${result.noteCount} notes in ${result.durationMs}ms`);
1199
+ if (result.failed > 0) {
1200
+ warn(`${result.failed} notes failed — see marker for details`);
1201
+ }
1202
+ info(`marker: ${result.markerPath}`);
1203
+ }
1204
+
1205
+ /**
1206
+ * `bizar memory search <query>`
1207
+ * v4.1.0 — merged lexical + semantic search.
1208
+ *
1209
+ * - Lexical: token-frequency matching against vault notes (instant, free).
1210
+ * - Semantic: LightRAG `/query` with `mode: 'mix'` (combines local entity
1211
+ * graph + global relation graph). Skipped if LightRAG isn't running.
1212
+ */
1213
+ async function cmdSearch(args) {
1214
+ const query = args.join(' ').trim();
1215
+ if (!query) {
1216
+ error('search requires a query: `bizar memory search <query>`');
1217
+ process.exit(1);
1218
+ }
1219
+ const projectRoot = getProjectRoot();
1220
+ const { searchVault } = memoryStore;
1221
+
1222
+ // Lexical
1223
+ const lexical = searchVault(projectRoot, query, { limit: 10 });
1224
+ console.log(chalk.bold('\n Lexical results (token-frequency):'));
1225
+ if (lexical.length === 0) {
1226
+ console.log(chalk.dim(' (no matches)'));
1227
+ } else {
1228
+ for (const r of lexical.slice(0, 5)) {
1229
+ console.log(` ${chalk.cyan(r.relPath)} ${chalk.dim(`(score ${r.score})`)}`);
1230
+ console.log(chalk.dim(` ${r.snippet}`));
1231
+ }
1232
+ }
1233
+
1234
+ // Semantic (best-effort)
1235
+ console.log(chalk.bold('\n Semantic answer (LightRAG):'));
1236
+ try {
1237
+ const cfg = memoryStore.resolveLightRAGConfig(projectRoot);
1238
+ const semantic = await memoryLightrag.query(cfg, query, { topK: 10 });
1239
+ if (!semantic.ok) {
1240
+ console.log(chalk.dim(` ${semantic.error || 'unavailable'}`));
1241
+ if (semantic.error?.includes('not running')) {
1242
+ console.log(chalk.dim(' tip: run `bizar memory reindex` to start + populate LightRAG'));
1243
+ }
1244
+ } else {
1245
+ const response = semantic.response?.response || semantic.response?.answer || '';
1246
+ if (response) {
1247
+ console.log(` ${response.split('\n').join('\n ')}`);
1248
+ } else {
1249
+ console.log(chalk.dim(' (no answer returned)'));
1250
+ }
1251
+ }
1252
+ } catch (err) {
1253
+ console.log(chalk.dim(` ${err.message}`));
1254
+ }
564
1255
  }
565
1256
 
566
1257
  /**
@@ -587,7 +1278,7 @@ async function cmdConflicts(_args) {
587
1278
  const filePath = join(vaultRoot, note.relPath);
588
1279
  try {
589
1280
  const content = rf(filePath, 'utf8');
590
- if (/^<{7}\s|^={7}\s|>{7}\s/.test(content)) {
1281
+ if (/^<{7}\s|^={7}\s|^>{7}\s/m.test(content)) {
591
1282
  conflicts.push({ relPath: note.relPath, reason: 'git conflict markers detected' });
592
1283
  }
593
1284
  } catch { /* skip */ }
@@ -655,17 +1346,35 @@ async function cmdDoctor(_args) {
655
1346
  allPassed = false;
656
1347
  }
657
1348
 
658
- // Check 7: last secret scan
659
- const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex-attempt.json');
1349
+ // Check 7: last reindex
1350
+ const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex.json');
660
1351
  if (existsSync(reindexMarker)) {
661
1352
  try {
662
- const { attemptedAt } = JSON.parse(readFileSync(reindexMarker, 'utf8'));
663
- const ageMs = Date.now() - new Date(attemptedAt).getTime();
1353
+ const { finishedAt, ok, inserted, failed, noteCount } = JSON.parse(readFileSync(reindexMarker, 'utf8'));
1354
+ const ageMs = Date.now() - new Date(finishedAt).getTime();
664
1355
  const ageH = ageMs / 3600000;
665
- kv('lastReindex', ageH < 24 ? chalk.green(`${Math.round(ageH)}h ago`) : chalk.yellow(`${Math.round(ageH)}h ago`));
1356
+ const summary = `${inserted}/${noteCount} inserted${failed > 0 ? `, ${failed} failed` : ''}`;
1357
+ const status = ok ? chalk.green(summary) : chalk.red(summary);
1358
+ kv('lastReindex', ageH < 24 ? `${status} ${chalk.dim(`(${Math.round(ageH)}h ago)`)}` : `${status} ${chalk.yellow(`(${Math.round(ageH)}h ago)`)}`);
666
1359
  } catch { /* ignore */ }
667
1360
  } else {
668
- kv('lastReindex', chalk.yellow('never'));
1361
+ kv('lastReindex', chalk.yellow('never — run `bizar memory reindex`'));
1362
+ }
1363
+
1364
+ // Check 8: LightRAG status
1365
+ try {
1366
+ const { isLightRAGInstalled, isLightRAGRunning, resolveLightRAGConfig } = memoryStore;
1367
+ const cfg = resolveLightRAGConfig(projectRoot);
1368
+ if (!cfg.enabled) {
1369
+ kv('lightrag', chalk.gray('disabled'));
1370
+ } else if (!(await isLightRAGInstalled())) {
1371
+ kv('lightrag', chalk.yellow('not installed — `uv tool install "lightrag-hku[api]"`'));
1372
+ } else {
1373
+ const running = await isLightRAGRunning(cfg);
1374
+ kv('lightrag', running ? chalk.green(`${cfg.host}:${cfg.port} running`) : chalk.yellow(`not running — run \`bizar memory reindex\``));
1375
+ }
1376
+ } catch (err) {
1377
+ kv('lightrag', chalk.gray(`status check failed: ${err.message}`));
669
1378
  }
670
1379
  }
671
1380
 
@@ -678,6 +1387,189 @@ async function cmdDoctor(_args) {
678
1387
  console.log();
679
1388
  }
680
1389
 
1390
+ // ─── read / list / delete (Fix 4 + Fix 5 plumbing) ────────────────────────────
1391
+
1392
+ /**
1393
+ * Parse `--namespace <ns>` and return the chosen namespace plus the remaining
1394
+ * positional args. Defaults to 'project' when the flag is absent.
1395
+ */
1396
+ function parseNamespaceFlag(args) {
1397
+ const idx = args.indexOf('--namespace');
1398
+ if (idx === -1) return { namespace: 'project', rest: args };
1399
+ const v = args[idx + 1];
1400
+ if (!v || v.startsWith('-')) return { namespace: 'project', rest: args };
1401
+ // Remove the flag and its value from the rest
1402
+ const rest = args.filter((a, i) => i !== idx && i !== idx + 1);
1403
+ return { namespace: v, rest };
1404
+ }
1405
+
1406
+ /**
1407
+ * `bizar memory read [--namespace <ns>] <relpath>`
1408
+ *
1409
+ * Read a single note and print its raw content (frontmatter + body) to stdout.
1410
+ */
1411
+ async function cmdRead(args) {
1412
+ const projectRoot = getProjectRoot();
1413
+ const { resolveVault, resolveNamespaceRoot } = memoryStore;
1414
+
1415
+ if (args.includes('--help') || args.includes('-h')) {
1416
+ console.log(`
1417
+ Usage: bizar memory read [--namespace <ns>] <relpath>
1418
+
1419
+ Read a single note and print it to stdout (YAML frontmatter + body).
1420
+
1421
+ Options:
1422
+ --namespace <project|global|user> Namespace to read from (default: project)
1423
+ --help, -h Show this help
1424
+ `.trim());
1425
+ return;
1426
+ }
1427
+
1428
+ const { namespace, rest } = parseNamespaceFlag(args);
1429
+ const relpath = rest.filter((a) => !a.startsWith('-'))[0];
1430
+ if (!relpath) {
1431
+ error('relpath is required: `bizar memory read <relpath>`');
1432
+ info('run `bizar memory read --help` for usage');
1433
+ process.exit(1);
1434
+ }
1435
+
1436
+ let note;
1437
+ if (namespace === 'project') {
1438
+ note = memoryStore.readNote(projectRoot, relpath);
1439
+ } else {
1440
+ const vi = resolveVault(projectRoot);
1441
+ const root = resolveNamespaceRoot(vi, namespace);
1442
+ if (!root) {
1443
+ error(`unknown namespace: ${namespace}`);
1444
+ process.exit(1);
1445
+ }
1446
+ note = memoryStore.readNote(projectRoot, relpath, { root });
1447
+ }
1448
+
1449
+ if (!note) {
1450
+ error(`note not found: ${relpath}`);
1451
+ process.exit(1);
1452
+ }
1453
+ process.stdout.write(note.raw);
1454
+ if (!note.raw.endsWith('\n')) process.stdout.write('\n');
1455
+ }
1456
+
1457
+ /**
1458
+ * `bizar memory list [--namespace <ns>] [--json] [dir]`
1459
+ *
1460
+ * List notes under a directory. Default: walk the project namespace root.
1461
+ * Pass `dir` (e.g. `decisions/`) to scope to a subdirectory. The dir is
1462
+ * resolved RELATIVE TO the namespace root.
1463
+ */
1464
+ async function cmdList(args) {
1465
+ const projectRoot = getProjectRoot();
1466
+ const { resolveVault, resolveNamespaceRoot } = memoryStore;
1467
+
1468
+ if (args.includes('--help') || args.includes('-h')) {
1469
+ console.log(`
1470
+ Usage: bizar memory list [--namespace <ns>] [dir]
1471
+
1472
+ List notes under a directory. Default: the project namespace root.
1473
+
1474
+ Options:
1475
+ --namespace <project|global|user> Namespace to list (default: project)
1476
+ --json Emit JSON array
1477
+ [dir] Subdirectory under the namespace (e.g. decisions/)
1478
+ --help, -h Show this help
1479
+ `.trim());
1480
+ return;
1481
+ }
1482
+
1483
+ const { namespace, rest } = parseNamespaceFlag(args);
1484
+ const asJson = rest.includes('--json');
1485
+ const dir = rest.filter((a) => !a.startsWith('-'))[0] || '';
1486
+
1487
+ let notes;
1488
+ if (namespace === 'project') {
1489
+ notes = memoryStore.listNotes(projectRoot, dir ? { namespace: dir } : {});
1490
+ } else {
1491
+ const vi = resolveVault(projectRoot);
1492
+ const root = resolveNamespaceRoot(vi, namespace);
1493
+ if (!root) {
1494
+ error(`unknown namespace: ${namespace}`);
1495
+ process.exit(1);
1496
+ }
1497
+ notes = memoryStore.listNotes(projectRoot, dir ? { root, namespace: dir } : { root });
1498
+ }
1499
+
1500
+ // listNotes returns relPaths RELATIVE to the search root, so when we
1501
+ // filter by `dir` we need to re-attach the dir prefix for display —
1502
+ // otherwise the user sees `0001-foo.md` instead of the more useful
1503
+ // `decisions/0001-foo.md`. Strip a trailing slash for cleanliness.
1504
+ const displayPrefix = dir ? dir.replace(/\/+$/, '') + '/' : '';
1505
+
1506
+ if (asJson) {
1507
+ console.log(JSON.stringify(notes.map((n) => ({
1508
+ relPath: displayPrefix + n.relPath,
1509
+ size: n.size,
1510
+ mtime: n.mtime,
1511
+ })), null, 2));
1512
+ return;
1513
+ }
1514
+ if (notes.length === 0) {
1515
+ console.log(chalk.dim(' (no notes)'));
1516
+ return;
1517
+ }
1518
+ for (const n of notes) {
1519
+ console.log(` ${displayPrefix}${n.relPath}`);
1520
+ }
1521
+ }
1522
+
1523
+ /**
1524
+ * `bizar memory delete [--namespace <ns>] <relpath>`
1525
+ *
1526
+ * Delete a single note from the vault.
1527
+ */
1528
+ async function cmdDelete(args) {
1529
+ const projectRoot = getProjectRoot();
1530
+ const { resolveVault, resolveNamespaceRoot } = memoryStore;
1531
+
1532
+ if (args.includes('--help') || args.includes('-h')) {
1533
+ console.log(`
1534
+ Usage: bizar memory delete [--namespace <ns>] <relpath>
1535
+
1536
+ Delete a single note from the vault.
1537
+
1538
+ Options:
1539
+ --namespace <project|global|user> Namespace to delete from (default: project)
1540
+ --help, -h Show this help
1541
+ `.trim());
1542
+ return;
1543
+ }
1544
+
1545
+ const { namespace, rest } = parseNamespaceFlag(args);
1546
+ const relpath = rest.filter((a) => !a.startsWith('-'))[0];
1547
+ if (!relpath) {
1548
+ error('relpath is required: `bizar memory delete <relpath>`');
1549
+ info('run `bizar memory delete --help` for usage');
1550
+ process.exit(1);
1551
+ }
1552
+
1553
+ let ok;
1554
+ if (namespace === 'project') {
1555
+ ok = memoryStore.deleteNote(projectRoot, relpath);
1556
+ } else {
1557
+ const vi = resolveVault(projectRoot);
1558
+ const root = resolveNamespaceRoot(vi, namespace);
1559
+ if (!root) {
1560
+ error(`unknown namespace: ${namespace}`);
1561
+ process.exit(1);
1562
+ }
1563
+ ok = memoryStore.deleteNote(projectRoot, relpath, { root });
1564
+ }
1565
+
1566
+ if (!ok) {
1567
+ error(`note not found: ${relpath}`);
1568
+ process.exit(1);
1569
+ }
1570
+ success(`deleted ${relpath}`);
1571
+ }
1572
+
681
1573
  // ─── Dispatcher ──────────────────────────────────────────────────────────────
682
1574
 
683
1575
  /**
@@ -689,6 +1581,9 @@ export async function runMemory(subcommand, args) {
689
1581
  case 'init':
690
1582
  await cmdInit(args);
691
1583
  break;
1584
+ case 'setup':
1585
+ await cmdSetup(args);
1586
+ break;
692
1587
  case 'status':
693
1588
  await cmdStatus(args);
694
1589
  break;
@@ -698,6 +1593,18 @@ export async function runMemory(subcommand, args) {
698
1593
  case 'unlink':
699
1594
  await cmdUnlink(args);
700
1595
  break;
1596
+ case 'write':
1597
+ await cmdWrite(args);
1598
+ break;
1599
+ case 'read':
1600
+ await cmdRead(args);
1601
+ break;
1602
+ case 'list':
1603
+ await cmdList(args);
1604
+ break;
1605
+ case 'delete':
1606
+ await cmdDelete(args);
1607
+ break;
701
1608
  case 'pull':
702
1609
  await cmdPull(args);
703
1610
  break;
@@ -713,6 +1620,9 @@ export async function runMemory(subcommand, args) {
713
1620
  case 'reindex':
714
1621
  await cmdReindex(args);
715
1622
  break;
1623
+ case 'search':
1624
+ await cmdSearch(args);
1625
+ break;
716
1626
  case 'conflicts':
717
1627
  await cmdConflicts(args);
718
1628
  break;
@@ -740,9 +1650,14 @@ function showHelp() {
740
1650
 
741
1651
  Subcommands:
742
1652
  init Initialize memory config for this project
1653
+ setup Configure or reconfigure the memory vault (mode + remote)
743
1654
  status Show current memory configuration
744
1655
  link <path> Link to a shared memory repo (clone if URL)
745
1656
  unlink Revert to local-only mode
1657
+ write Write a single note to the vault
1658
+ read Read a single note and print it to stdout
1659
+ list List notes (optionally filtered by namespace + dir)
1660
+ delete Delete a single note from the vault
746
1661
  pull Git pull from the shared repo
747
1662
  commit [-m] Git commit staged changes
748
1663
  push Git push to the shared repo