@polderlabs/bizar 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) 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 +80 -0
  6. package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
  7. package/bizar-dash/src/server/memory-store.mjs +47 -0
  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.test.mjs +542 -0
  14. package/bizar-dash/tests/memory-config.test.mjs +422 -0
  15. package/bizar-dash/tests/memory-git.test.mjs +109 -1
  16. package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
  17. package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
  18. package/cli/banner.mjs +1 -1
  19. package/cli/bin.mjs +4 -4
  20. package/cli/bootstrap.mjs +1 -1
  21. package/cli/copy.mjs +22 -16
  22. package/cli/doctor.mjs +4 -4
  23. package/cli/doctor.test.mjs +2 -2
  24. package/cli/init.mjs +2 -2
  25. package/cli/install.mjs +21 -16
  26. package/cli/memory.mjs +710 -31
  27. package/cli/utils.mjs +6 -3
  28. package/config/AGENTS.md +7 -7
  29. package/config/agents/_shared/AGENT_BASELINE.md +59 -61
  30. package/config/opencode.json +13 -38
  31. package/config/skills/memory-protocol/SKILL.md +105 -0
  32. package/config/skills/obsidian/SKILL.md +58 -1
  33. package/install.sh +11 -1
  34. package/package.json +2 -2
  35. package/plugins/bizar/index.ts +7 -0
  36. package/plugins/bizar/src/commands.ts +42 -1
  37. package/plugins/bizar/src/tools/open-kb.ts +191 -0
  38. package/plugins/bizar/tests/commands.test.ts +36 -0
@@ -19,6 +19,7 @@ import { parseFrontmatter, serializeFrontmatter } from './yaml.mjs';
19
19
  import { validateNote } from './memory-schema.mjs';
20
20
  import { scan as scanSecrets, hasHighFindings } from './memory-secrets.mjs';
21
21
  import { atomicWriteJson, safeReadJSON, safeReadText } from '../../../cli/atomic.mjs';
22
+ import * as memoryGit from './memory-git.mjs';
22
23
 
23
24
  const HOME = homedir();
24
25
  const BIZAR_MEMORY_ROOT = join(HOME, '.local', 'share', 'bizar', 'memory');
@@ -333,6 +334,33 @@ export function writeNote(projectRoot, relPath, { frontmatter, body }) {
333
334
  const content = `---\n${yamlBlock}\n---\n\n${body}`;
334
335
  writeFileSync(filePath, content, 'utf8');
335
336
 
337
+ // Auto-commit the written file to git (if configured).
338
+ // The lock serializes commits, not writes — concurrent writes will each
339
+ // trigger their own commit, which is the intended behaviour.
340
+ (() => {
341
+ try {
342
+ if (!memoryGit.isGitInstalled()) return;
343
+ const { config } = loadConfig(projectRoot);
344
+ if (config.mode === 'local-only') return;
345
+ if (config.git?.autoCommitOnMemoryWrite !== true) return;
346
+ const lockResult = memoryGit.acquireLock(vaultRoot);
347
+ if (!lockResult || lockResult.error) return; // skip if locked or error
348
+ try {
349
+ memoryGit.addFile(vaultRoot, relPath);
350
+ const summary = frontmatter.title || relPath;
351
+ const message = (config.git.commitMessageTemplate || 'memory(BizarHarness): {summary}')
352
+ .replace('{summary}', summary);
353
+ memoryGit.commit(vaultRoot, message, { author: config.git.commitAuthor });
354
+ } finally {
355
+ lockResult.release();
356
+ }
357
+ } catch (err) {
358
+ // Log but never re-throw — the write already succeeded; a failed
359
+ // auto-commit is recoverable on next write or manual commit.
360
+ console.error('[memory-store] autoCommitOnMemoryWrite failed:', err?.message || err);
361
+ }
362
+ })();
363
+
336
364
  const st = statSync(filePath);
337
365
  return {
338
366
  relPath,
@@ -422,3 +450,22 @@ export function validateAll(projectRoot) {
422
450
  }
423
451
  return results;
424
452
  }
453
+
454
+ // ── LightRAG integration (v4.1.0) ──────────────────────────────────────────
455
+ //
456
+ // Re-export the LightRAG orchestrator from the memory-store module so the
457
+ // dashboard, CLI, and tests have a single import surface. The actual
458
+ // implementation lives in `memory-lightrag.mjs`.
459
+
460
+ export {
461
+ resolveLightRAGConfig,
462
+ isInstalled as isLightRAGInstalled,
463
+ isRunning as isLightRAGRunning,
464
+ startServer as startLightRAG,
465
+ stopServer as stopLightRAG,
466
+ ensureRunning as ensureLightRAGRunning,
467
+ insertNote as insertLightRAGNote,
468
+ insertAllNotes as insertLightRAGAll,
469
+ reindexVault,
470
+ query as queryLightRAG,
471
+ } from './memory-lightrag.mjs';
@@ -22,7 +22,7 @@
22
22
  * Path-traversal attacks (e.g. `../`) are blocked.
23
23
  * 3. **Subprocess allowlist** — only whitelisted binaries
24
24
  * (`bizar`, `opencode`, `python3`, `graphify`, `git`, `node`,
25
- * `npm`, `pip`, `pipx`, `uv`, `rtk`) can be spawned. Custom
25
+ * `npm`, `pip`, `pipx`, `uv`, `headroom`) can be spawned. Custom
26
26
  * binaries require an explicit `process:spawn:<bin>` permission.
27
27
  * 4. **Audit log** — every privileged operation (fs read/write,
28
28
  * process spawn, network fetch) is logged to
@@ -73,7 +73,7 @@ export const ALLOWED_BINARIES = new Set([
73
73
  'npm',
74
74
  'npx',
75
75
  'git',
76
- 'rtk',
76
+ 'headroom',
77
77
  'jq',
78
78
  ]);
79
79
 
@@ -23,6 +23,28 @@ const { atomicWriteJson } = await import(`${SERVER_ROOT}/../../../cli/atomic.mjs
23
23
 
24
24
  const { wrap } = await import('./_shared.mjs').then((m) => m);
25
25
 
26
+ // Lazy import to avoid circular dependency with memory-lightrag.mjs
27
+ async function getMemoryLightrag() {
28
+ return import(`${SERVER_ROOT}/memory-lightrag.mjs`).then((m) => m);
29
+ }
30
+
31
+ /**
32
+ * Redact apiKey in the lightrag block before sending to the UI.
33
+ * apiKey '***' means it was already redacted.
34
+ * Any non-empty string that is not '***' is replaced with '***'.
35
+ */
36
+ function redactLightRAGConfig(config) {
37
+ if (!config || !config.lightrag) return config;
38
+ const { lightrag, ...rest } = config;
39
+ const redactedLightrag = {
40
+ ...lightrag,
41
+ apiKey: lightrag.apiKey && lightrag.apiKey !== '' && lightrag.apiKey !== '***'
42
+ ? '***'
43
+ : lightrag.apiKey || undefined,
44
+ };
45
+ return { ...rest, lightrag: redactedLightrag };
46
+ }
47
+
26
48
  export function createMemoryRouter({ projectRoot }) {
27
49
  const router = Router();
28
50
 
@@ -82,12 +104,42 @@ export function createMemoryRouter({ projectRoot }) {
82
104
  router.get('/memory/config', wrap(async (_req, res) => {
83
105
  const { loadConfig } = memoryStore;
84
106
  const { config, exists } = loadConfig(projectRoot);
85
- res.json({ exists, config });
107
+ res.json({ exists, config: redactLightRAGConfig(config) });
86
108
  }));
87
109
 
88
110
  // POST /memory/config
89
111
  router.post('/memory/config', wrap(async (req, res) => {
90
112
  const { loadConfig, saveConfig } = memoryStore;
113
+
114
+ // ── Patch mode: merge only the lightrag block ──────────────────────────
115
+ if (req.body && req.body.patch === true) {
116
+ const { writeLightRAGConfig } = await getMemoryLightrag();
117
+ const { lightrag, patch: _patch, ...restPatch } = req.body;
118
+
119
+ // Validate no unexpected top-level fields in patch
120
+ if (Object.keys(restPatch).length > 0) {
121
+ res.status(400).json({ error: 'bad_request', message: 'patch mode only accepts a "lightrag" field' });
122
+ return;
123
+ }
124
+ if (!lightrag || typeof lightrag !== 'object') {
125
+ res.status(400).json({ error: 'bad_request', message: 'patch mode requires a "lightrag" object' });
126
+ return;
127
+ }
128
+
129
+ const result = writeLightRAGConfig(projectRoot, lightrag);
130
+ if (!result.ok) {
131
+ res.status(400).json({ error: 'validation_error', message: result.error });
132
+ return;
133
+ }
134
+
135
+ // Return full config with redacted lightrag block
136
+ const { config: full } = loadConfig(projectRoot);
137
+ const redacted = redactLightRAGConfig(full);
138
+ res.json({ ok: true, config: redacted });
139
+ return;
140
+ }
141
+
142
+ // ── Full replace mode (existing behaviour) ───────────────────────────────
91
143
  const { config: newConfig, confirm } = req.body || {};
92
144
  if (!newConfig || typeof newConfig !== 'object') {
93
145
  res.status(400).json({ error: 'bad_request', message: 'body must include config object' });
@@ -111,7 +163,8 @@ export function createMemoryRouter({ projectRoot }) {
111
163
  res.status(500).json({ error: 'write_failed', message: result.error });
112
164
  return;
113
165
  }
114
- res.json({ ok: true, config: merged });
166
+ const redacted = redactLightRAGConfig(merged);
167
+ res.json({ ok: true, config: redacted });
115
168
  }));
116
169
 
117
170
  // GET /memory/notes
@@ -437,20 +490,34 @@ export function createMemoryRouter({ projectRoot }) {
437
490
  res.json({ conflicts });
438
491
  }));
439
492
 
440
- // POST /memory/reindex — STUB
493
+ // POST /memory/reindex — v4.1.0 real LightRAG population
441
494
  router.post('/memory/reindex', wrap(async (_req, res) => {
442
- const cacheDir = join(projectRoot, '.bizar', 'memory-cache');
443
- if (!existsSync(cacheDir)) {
444
- mkdirSync(cacheDir, { recursive: true });
445
- }
446
- const marker = join(cacheDir, 'last-reindex-attempt.json');
447
- const data = {
448
- attemptedAt: new Date().toISOString(),
449
- ok: false,
450
- reason: 'lightrag-not-implemented',
451
- };
452
- atomicWriteJson(marker, data);
453
- res.json({ ok: false, stub: true });
495
+ const { reindexVault } = memoryStore;
496
+ const result = await reindexVault(projectRoot, {});
497
+ if (!result.ok && result.inserted === 0) {
498
+ res.status(422).json(result);
499
+ return;
500
+ }
501
+ res.json(result);
502
+ }));
503
+
504
+ // GET /memory/query?q=...&topK=10 — merged lexical + semantic search
505
+ router.get('/memory/query', wrap(async (req, res) => {
506
+ const q = String(req.query.q || '').trim();
507
+ if (!q) {
508
+ res.status(400).json({ error: 'q required' });
509
+ return;
510
+ }
511
+ const topK = Math.min(parseInt(req.query.topK, 10) || 10, 50);
512
+ const { searchVault, queryLightRAG } = memoryStore;
513
+ const lexical = searchVault(projectRoot, q, { limit: topK });
514
+ let semantic = null;
515
+ try {
516
+ semantic = await queryLightRAG(projectRoot, q, { topK });
517
+ } catch (err) {
518
+ semantic = { ok: false, error: err.message };
519
+ }
520
+ res.json({ ok: true, q, lexical, semantic });
454
521
  }));
455
522
 
456
523
  // GET /memory/doctor
@@ -495,5 +562,97 @@ export function createMemoryRouter({ projectRoot }) {
495
562
  res.json({ ok: allPassed, checks });
496
563
  }));
497
564
 
565
+ // GET /memory/lightrag/status
566
+ router.get('/memory/lightrag/status', wrap(async (_req, res) => {
567
+ const { resolveLightRAGConfig, isRunning } = await getMemoryLightrag();
568
+ const cfg = resolveLightRAGConfig(projectRoot);
569
+ const pidFile = join(cfg.workingDir, 'lightrag.pid');
570
+
571
+ // Inline readPidAlive logic
572
+ let pid = null;
573
+ let alive = false;
574
+ if (existsSync(pidFile)) {
575
+ try {
576
+ pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
577
+ if (Number.isFinite(pid) && pid > 0) {
578
+ try {
579
+ process.kill(pid, 0);
580
+ alive = true;
581
+ } catch {
582
+ alive = false;
583
+ }
584
+ } else {
585
+ pid = null;
586
+ }
587
+ } catch {
588
+ pid = null;
589
+ }
590
+ }
591
+
592
+ const running = alive && (await isRunning(cfg));
593
+ const logFile = join(cfg.workingDir, 'lightrag.log');
594
+ const logTail = [];
595
+
596
+ if (existsSync(logFile)) {
597
+ try {
598
+ const content = readFileSync(logFile, 'utf8');
599
+ const lines = content.split('\n');
600
+ logTail.push(...lines.slice(-30));
601
+ } catch {}
602
+ }
603
+
604
+ let lastError = null;
605
+ if (!running && pid !== null) {
606
+ lastError = 'process not responding to health checks';
607
+ }
608
+
609
+ res.json({
610
+ running,
611
+ pid: running ? pid : null,
612
+ host: cfg.host,
613
+ port: cfg.port,
614
+ llmBinding: cfg.llmBinding,
615
+ embeddingBinding: cfg.embeddingBinding,
616
+ llmModel: cfg.llmModel,
617
+ embeddingModel: cfg.embeddingModel,
618
+ lastError,
619
+ logTail,
620
+ });
621
+ }));
622
+
623
+ // POST /memory/lightrag/start
624
+ router.post('/memory/lightrag/start', wrap(async (_req, res) => {
625
+ const { resolveLightRAGConfig, startServer } = await getMemoryLightrag();
626
+ const cfg = resolveLightRAGConfig(projectRoot);
627
+ const result = await startServer(cfg, {});
628
+ res.json(result);
629
+ }));
630
+
631
+ // POST /memory/lightrag/stop
632
+ router.post('/memory/lightrag/stop', wrap(async (_req, res) => {
633
+ const { resolveLightRAGConfig, stopServer } = await getMemoryLightrag();
634
+ const cfg = resolveLightRAGConfig(projectRoot);
635
+ const result = await stopServer(cfg, {});
636
+ res.json(result);
637
+ }));
638
+
639
+ // GET /memory/lightrag/log
640
+ router.get('/memory/lightrag/log', wrap(async (_req, res) => {
641
+ const { resolveLightRAGConfig } = await getMemoryLightrag();
642
+ const cfg = resolveLightRAGConfig(projectRoot);
643
+ const logFile = join(cfg.workingDir, 'lightrag.log');
644
+ if (!existsSync(logFile)) {
645
+ res.json({ lines: [] });
646
+ return;
647
+ }
648
+ try {
649
+ const content = readFileSync(logFile, 'utf8');
650
+ const lines = content.split('\n');
651
+ res.json({ lines: lines.slice(-200) });
652
+ } catch (err) {
653
+ res.status(500).json({ error: err.message });
654
+ }
655
+ }));
656
+
498
657
  return router;
499
658
  }
@@ -302,7 +302,7 @@ export async function createServer({
302
302
  }
303
303
  currentBroadcast = localBroadcast;
304
304
 
305
- const apiRouter = createApiRouter({
305
+ const apiRouter = await createApiRouter({
306
306
  state,
307
307
  watcher,
308
308
  projectRoot,
@@ -13,7 +13,7 @@
13
13
  * - getChat: per-project sessions/<id>.jsonl (preferred) — falls
14
14
  * back to legacy .bizar/sessions if no project is active
15
15
  * - getAgents: ~/.config/opencode/agents/*.md (frontmatter parse)
16
- * - getArtifacts: scans artifacts/ (worktree) and ~/.config/opencode/artifacts/
16
+ * - getArtifacts: scans .bizar/artifacts/ (worktree) and ~/.config/opencode/artifacts/
17
17
  */
18
18
  import {
19
19
  existsSync,
@@ -47,7 +47,7 @@ export function createState({ projectRoot, opencodeConfigDir, bizarRoot }) {
47
47
  bizarDir: join(projectRoot, '.bizar'),
48
48
  sessionsDir: join(projectRoot, '.bizar', 'sessions'),
49
49
  activityLog: join(projectRoot, '.bizar', 'activity.log'),
50
- plansDir: join(projectRoot, 'artifacts'),
50
+ plansDir: join(projectRoot, '.bizar', 'artifacts'),
51
51
  globalPlansDir: join(opencodeConfigDir, 'artifacts'),
52
52
  settingsFile: join(HOME, '.config', 'bizar', 'settings.json'),
53
53
  };