@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.
- package/README.md +11 -14
- package/bizar-dash/CHANGELOG.md +1 -1
- package/bizar-dash/src/server/api.mjs +2 -2
- package/bizar-dash/src/server/artifacts-store.mjs +4 -4
- package/bizar-dash/src/server/memory-git.mjs +80 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-store.mjs +47 -0
- package/bizar-dash/src/server/mod-security.mjs +2 -2
- package/bizar-dash/src/server/routes/memory.mjs +174 -15
- package/bizar-dash/src/server/server.mjs +1 -1
- package/bizar-dash/src/server/state.mjs +2 -2
- package/bizar-dash/src/web/views/Config.tsx +461 -1
- package/bizar-dash/tests/memory-cli.test.mjs +542 -0
- package/bizar-dash/tests/memory-config.test.mjs +422 -0
- package/bizar-dash/tests/memory-git.test.mjs +109 -1
- package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +4 -4
- package/cli/bootstrap.mjs +1 -1
- package/cli/copy.mjs +22 -16
- package/cli/doctor.mjs +4 -4
- package/cli/doctor.test.mjs +2 -2
- package/cli/init.mjs +2 -2
- package/cli/install.mjs +21 -16
- package/cli/memory.mjs +710 -31
- package/cli/utils.mjs +6 -3
- package/config/AGENTS.md +7 -7
- package/config/agents/_shared/AGENT_BASELINE.md +59 -61
- package/config/opencode.json +13 -38
- package/config/skills/memory-protocol/SKILL.md +105 -0
- package/config/skills/obsidian/SKILL.md +58 -1
- package/install.sh +11 -1
- package/package.json +2 -2
- package/plugins/bizar/index.ts +7 -0
- package/plugins/bizar/src/commands.ts +42 -1
- package/plugins/bizar/src/tools/open-kb.ts +191 -0
- 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`, `
|
|
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
|
-
'
|
|
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
|
-
|
|
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 —
|
|
493
|
+
// POST /memory/reindex — v4.1.0 real LightRAG population
|
|
441
494
|
router.post('/memory/reindex', wrap(async (_req, res) => {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
}
|
|
@@ -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
|
};
|