@polderlabs/bizar 4.7.2 → 4.9.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 (44) hide show
  1. package/bizar-dash/dist/assets/index-DU61awG3.js +9 -0
  2. package/bizar-dash/dist/assets/index-DU61awG3.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-DaC1Lc6q.js +366 -0
  4. package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +1 -0
  5. package/bizar-dash/dist/assets/{main-DX_Jh8Wc.css → main-DfmIfOUS.css} +1 -1
  6. package/bizar-dash/dist/assets/{mobile-Chvf9u_B.js → mobile-CL5uUQEC.js} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-Chvf9u_B.js.map → mobile-CL5uUQEC.js.map} +1 -1
  8. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +338 -0
  9. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +1 -0
  10. package/bizar-dash/dist/index.html +3 -3
  11. package/bizar-dash/dist/mobile.html +2 -2
  12. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  13. package/bizar-dash/src/server/memory-lightrag.mjs +109 -0
  14. package/bizar-dash/src/server/memory-store.mjs +121 -0
  15. package/bizar-dash/src/server/otel.mjs +133 -0
  16. package/bizar-dash/src/server/routes/chat.mjs +246 -170
  17. package/bizar-dash/src/server/routes/memory.mjs +46 -0
  18. package/bizar-dash/src/server/routes/opencode-sessions.mjs +82 -48
  19. package/bizar-dash/src/server/server.mjs +40 -0
  20. package/bizar-dash/src/web/components/SettingsSearch.tsx +204 -89
  21. package/bizar-dash/src/web/lib/search.ts +115 -0
  22. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +10 -35
  23. package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +69 -0
  24. package/bizar-dash/src/web/styles/memory.css +84 -1
  25. package/bizar-dash/src/web/styles/settings.css +80 -0
  26. package/bizar-dash/src/web/views/Memory.tsx +6 -1
  27. package/bizar-dash/src/web/views/Settings.tsx +96 -0
  28. package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +29 -0
  29. package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +192 -0
  30. package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +336 -0
  31. package/bizar-dash/tests/backup-restore.test.tsx +35 -17
  32. package/bizar-dash/tests/bundle-analysis.test.mjs +70 -0
  33. package/bizar-dash/tests/components/settings-search.test.tsx +180 -0
  34. package/bizar-dash/tests/docker-build.test.mjs +96 -0
  35. package/bizar-dash/tests/lib/search-fuzzy.test.ts +149 -0
  36. package/bizar-dash/tests/memory-graph-view.test.tsx +69 -0
  37. package/bizar-dash/tests/memory-graph.test.mjs +95 -0
  38. package/bizar-dash/tests/otel.test.mjs +188 -0
  39. package/cli/commands/dash.mjs +6 -0
  40. package/package.json +7 -1
  41. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +0 -361
  42. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +0 -1
  43. package/bizar-dash/dist/assets/mobile-BK8-ythT.js +0 -351
  44. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +0 -1
@@ -29,10 +29,10 @@
29
29
  }
30
30
  })();
31
31
  </script>
32
- <script type="module" crossorigin src="/assets/main-DHZmbnxQ.js"></script>
33
- <link rel="modulepreload" crossorigin href="/assets/mobile-BK8-ythT.js">
32
+ <script type="module" crossorigin src="/assets/main-DaC1Lc6q.js"></script>
33
+ <link rel="modulepreload" crossorigin href="/assets/mobile-D5WTWvuh.js">
34
34
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
35
- <link rel="stylesheet" crossorigin href="/assets/main-DX_Jh8Wc.css">
35
+ <link rel="stylesheet" crossorigin href="/assets/main-DfmIfOUS.css">
36
36
  </head>
37
37
  <body>
38
38
  <div id="root"></div>
@@ -12,8 +12,8 @@
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
14
  <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
15
- <script type="module" crossorigin src="/assets/mobile-Chvf9u_B.js"></script>
16
- <link rel="modulepreload" crossorigin href="/assets/mobile-BK8-ythT.js">
15
+ <script type="module" crossorigin src="/assets/mobile-CL5uUQEC.js"></script>
16
+ <link rel="modulepreload" crossorigin href="/assets/mobile-D5WTWvuh.js">
17
17
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
18
18
  </head>
19
19
  <body>
@@ -1 +1 @@
1
- {"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":18.560040000000015,"failed":false}],[":tests/components/Button.test.tsx",{"duration":64.33624699999996,"failed":false}],[":tests/components/Card.test.tsx",{"duration":26.467094999999972,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":82.54429499999998,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":98.25438599999995,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":103.29611399999999,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":4.320750000000032,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":26.31683799999996,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":69.66249899999997,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":39.154809,"failed":false}],[":tests/backup-restore.test.tsx",{"duration":3092.207656,"failed":true}],[":tests/a11y.test.tsx",{"duration":567.891542,"failed":false}]]}
1
+ {"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":19.332510000000013,"failed":false}],[":tests/components/Button.test.tsx",{"duration":67.29968700000006,"failed":false}],[":tests/components/Card.test.tsx",{"duration":22.37102600000003,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":86.52507100000003,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":103.76524499999994,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":107.89120399999996,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":4.197033999999974,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":28.41539800000004,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":70.67926699999998,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":49.17134299999998,"failed":false}],[":tests/backup-restore.test.tsx",{"duration":103.80786699999999,"failed":false}],[":tests/a11y.test.tsx",{"duration":537.859814,"failed":false}],[":tests/lib/search-fuzzy.test.ts",{"duration":6.371206000000029,"failed":false}],[":tests/components/settings-search.test.tsx",{"duration":680.7986129999999,"failed":false}],[":tests/memory-graph-view.test.tsx",{"duration":34.91235599999999,"failed":false}]]}
@@ -1051,6 +1051,115 @@ function statMaybe(path) {
1051
1051
  }
1052
1052
  }
1053
1053
 
1054
+ /**
1055
+ * Get a graph representation of the LightRAG index.
1056
+ *
1057
+ * If `root` is given, fetches entities reachable within `depth` hops
1058
+ * from that entity via LightRAG's /graph endpoint. Otherwise returns
1059
+ * all entities up to `limit`.
1060
+ *
1061
+ * Falls back to reading .bizar/memory/lightrag/kv_store_*.json directly
1062
+ * when the LightRAG server is unavailable.
1063
+ *
1064
+ * @param {{ projectRoot: string, root?: string|null, depth?: number, limit?: number }}
1065
+ * @returns {Promise<{nodes: Array<{id:string,label:string,type:string,size:number,group:string}>, edges: Array<{source:string,target:string,type:string,weight:number}>}>}
1066
+ */
1067
+ export async function getLightRAGGraph({ projectRoot, root = null, depth = 2, limit = 200 }) {
1068
+ const config = resolveLightRAGConfig(projectRoot);
1069
+
1070
+ // Attempt to query the running server.
1071
+ if (await isRunning(config)) {
1072
+ try {
1073
+ const nodes = [];
1074
+ const edges = [];
1075
+
1076
+ if (root) {
1077
+ // Use LightRAG's /graph endpoint with root entity.
1078
+ const url = `http://${config.host}:${config.port}/graph?root=${encodeURIComponent(root)}&depth=${depth}&limit=${limit}`;
1079
+ const res = await httpGet(url, config.timeoutMs);
1080
+ if (res.status === 200) {
1081
+ let parsed;
1082
+ try { parsed = JSON.parse(res.body); } catch { parsed = {}; }
1083
+ // LightRAG /graph shape varies; normalise to our contract.
1084
+ // LightRAG returns { entities: [...], relations: [...] } or similar.
1085
+ const ents = parsed.entities || parsed.nodes || [];
1086
+ const rels = parsed.relations || parsed.edges || [];
1087
+ for (const e of ents.slice(0, limit)) {
1088
+ const id = String(e.id || e.name || JSON.stringify(e));
1089
+ nodes.push({
1090
+ id,
1091
+ label: String(e.label || e.name || id).slice(0, 60),
1092
+ type: String(e.type || 'entity'),
1093
+ size: Number(e.size || 1),
1094
+ group: String(e.group || e.type || 'default'),
1095
+ });
1096
+ }
1097
+ for (const r of rels) {
1098
+ edges.push({
1099
+ source: String(r.source || r.from || r.src || ''),
1100
+ target: String(r.target || r.to || r.dst || ''),
1101
+ type: String(r.type || 'related'),
1102
+ weight: Number(r.weight || 1),
1103
+ });
1104
+ }
1105
+ return { nodes, edges };
1106
+ }
1107
+ } else {
1108
+ // Return all entities from kv_store_full_docs.json.
1109
+ const docsPath = join(config.workingDir, 'kv_store_full_docs.json');
1110
+ if (existsSync(docsPath)) {
1111
+ try {
1112
+ const docs = JSON.parse(readFileSync(docsPath, 'utf8'));
1113
+ const keys = Object.keys(docs || {}).slice(0, limit);
1114
+ // Group by top-level path segment as a proxy for group.
1115
+ for (const k of keys) {
1116
+ const parts = k.replace(/^bizar:\/\//, '').split('/');
1117
+ const group = parts.length > 1 ? parts[0] : 'root';
1118
+ nodes.push({
1119
+ id: k,
1120
+ label: parts[parts.length - 1].replace(/[_-]/g, ' '),
1121
+ type: 'note',
1122
+ size: 1,
1123
+ group,
1124
+ });
1125
+ }
1126
+ return { nodes, edges };
1127
+ } catch (err) {
1128
+ console.warn('[lightrag] getLightRAGGraph fallback read failed:', err?.message);
1129
+ }
1130
+ }
1131
+ }
1132
+ } catch (err) {
1133
+ console.warn('[lightrag] getLightRAGGraph server query failed, falling back:', err?.message);
1134
+ }
1135
+ }
1136
+
1137
+ // Fallback: read kv_store files directly.
1138
+ const nodes = [];
1139
+ const edges = [];
1140
+ try {
1141
+ const docsPath = join(config.workingDir, 'kv_store_full_docs.json');
1142
+ if (existsSync(docsPath)) {
1143
+ const docs = JSON.parse(readFileSync(docsPath, 'utf8'));
1144
+ const keys = Object.keys(docs || {}).slice(0, limit);
1145
+ for (const k of keys) {
1146
+ const parts = k.replace(/^bizar:\/\//, '').split('/');
1147
+ const group = parts.length > 1 ? parts[0] : 'root';
1148
+ nodes.push({
1149
+ id: k,
1150
+ label: parts[parts.length - 1].replace(/[_-]/g, ' '),
1151
+ type: 'note',
1152
+ size: 1,
1153
+ group,
1154
+ });
1155
+ }
1156
+ }
1157
+ } catch (err) {
1158
+ console.warn('[lightrag] getLightRAGGraph fallback read error:', err?.message);
1159
+ }
1160
+ return { nodes, edges };
1161
+ }
1162
+
1054
1163
  /**
1055
1164
  * Rebuild the graph from scratch. Stops the server, wipes the working dir,
1056
1165
  * and re-runs reindexVault. Returns { ok, started, error?, markerPath? }.
@@ -706,6 +706,127 @@ export {
706
706
  export const LIGHTRAG_DEFAULT_LLM = 'opencode/gpt-5-nano';
707
707
  export const LIGHTRAG_DEFAULT_EMBEDDING = 'opencode/text-embedding-3-small';
708
708
 
709
+ /**
710
+ * Build a wikilink link graph from all .md notes in the vault.
711
+ *
712
+ * Nodes: one per note (id = relPath, label = title or basename).
713
+ * Edges: one per wikilink [[Target]] found in note bodies.
714
+ *
715
+ * @param {{ projectRoot: string, vaultPath?: string|null, limit?: number }} opts
716
+ * @returns {{ nodes: Array<{id:string,label:string,type:string,size:number,group:string}>, edges: Array<{source:string,target:string,type:string,weight:number}> }}
717
+ */
718
+ export function getObsidianLinkGraph({ projectRoot, vaultPath = null, limit = 200 }) {
719
+ const { vaultRoot } = resolveVault(projectRoot);
720
+ const root = vaultPath || vaultRoot;
721
+ const nodesMap = new Map(); // id → node
722
+ const edges = [];
723
+
724
+ // Collect all notes.
725
+ const notes = listNotesForGraph(root);
726
+ const sliced = notes.slice(0, limit);
727
+
728
+ for (const note of sliced) {
729
+ if (!nodesMap.has(note.relPath)) {
730
+ nodesMap.set(note.relPath, {
731
+ id: note.relPath,
732
+ label: note.frontmatter?.title || note.relPath.split('/').pop()?.replace(/\.md$/i, '') || note.relPath,
733
+ type: 'note',
734
+ size: 1,
735
+ group: note.relPath.split('/')[0] || 'root',
736
+ });
737
+ }
738
+
739
+ // Extract wikilinks from body.
740
+ const WIKILINK_RE = /\[\[([^\]\n|]+?)(?:\|[^\]\n]+?)?(?:#[^\]\n]+?)?\]\]/g;
741
+ const body = note.body || '';
742
+ let m;
743
+ WIKILINK_RE.lastIndex = 0;
744
+ while ((m = WIKILINK_RE.exec(body)) !== null) {
745
+ const raw = (m[1] || '').trim();
746
+ if (!raw) continue;
747
+ const target = raw.split('#')[0].trim().split('/').pop() || raw;
748
+ // Resolve to a note id (basename match).
749
+ const targetId = resolveWikilinkTarget(target, notes);
750
+ if (targetId && targetId !== note.relPath) {
751
+ edges.push({
752
+ source: note.relPath,
753
+ target: targetId,
754
+ type: 'links_to',
755
+ weight: 1,
756
+ });
757
+ // Ensure target node exists.
758
+ if (!nodesMap.has(targetId)) {
759
+ nodesMap.set(targetId, {
760
+ id: targetId,
761
+ label: target,
762
+ type: 'note',
763
+ size: 1,
764
+ group: targetId.split('/')[0] || 'root',
765
+ });
766
+ }
767
+ }
768
+ }
769
+ }
770
+
771
+ return {
772
+ nodes: [...nodesMap.values()].slice(0, limit),
773
+ edges: edges.slice(0, limit * 3),
774
+ };
775
+ }
776
+
777
+ /**
778
+ * List notes from a specific root path (not the project vault root).
779
+ * Used by getObsidianLinkGraph for scanning arbitrary vaults.
780
+ */
781
+ function listNotesForGraph(scanRoot) {
782
+ if (!existsSync(scanRoot)) return [];
783
+ const out = [];
784
+ function walk(dir, prefix) {
785
+ let entries;
786
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
787
+ catch { return; }
788
+ for (const e of entries) {
789
+ if (e.name.startsWith('.')) continue;
790
+ const full = join(dir, e.name);
791
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
792
+ if (e.isDirectory()) {
793
+ walk(full, rel);
794
+ } else if (e.name.endsWith('.md')) {
795
+ try {
796
+ const raw = readFileSync(full, 'utf8');
797
+ const { frontmatter, body } = parseFrontmatter(raw);
798
+ out.push({ relPath: rel, frontmatter, body });
799
+ } catch { /* skip */ }
800
+ }
801
+ }
802
+ }
803
+ walk(scanRoot, '');
804
+ return out;
805
+ }
806
+
807
+ /**
808
+ * Resolve a wikilink target (e.g. "My Note" or "subdir/My Note") to a note id.
809
+ * Uses basename matching (Obsidian semantics).
810
+ */
811
+ function resolveWikilinkTarget(targetName, notes) {
812
+ const normalized = targetName.replace(/\.md$/i, '').toLowerCase();
813
+ // Try exact relPath match first.
814
+ for (const n of notes) {
815
+ const base = n.relPath.replace(/\.md$/i, '').toLowerCase();
816
+ if (base === normalized || base.endsWith('/' + normalized)) {
817
+ return n.relPath;
818
+ }
819
+ }
820
+ // Fallback: basename-only match.
821
+ for (const n of notes) {
822
+ const base = n.relPath.split('/').pop()?.replace(/\.md$/i, '').toLowerCase();
823
+ if (base === normalized) {
824
+ return n.relPath;
825
+ }
826
+ }
827
+ return null;
828
+ }
829
+
709
830
  /**
710
831
  * Return the effective LightRAG model defaults, applying env-var
711
832
  * overrides on top of the built-in opencode-Zen-free defaults.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * src/server/otel.mjs
3
+ *
4
+ * v4.9.0 — OpenTelemetry distributed tracing for the Bizar dashboard.
5
+ *
6
+ * Initialises the NodeSDK with an OTLP HTTP trace exporter and exposes
7
+ * a single `tracer` instance for ad-hoc spans (chat.send, opencode
8
+ * session creation, etc). Auto-instrumentation is intentionally not
9
+ * enabled — the dashboard already has structured logger + Prometheus
10
+ * for the "what" metrics, so distributed tracing is only interesting
11
+ * for the multi-step flows where request correlation matters
12
+ * (chat SSE pump, opencode session creation).
13
+ *
14
+ * Off by default. Enable with one of:
15
+ * BIZAR_OTEL=1
16
+ * OTEL_ENABLED=1
17
+ * OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/v1/traces (also opts in)
18
+ *
19
+ * `shutdownOtel()` flushes any pending spans and tears down the SDK.
20
+ * It is idempotent and safe to call multiple times — subsequent calls
21
+ * are a no-op once the SDK has been shut down.
22
+ *
23
+ * The NodeSDK constructor is wrapped in try/catch so that a malformed
24
+ * OTLP endpoint or missing optional binding (e.g. test runs that
25
+ * import this module but never call `initOtel`) does not crash the
26
+ * dashboard.
27
+ */
28
+ import { NodeSDK } from '@opentelemetry/sdk-node';
29
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
30
+ import { Resource } from '@opentelemetry/resources';
31
+ import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
32
+ import { trace } from '@opentelemetry/api';
33
+ import { info as logInfo, warn as logWarn } from './logger.mjs';
34
+
35
+ const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4318/v1/traces';
36
+ const SERVICE_NAME = 'bizar-dash';
37
+ const TRACER_NAME = 'bizar-dash';
38
+ const TRACER_VERSION = '0.1.0';
39
+
40
+ let sdk = null;
41
+ let shuttingDown = false;
42
+
43
+ /**
44
+ * The shared tracer used by route handlers. Bound to the global
45
+ * tracer provider — when OTEL is not enabled this resolves to a
46
+ * no-op tracer (a `startActiveSpan` call still returns a valid span
47
+ * object whose methods are all no-ops), so route handlers do not
48
+ * need a feature flag of their own.
49
+ */
50
+ export const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
51
+
52
+ /**
53
+ * Initialise the NodeSDK with an OTLP HTTP trace exporter. Safe to
54
+ * call multiple times — the second call returns the cached SDK.
55
+ *
56
+ * `npm_package_version` is set by `npm run` contexts; in any other
57
+ * context we fall back to '0.0.0'. The version is informational
58
+ * (the OTLP collector shows it in the resource panel), so failing to
59
+ * read it is never a startup blocker.
60
+ *
61
+ * @param {object} [opts]
62
+ * @param {string} [opts.serviceName] Override SERVICE_NAME (default 'bizar-dash')
63
+ * @param {string} [opts.endpoint] OTLP HTTP traces endpoint
64
+ * @returns {object|null} The NodeSDK instance, or null on failure
65
+ */
66
+ export function initOtel({
67
+ serviceName = SERVICE_NAME,
68
+ endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTLP_ENDPOINT,
69
+ } = {}) {
70
+ if (sdk) return sdk;
71
+ if (shuttingDown) return null;
72
+ try {
73
+ sdk = new NodeSDK({
74
+ resource: new Resource({
75
+ [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
76
+ [SemanticResourceAttributes.SERVICE_VERSION]:
77
+ process.env.npm_package_version || '0.0.0',
78
+ }),
79
+ traceExporter: new OTLPTraceExporter({ url: endpoint }),
80
+ });
81
+ sdk.start();
82
+ logInfo('OpenTelemetry initialised', {
83
+ module: 'otel',
84
+ serviceName,
85
+ endpoint,
86
+ });
87
+ return sdk;
88
+ } catch (err) {
89
+ logWarn('OpenTelemetry init failed; continuing without tracing', {
90
+ module: 'otel',
91
+ err: err?.message || String(err),
92
+ });
93
+ sdk = null;
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Flush pending spans and shut down the SDK. Idempotent.
100
+ *
101
+ * Designed to be wired into the dashboard's SIGTERM/SIGINT handler
102
+ * so spans in flight at shutdown are not silently dropped. Tests
103
+ * call this between scenarios to keep registry state clean.
104
+ *
105
+ * @returns {Promise<void>}
106
+ */
107
+ export async function shutdownOtel() {
108
+ if (!sdk) return;
109
+ if (shuttingDown) return;
110
+ shuttingDown = true;
111
+ try {
112
+ await sdk.shutdown();
113
+ } catch (err) {
114
+ logWarn('OpenTelemetry shutdown failed', {
115
+ module: 'otel',
116
+ err: err?.message || String(err),
117
+ });
118
+ } finally {
119
+ sdk = null;
120
+ shuttingDown = false;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Test-only: return whether the SDK is currently initialised. Lets
126
+ * tests assert that initOtel really did instantiate the SDK without
127
+ * having to introspect private state from outside.
128
+ *
129
+ * @returns {boolean}
130
+ */
131
+ export function isOtelEnabled() {
132
+ return sdk !== null;
133
+ }