@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
@@ -11,7 +11,7 @@
11
11
  * initVault time (F7 invariant).
12
12
  */
13
13
 
14
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs';
15
15
  import { execFileSync } from 'node:child_process';
16
16
  import { join, dirname, relative, resolve as pathResolve, sep } from 'node:path';
17
17
  import { homedir } from 'node:os';
@@ -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');
@@ -26,15 +27,51 @@ const BIZAR_MEMORY_ROOT = join(HOME, '.local', 'share', 'bizar', 'memory');
26
27
  /**
27
28
  * Path safety: reject any relPath that would escape the vault root.
28
29
  *
30
+ * Defenses (in order):
31
+ * 1. Empty / null-byte injection → reject.
32
+ * 2. Explicit segment check for `..` and absolute path prefixes → reject
33
+ * before path.resolve normalizes them away. The check is on SEGMENTS
34
+ * (`..` separated by `/` or `\`), so filenames like `my..note.md` are
35
+ * still allowed.
36
+ * 3. Post-resolve check: the resulting absolute path must be inside
37
+ * vaultRoot (relative path must not start with `..`).
38
+ * 4. Symlink guard: if the resolved path is a pre-existing symlink, refuse.
39
+ * This blocks an attacker who placed a symlink inside the vault pointing
40
+ * outside from writing/reading through it.
41
+ *
42
+ * Known limitation: the symlink guard is not TOCTOU-safe. A swap between
43
+ * lstatSync and the subsequent read/write/delete is still possible. For a
44
+ * full TOCTOU fix the read/write/delete would need to use O_NOFOLLOW and
45
+ * operate on an open fd, not a path. This is acceptable for the current
46
+ * threat model (the vault is a per-user directory, not a multi-tenant FS).
47
+ *
29
48
  * @param {string} vaultRoot
30
49
  * @param {string} relPath
31
50
  * @returns {string | null} — resolved absolute path, or null if unsafe
32
51
  */
33
52
  function resolveSafe(vaultRoot, relPath) {
34
53
  if (!relPath || relPath.includes('\0')) return null;
54
+ // Segment-level check: reject any `..` segment or absolute-path prefix
55
+ // BEFORE path.resolve normalizes them away. This explicitly rejects
56
+ // paths like `notes/../escape.md`, while still allowing filenames like
57
+ // `my..note.md` (the substring `..` inside a single segment is fine).
58
+ const segments = relPath.split(/[\\/]+/);
59
+ if (segments.includes('..') || segments[0] === '') return null;
35
60
  const abs = pathResolve(vaultRoot, relPath);
36
61
  const rel = relative(vaultRoot, abs);
37
62
  if (rel.startsWith('..') || abs !== pathResolve(abs)) return null;
63
+ // Symlink guard (see note above re: TOCTOU). Note: existsSync follows
64
+ // symlinks, so we MUST use lstatSync directly — existsSync + isSymbolicLink
65
+ // misses dangling symlinks (target doesn't exist). lstatSync returns a
66
+ // Stats object even for dangling symlinks; catch only unexpected errors
67
+ // (e.g. EACCES on the parent directory).
68
+ try {
69
+ if (lstatSync(abs).isSymbolicLink()) return null;
70
+ } catch {
71
+ // Path doesn't exist or parent dir inaccessible — not a symlink,
72
+ // proceed. (A fresh write won't traverse a symlink because the
73
+ // symlink itself doesn't exist yet at write time.)
74
+ }
38
75
  return abs;
39
76
  }
40
77
 
@@ -119,6 +156,31 @@ export function resolveVault(projectRoot) {
119
156
  return { mode, projectId, vaultRoot, repoPath, configDir, gitRemote, branch, lightragDir, namespaces };
120
157
  }
121
158
 
159
+ /**
160
+ * Resolve the absolute path of a namespace root.
161
+ *
162
+ * For `project`, returns vaultRoot (the existing default). For `global` and
163
+ * `user`, returns `<vaultRoot>/<namespaces.global|user>` in both modes —
164
+ * `writeNote('global/bizar/foo.md', …)` already places files there in both
165
+ * local-only and managed mode, so this matches the on-disk layout that
166
+ * existing callers rely on.
167
+ *
168
+ * (Note: an earlier draft distinguished local-only vs managed, putting
169
+ * global under `<repoPath>/global/bizar` in managed mode. That diverged from
170
+ * where writeNote actually creates the file, which is always under
171
+ * vaultRoot. This implementation matches the on-disk reality.)
172
+ *
173
+ * @param {{ mode: string, vaultRoot: string, namespaces: object }} vaultInfo
174
+ * @param {'project'|'global'|'user'} namespace
175
+ * @returns {string|null} absolute path, or null if the namespace is unknown
176
+ */
177
+ export function resolveNamespaceRoot(vaultInfo, namespace) {
178
+ if (namespace === 'project') return vaultInfo.vaultRoot;
179
+ const ns = vaultInfo.namespaces?.[namespace];
180
+ if (!ns) return null;
181
+ return join(vaultInfo.vaultRoot, ns);
182
+ }
183
+
122
184
  /**
123
185
  * Initialize the vault for a project. In local-only mode, creates .obsidian/
124
186
  * and standard subdirectories. In managed mode, the shared repo is assumed to
@@ -225,15 +287,20 @@ export function initVault(projectRoot) {
225
287
  * List all notes in the vault. Returns enriched note metadata.
226
288
  *
227
289
  * @param {string} projectRoot
228
- * @param {{ namespace?: string }} [opts]
290
+ * @param {{ namespace?: string, root?: string }} [opts]
291
+ * `namespace`: subdirectory under `root` to walk (e.g. `projects/<id>`).
292
+ * `root`: override the root used as the walking base. Defaults to
293
+ * `vaultRoot` from resolveVault. Used by namespace helpers
294
+ * in cli/memory.mjs to list global / user notes.
229
295
  * @returns {Array<{ relPath: string, mtime: number, size: number, frontmatter: Record<string, unknown>, body: string, schemaValid: boolean }>}
230
296
  */
231
297
  export function listNotes(projectRoot, opts = {}) {
232
298
  const { vaultRoot } = resolveVault(projectRoot);
233
- if (!existsSync(vaultRoot)) return [];
299
+ const root = opts.root || vaultRoot;
300
+ if (!existsSync(root)) return [];
234
301
 
235
302
  const namespace = opts.namespace || '';
236
- const searchRoot = namespace ? join(vaultRoot, namespace) : vaultRoot;
303
+ const searchRoot = namespace ? join(root, namespace) : root;
237
304
  if (!existsSync(searchRoot)) return [];
238
305
 
239
306
  const out = [];
@@ -267,11 +334,14 @@ export function listNotes(projectRoot, opts = {}) {
267
334
  *
268
335
  * @param {string} projectRoot
269
336
  * @param {string} relPath
337
+ * @param {{ root?: string }} [opts] optional override for the root used by
338
+ * resolveSafe; defaults to vaultRoot
270
339
  * @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean } | null}
271
340
  */
272
- export function readNote(projectRoot, relPath) {
273
- const { vaultRoot } = resolveVault(projectRoot);
274
- const filePath = resolveSafe(vaultRoot, relPath);
341
+ export function readNote(projectRoot, relPath, opts = {}) {
342
+ const vaultInfo = resolveVault(projectRoot);
343
+ const root = opts.root || vaultInfo.vaultRoot;
344
+ const filePath = resolveSafe(root, relPath);
275
345
  if (!filePath || !existsSync(filePath)) return null;
276
346
  try {
277
347
  const raw = readFileSync(filePath, 'utf8');
@@ -333,6 +403,33 @@ export function writeNote(projectRoot, relPath, { frontmatter, body }) {
333
403
  const content = `---\n${yamlBlock}\n---\n\n${body}`;
334
404
  writeFileSync(filePath, content, 'utf8');
335
405
 
406
+ // Auto-commit the written file to git (if configured).
407
+ // The lock serializes commits, not writes — concurrent writes will each
408
+ // trigger their own commit, which is the intended behaviour.
409
+ (() => {
410
+ try {
411
+ if (!memoryGit.isGitInstalled()) return;
412
+ const { config } = loadConfig(projectRoot);
413
+ if (config.mode === 'local-only') return;
414
+ if (config.git?.autoCommitOnMemoryWrite !== true) return;
415
+ const lockResult = memoryGit.acquireLock(vaultRoot);
416
+ if (!lockResult || lockResult.error) return; // skip if locked or error
417
+ try {
418
+ memoryGit.addFile(vaultRoot, relPath);
419
+ const summary = frontmatter.title || relPath;
420
+ const message = (config.git.commitMessageTemplate || 'memory(BizarHarness): {summary}')
421
+ .replace('{summary}', summary);
422
+ memoryGit.commit(vaultRoot, message, { author: config.git.commitAuthor });
423
+ } finally {
424
+ lockResult.release();
425
+ }
426
+ } catch (err) {
427
+ // Log but never re-throw — the write already succeeded; a failed
428
+ // auto-commit is recoverable on next write or manual commit.
429
+ console.error('[memory-store] autoCommitOnMemoryWrite failed:', err?.message || err);
430
+ }
431
+ })();
432
+
336
433
  const st = statSync(filePath);
337
434
  return {
338
435
  relPath,
@@ -350,11 +447,14 @@ export function writeNote(projectRoot, relPath, { frontmatter, body }) {
350
447
  *
351
448
  * @param {string} projectRoot
352
449
  * @param {string} relPath
450
+ * @param {{ root?: string }} [opts] optional override for the root used by
451
+ * resolveSafe; defaults to vaultRoot
353
452
  * @returns {boolean}
354
453
  */
355
- export function deleteNote(projectRoot, relPath) {
356
- const { vaultRoot } = resolveVault(projectRoot);
357
- const filePath = resolveSafe(vaultRoot, relPath);
454
+ export function deleteNote(projectRoot, relPath, opts = {}) {
455
+ const vaultInfo = resolveVault(projectRoot);
456
+ const root = opts.root || vaultInfo.vaultRoot;
457
+ const filePath = resolveSafe(root, relPath);
358
458
  if (!filePath || !existsSync(filePath)) return false;
359
459
  unlinkSync(filePath);
360
460
  return true;
@@ -422,3 +522,22 @@ export function validateAll(projectRoot) {
422
522
  }
423
523
  return results;
424
524
  }
525
+
526
+ // ── LightRAG integration (v4.1.0) ──────────────────────────────────────────
527
+ //
528
+ // Re-export the LightRAG orchestrator from the memory-store module so the
529
+ // dashboard, CLI, and tests have a single import surface. The actual
530
+ // implementation lives in `memory-lightrag.mjs`.
531
+
532
+ export {
533
+ resolveLightRAGConfig,
534
+ isInstalled as isLightRAGInstalled,
535
+ isRunning as isLightRAGRunning,
536
+ startServer as startLightRAG,
537
+ stopServer as stopLightRAG,
538
+ ensureRunning as ensureLightRAGRunning,
539
+ insertNote as insertLightRAGNote,
540
+ insertAllNotes as insertLightRAGAll,
541
+ reindexVault,
542
+ query as queryLightRAG,
543
+ } 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
  };