@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
@@ -36,6 +36,19 @@ import type {
36
36
  Snapshot,
37
37
  } from '../lib/types';
38
38
 
39
+ type LightragStatus = {
40
+ running: boolean;
41
+ pid: number | null;
42
+ host: string;
43
+ port: number;
44
+ llmBinding: string;
45
+ embeddingBinding: string;
46
+ llmModel: string;
47
+ embeddingModel: string;
48
+ lastError: string | null;
49
+ logTail: string[];
50
+ };
51
+
39
52
  type Props = {
40
53
  snapshot: Snapshot;
41
54
  settings: Settings;
@@ -44,13 +57,14 @@ type Props = {
44
57
  refreshSnapshot: () => Promise<void>;
45
58
  };
46
59
 
47
- type NavId = 'opencode' | 'providers' | 'mcps' | 'diagnostics' | 'export';
60
+ type NavId = 'opencode' | 'providers' | 'mcps' | 'diagnostics' | 'memory' | 'export';
48
61
 
49
62
  const NAV_ITEMS: { id: NavId; label: string; icon: typeof Settings2; desc: string }[] = [
50
63
  { id: 'opencode', label: 'OpenCode config', icon: FileCode2, desc: 'Edit opencode.json directly' },
51
64
  { id: 'providers', label: 'Providers', icon: ServerIcon, desc: 'AI providers + API keys' },
52
65
  { id: 'mcps', label: 'MCPs', icon: Plug, desc: 'Model Context Protocol servers' },
53
66
  { id: 'diagnostics', label: 'Diagnostics', icon: Stethoscope, desc: 'Service health + counts' },
67
+ { id: 'memory', label: 'Memory & LightRAG', icon: Database, desc: 'Configure Bizar Memory Service and the LightRAG embedding server.' },
54
68
  { id: 'export', label: 'Export / Import', icon: Download, desc: 'Download diagnostics bundle' },
55
69
  ];
56
70
 
@@ -77,6 +91,10 @@ export function Config({ snapshot, refreshSnapshot }: Props) {
77
91
  // Diagnostics state
78
92
  const [diagnostics, setDiagnostics] = useState<Diagnostics | null>(null);
79
93
 
94
+ // Memory & LightRAG state
95
+ const [lightragStatus, setLightragStatus] = useState<LightragStatus | null>(null);
96
+ const [lightragLog, setLightragLog] = useState<string[]>([]);
97
+
80
98
  // Providers + MCPs state — re-fetch when nav switches so we always have fresh data.
81
99
  const [providers, setProviders] = useState<Provider[]>(snapshot.providers || []);
82
100
  const [mcps, setMcps] = useState<McpServer[]>(snapshot.mcps || []);
@@ -125,6 +143,7 @@ export function Config({ snapshot, refreshSnapshot }: Props) {
125
143
  if (activeNav === 'providers' && providers.length === 0) reloadProviders();
126
144
  if (activeNav === 'mcps' && mcps.length === 0) reloadMcps();
127
145
  if (activeNav === 'diagnostics' && !diagnostics) loadDiagnostics();
146
+ if (activeNav === 'memory') loadLightragStatus();
128
147
  // eslint-disable-next-line react-hooks/exhaustive-deps
129
148
  }, [activeNav]);
130
149
 
@@ -181,6 +200,24 @@ export function Config({ snapshot, refreshSnapshot }: Props) {
181
200
  }
182
201
  };
183
202
 
203
+ const loadLightragStatus = async () => {
204
+ try {
205
+ const d = await api.get<LightragStatus>('/memory/lightrag/status');
206
+ setLightragStatus(d);
207
+ } catch (err) {
208
+ toast.error(`LightRAG status failed: ${(err as Error).message}`);
209
+ }
210
+ };
211
+
212
+ const loadLightragLog = async () => {
213
+ try {
214
+ const d = await api.get<{ lines: string[] }>('/memory/lightrag/log');
215
+ setLightragLog(d.lines || []);
216
+ } catch {
217
+ setLightragLog([]);
218
+ }
219
+ };
220
+
184
221
  const onDownloadDiagnostics = () => {
185
222
  const bundle = {
186
223
  generatedAt: new Date().toISOString(),
@@ -263,6 +300,15 @@ export function Config({ snapshot, refreshSnapshot }: Props) {
263
300
  <DiagnosticsPanel diagnostics={diagnostics} loading={!diagnostics} onReload={loadDiagnostics} />
264
301
  )}
265
302
 
303
+ {activeNav === 'memory' && (
304
+ <MemoryLightragPanel
305
+ status={lightragStatus}
306
+ onReload={loadLightragStatus}
307
+ logLines={lightragLog}
308
+ onReloadLog={loadLightragLog}
309
+ />
310
+ )}
311
+
266
312
  {activeNav === 'export' && (
267
313
  <ExportPanel onDownload={onDownloadDiagnostics} />
268
314
  )}
@@ -1468,6 +1514,420 @@ function ExportPanel({ onDownload }: { onDownload: () => void }) {
1468
1514
  );
1469
1515
  }
1470
1516
 
1517
+ /* ──────────────────────────────────────────────────────────────
1518
+ Memory & LightRAG Panel
1519
+ ────────────────────────────────────────────────────────────── */
1520
+
1521
+ type LightragDraft = {
1522
+ enabled: boolean;
1523
+ host: string;
1524
+ port: number | '';
1525
+ workingDir: string;
1526
+ llmBinding: string;
1527
+ embeddingBinding: string;
1528
+ llmBindingHost: string;
1529
+ embeddingBindingHost: string;
1530
+ llmModel: string;
1531
+ embeddingModel: string;
1532
+ apiKeySource: 'env' | 'file';
1533
+ apiKey: string;
1534
+ };
1535
+
1536
+ const LLM_BINDING_OPTIONS = ['ollama', 'openai', 'lollms', 'azure_openai', 'bedrock', 'gemini'];
1537
+ const EMBEDDING_BINDING_OPTIONS = ['ollama', 'openai', 'azure_openai', 'bedrock', 'jina', 'gemini', 'voyageai'];
1538
+
1539
+ function MemoryLightragPanel({
1540
+ status,
1541
+ onReload,
1542
+ logLines,
1543
+ onReloadLog,
1544
+ }: {
1545
+ status: LightragStatus | null;
1546
+ onReload: () => void;
1547
+ logLines: string[];
1548
+ onReloadLog: () => void;
1549
+ }) {
1550
+ const toast = useToast();
1551
+ const [saving, setSaving] = useState(false);
1552
+ const [starting, setStarting] = useState(false);
1553
+ const [stopping, setStopping] = useState(false);
1554
+ const [loadingLog, setLoadingLog] = useState(false);
1555
+ const [showLog, setShowLog] = useState(false);
1556
+
1557
+ // Draft form state — initialise from status once loaded
1558
+ const [draft, setDraft] = useState<LightragDraft>({
1559
+ enabled: true,
1560
+ host: '127.0.0.1',
1561
+ port: 9621,
1562
+ workingDir: '',
1563
+ llmBinding: 'ollama',
1564
+ embeddingBinding: 'ollama',
1565
+ llmBindingHost: '',
1566
+ embeddingBindingHost: '',
1567
+ llmModel: 'minimax/MiniMax-M3',
1568
+ embeddingModel: 'text-embedding-3-small',
1569
+ apiKeySource: 'env',
1570
+ apiKey: '',
1571
+ });
1572
+ const [dirty, setDirty] = useState(false);
1573
+
1574
+ // Sync draft from status when status loads
1575
+ useEffect(() => {
1576
+ if (!status) return;
1577
+ setDraft((d) => ({
1578
+ ...d,
1579
+ enabled: true,
1580
+ host: status.host,
1581
+ port: status.port,
1582
+ llmBinding: status.llmBinding,
1583
+ embeddingBinding: status.embeddingBinding,
1584
+ llmBindingHost: status.llmBindingHost || '',
1585
+ embeddingBindingHost: status.embeddingBindingHost || '',
1586
+ llmModel: status.llmModel,
1587
+ embeddingModel: status.embeddingModel,
1588
+ apiKeySource: 'env',
1589
+ apiKey: '',
1590
+ }));
1591
+ setDirty(false);
1592
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1593
+ }, [status?.running]);
1594
+
1595
+ const set = <K extends keyof LightragDraft>(key: K, value: LightragDraft[K]) => {
1596
+ setDraft((d) => ({ ...d, [key]: value }));
1597
+ setDirty(true);
1598
+ };
1599
+
1600
+ const handleSave = async () => {
1601
+ if (saving) return;
1602
+ setSaving(true);
1603
+ try {
1604
+ // Build patch payload
1605
+ const patch: Record<string, unknown> = {
1606
+ enabled: draft.enabled,
1607
+ host: draft.host,
1608
+ port: draft.port === '' ? 9621 : Number(draft.port),
1609
+ workingDir: draft.workingDir,
1610
+ llmBinding: draft.llmBinding,
1611
+ embeddingBinding: draft.embeddingBinding,
1612
+ llmBindingHost: draft.llmBindingHost || null,
1613
+ embeddingBindingHost: draft.embeddingBindingHost || null,
1614
+ llmModel: draft.llmModel,
1615
+ embeddingModel: draft.embeddingModel,
1616
+ apiKeySource: draft.apiKeySource,
1617
+ };
1618
+ if (draft.apiKeySource === 'file' && draft.apiKey !== '') {
1619
+ patch.apiKey = draft.apiKey;
1620
+ }
1621
+ if (draft.apiKeySource === 'env') {
1622
+ patch.apiKey = '<empty>';
1623
+ }
1624
+
1625
+ await api.post('/memory/config', { patch: true, lightrag: patch });
1626
+ toast.success('LightRAG config saved.');
1627
+ setDirty(false);
1628
+ onReload();
1629
+ } catch (err) {
1630
+ toast.error(`Save failed: ${(err as Error).message}`);
1631
+ } finally {
1632
+ setSaving(false);
1633
+ }
1634
+ };
1635
+
1636
+ const handleStart = async () => {
1637
+ if (starting) return;
1638
+ setStarting(true);
1639
+ try {
1640
+ const r = await api.post<{ ok: boolean; error?: string; pid?: number }>('/memory/lightrag/start');
1641
+ if (r.ok) {
1642
+ toast.success(`LightRAG started (pid ${r.pid}).`);
1643
+ } else {
1644
+ toast.error(`Start failed: ${r.error}`);
1645
+ }
1646
+ onReload();
1647
+ } catch (err) {
1648
+ toast.error(`Start failed: ${(err as Error).message}`);
1649
+ } finally {
1650
+ setStarting(false);
1651
+ }
1652
+ };
1653
+
1654
+ const handleStop = async () => {
1655
+ if (stopping) return;
1656
+ setStopping(true);
1657
+ try {
1658
+ await api.post('/memory/lightrag/stop');
1659
+ toast.success('LightRAG stopped.');
1660
+ onReload();
1661
+ } catch (err) {
1662
+ toast.error(`Stop failed: ${(err as Error).message}`);
1663
+ } finally {
1664
+ setStopping(false);
1665
+ }
1666
+ };
1667
+
1668
+ const handleRestart = async () => {
1669
+ await handleStop();
1670
+ await handleStart();
1671
+ };
1672
+
1673
+ const handleShowLog = async () => {
1674
+ if (!showLog) {
1675
+ setLoadingLog(true);
1676
+ await onReloadLog();
1677
+ setLoadingLog(false);
1678
+ }
1679
+ setShowLog((v) => !v);
1680
+ };
1681
+
1682
+ const running = status?.running ?? false;
1683
+
1684
+ return (
1685
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
1686
+ {/* Status card */}
1687
+ <Card>
1688
+ <CardTitle>
1689
+ <Database size={14} /> LightRAG Server
1690
+ </CardTitle>
1691
+ <CardMeta>
1692
+ In-process embedding server for semantic memory search.{' '}
1693
+ <button type="button" className="link-btn" onClick={onReload}>Refresh</button>
1694
+ </CardMeta>
1695
+ <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap', alignItems: 'center' }}>
1696
+ {running ? (
1697
+ <span className="tag tag-success">
1698
+ running {status?.pid ? `(pid ${status.pid})` : ''}
1699
+ </span>
1700
+ ) : (
1701
+ <span className="tag tag-neutral">stopped</span>
1702
+ )}
1703
+ {status && (
1704
+ <span className="mono text-sm muted">
1705
+ {status.host}:{status.port} · {status.llmBinding} · {status.embeddingModel}
1706
+ </span>
1707
+ )}
1708
+ {status?.lastError && (
1709
+ <span className="text-error text-sm">⚠ {status.lastError}</span>
1710
+ )}
1711
+ </div>
1712
+ <div style={{ display: 'flex', gap: 8, marginTop: 12, flexWrap: 'wrap' }}>
1713
+ {!running ? (
1714
+ <Button variant="primary" size="sm" onClick={handleStart} disabled={starting}>
1715
+ {starting ? <Spinner size="sm" /> : <RefreshCw size={12} />}
1716
+ {starting ? 'Starting…' : 'Start'}
1717
+ </Button>
1718
+ ) : (
1719
+ <>
1720
+ <Button variant="secondary" size="sm" onClick={handleRestart} disabled={stopping || starting}>
1721
+ <RefreshCw size={12} /> Restart
1722
+ </Button>
1723
+ <Button variant="ghost" size="sm" onClick={handleStop} disabled={stopping}>
1724
+ {stopping ? <Spinner size="sm" /> : null}
1725
+ {stopping ? 'Stopping…' : 'Stop'}
1726
+ </Button>
1727
+ </>
1728
+ )}
1729
+ <Button variant="ghost" size="sm" onClick={handleShowLog}>
1730
+ {showLog ? 'Hide' : 'Show'} log
1731
+ </Button>
1732
+ </div>
1733
+ {showLog && (
1734
+ <div style={{ marginTop: 12 }}>
1735
+ <div className="field-help" style={{ marginBottom: 6 }}>
1736
+ Last {logLines.length} line(s)
1737
+ <button type="button" className="link-btn" style={{ marginLeft: 8 }} onClick={onReloadLog}>
1738
+ Refresh
1739
+ </button>
1740
+ </div>
1741
+ {loadingLog ? (
1742
+ <p className="muted text-sm">Loading…</p>
1743
+ ) : logLines.length === 0 ? (
1744
+ <p className="muted text-sm">No log output yet.</p>
1745
+ ) : (
1746
+ <pre className="log-pre">{logLines.join('\n')}</pre>
1747
+ )}
1748
+ </div>
1749
+ )}
1750
+ </Card>
1751
+
1752
+ {/* Binding card */}
1753
+ <Card>
1754
+ <CardTitle>
1755
+ <ShieldCheck size={14} /> Bindings
1756
+ </CardTitle>
1757
+ <CardMeta>Choose the LLM and embedding provider bindings.</CardMeta>
1758
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}>
1759
+ <div className="field-row">
1760
+ <label className="field-label">LLM Binding</label>
1761
+ <select
1762
+ className="input"
1763
+ value={draft.llmBinding}
1764
+ onChange={(e) => set('llmBinding', e.target.value)}
1765
+ >
1766
+ {LLM_BINDING_OPTIONS.map((b) => (
1767
+ <option key={b} value={b}>{b}</option>
1768
+ ))}
1769
+ </select>
1770
+ </div>
1771
+ <div className="field-row">
1772
+ <label className="field-label">Embedding Binding</label>
1773
+ <select
1774
+ className="input"
1775
+ value={draft.embeddingBinding}
1776
+ onChange={(e) => set('embeddingBinding', e.target.value)}
1777
+ >
1778
+ {EMBEDDING_BINDING_OPTIONS.map((b) => (
1779
+ <option key={b} value={b}>{b}</option>
1780
+ ))}
1781
+ </select>
1782
+ </div>
1783
+ {draft.llmBinding !== 'ollama' && (
1784
+ <div className="field-row">
1785
+ <label className="field-label">LLM Binding Host</label>
1786
+ <input
1787
+ className="input"
1788
+ type="text"
1789
+ value={draft.llmBindingHost}
1790
+ onChange={(e) => set('llmBindingHost', e.target.value)}
1791
+ placeholder="https://api.minimax.chat/v1"
1792
+ />
1793
+ </div>
1794
+ )}
1795
+ {draft.embeddingBinding !== 'ollama' && (
1796
+ <div className="field-row">
1797
+ <label className="field-label">Embedding Binding Host</label>
1798
+ <input
1799
+ className="input"
1800
+ type="text"
1801
+ value={draft.embeddingBindingHost}
1802
+ onChange={(e) => set('embeddingBindingHost', e.target.value)}
1803
+ placeholder="https://api.minimax.chat/v1"
1804
+ />
1805
+ </div>
1806
+ )}
1807
+ </div>
1808
+ </Card>
1809
+
1810
+ {/* Models card */}
1811
+ <Card>
1812
+ <CardTitle>
1813
+ <ServerIcon size={14} /> Models
1814
+ </CardTitle>
1815
+ <CardMeta>Model IDs for the LLM and embedding engine.</CardMeta>
1816
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}>
1817
+ <div className="field-row">
1818
+ <label className="field-label">LLM Model</label>
1819
+ <input
1820
+ className="input"
1821
+ type="text"
1822
+ value={draft.llmModel}
1823
+ onChange={(e) => set('llmModel', e.target.value)}
1824
+ placeholder="minimax/MiniMax-M3"
1825
+ />
1826
+ </div>
1827
+ <div className="field-row">
1828
+ <label className="field-label">Embedding Model</label>
1829
+ <input
1830
+ className="input"
1831
+ type="text"
1832
+ value={draft.embeddingModel}
1833
+ onChange={(e) => set('embeddingModel', e.target.value)}
1834
+ placeholder="text-embedding-3-small"
1835
+ />
1836
+ </div>
1837
+ </div>
1838
+ </Card>
1839
+
1840
+ {/* Connection card */}
1841
+ <Card>
1842
+ <CardTitle>
1843
+ <Plug size={14} /> Connection
1844
+ </CardTitle>
1845
+ <CardMeta>Host, port, and working directory for the LightRAG server.</CardMeta>
1846
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}>
1847
+ <div className="field-row">
1848
+ <label className="field-label">Host</label>
1849
+ <input
1850
+ className="input"
1851
+ type="text"
1852
+ value={draft.host}
1853
+ onChange={(e) => set('host', e.target.value)}
1854
+ placeholder="127.0.0.1"
1855
+ />
1856
+ </div>
1857
+ <div className="field-row">
1858
+ <label className="field-label">Port</label>
1859
+ <input
1860
+ className="input"
1861
+ type="number"
1862
+ min={1}
1863
+ max={65535}
1864
+ value={draft.port}
1865
+ onChange={(e) => set('port', e.target.value === '' ? '' : Number(e.target.value))}
1866
+ placeholder="9621"
1867
+ />
1868
+ </div>
1869
+ <div className="field-row">
1870
+ <label className="field-label">Working Dir</label>
1871
+ <input
1872
+ className="input"
1873
+ type="text"
1874
+ value={draft.workingDir}
1875
+ onChange={(e) => set('workingDir', e.target.value)}
1876
+ placeholder=".bizar/lightrag (default)"
1877
+ />
1878
+ </div>
1879
+ </div>
1880
+ </Card>
1881
+
1882
+ {/* Credentials card */}
1883
+ <Card>
1884
+ <CardTitle>
1885
+ <ShieldCheck size={14} /> Credentials
1886
+ </CardTitle>
1887
+ <CardMeta>API key source — set in your shell env, or stored in .bizar/memory.json.</CardMeta>
1888
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}>
1889
+ <div className="field-row">
1890
+ <label className="field-label">Source</label>
1891
+ <select
1892
+ className="input"
1893
+ value={draft.apiKeySource}
1894
+ onChange={(e) => set('apiKeySource', e.target.value as 'env' | 'file')}
1895
+ >
1896
+ <option value="env">Environment variable (recommended)</option>
1897
+ <option value="file">Stored in .bizar/memory.json</option>
1898
+ </select>
1899
+ </div>
1900
+ {draft.apiKeySource === 'env' ? (
1901
+ <p className="muted text-sm" style={{ padding: '8px 0' }}>
1902
+ Set <code>OPENAI_API_KEY</code> (or provider-specific env var) in your shell before
1903
+ running reindex or queries.
1904
+ </p>
1905
+ ) : (
1906
+ <div className="field-row">
1907
+ <label className="field-label">API Key</label>
1908
+ <input
1909
+ className="input"
1910
+ type="password"
1911
+ value={draft.apiKey}
1912
+ onChange={(e) => set('apiKey', e.target.value)}
1913
+ placeholder={status?.running ? '(unchanged — leave blank)' : 'sk-…'}
1914
+ autoComplete="off"
1915
+ />
1916
+ </div>
1917
+ )}
1918
+ </div>
1919
+ </Card>
1920
+
1921
+ <div className="view-actions">
1922
+ <Button variant="primary" disabled={!dirty || saving} onClick={handleSave}>
1923
+ {saving ? <Spinner size="sm" /> : <Save size={14} />}
1924
+ {saving ? 'Saving…' : 'Save config'}
1925
+ </Button>
1926
+ </div>
1927
+ </div>
1928
+ );
1929
+ }
1930
+
1471
1931
  // ─── AutoDetectBanner (v3.16.0) ────────────────────────────────────
1472
1932
  // Scans env vars + opencode.json for known provider API keys
1473
1933
  // (Anthropic, OpenAI, Google, Mistral, Groq, Cohere, OpenRouter,