@polderlabs/bizar 4.5.0 → 4.5.2
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/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +361 -0
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +1 -0
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
- package/bizar-dash/dist/assets/{mobile--0FBIKX3.js → mobile-BRhoDOUz.js} +1 -2
- package/bizar-dash/dist/assets/{mobile--0FBIKX3.js.map → mobile-BRhoDOUz.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-OgRp8VIb.js → mobile-lbH6szyX.js} +2 -3
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/bizar/SKILL.md +20 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +11 -0
- package/bizar-dash/skills/obsidian/SKILL.md +13 -0
- package/bizar-dash/src/server/api.mjs +6 -0
- package/bizar-dash/src/server/headroom.mjs +647 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +281 -6
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +151 -0
- package/bizar-dash/src/server/mods-loader.mjs +13 -5
- package/bizar-dash/src/server/providers-store.mjs +73 -1
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- package/bizar-dash/src/server/routes/chat.mjs +38 -0
- package/bizar-dash/src/server/routes/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +6 -2
- package/bizar-dash/src/server/routes/memory.mjs +439 -4
- package/bizar-dash/src/server/schedules-runner.mjs +4 -4
- package/bizar-dash/src/server/server.mjs +55 -37
- package/bizar-dash/src/server/watcher.mjs +2 -2
- package/bizar-dash/src/web/App.tsx +42 -6
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/Toast.tsx +1 -1
- package/bizar-dash/src/web/components/Topbar.tsx +3 -1
- package/bizar-dash/src/web/lib/api.ts +12 -11
- package/bizar-dash/src/web/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/styles/chat.css +2 -0
- package/bizar-dash/src/web/styles/main.css +52 -23
- package/bizar-dash/src/web/styles/memory.css +955 -0
- package/bizar-dash/src/web/styles/tasks.css +2 -0
- package/bizar-dash/src/web/views/Memory.tsx +141 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
- package/bizar-dash/src/web/views/Overview.tsx +7 -2
- package/bizar-dash/src/web/views/Settings.tsx +32 -1
- package/bizar-dash/src/web/views/Skills.tsx +3 -2
- package/bizar-dash/src/web/views/Tasks.tsx +4 -3
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +356 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
- package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
- package/cli/artifact.mjs +11 -4
- package/cli/bin.mjs +211 -12
- package/cli/doctor.mjs +5 -3
- package/cli/memory.mjs +65 -21
- package/cli/provision.mjs +8 -2
- package/install.sh +1 -2
- package/package.json +3 -3
- package/bizar-dash/dist/assets/main-CDFKHzBg.css +0 -1
- package/bizar-dash/dist/assets/main-NYFpS2wY.js +0 -312
- package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +0 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { Router } from 'express';
|
|
13
13
|
import { join, dirname } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, statSync, readdirSync } from 'node:fs';
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
17
|
|
|
18
18
|
const SERVER_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -78,7 +78,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
78
78
|
try {
|
|
79
79
|
const data = JSON.parse(readFileSync(reindexMarker, 'utf8'));
|
|
80
80
|
lastSecretScan = data.attemptedAt || null;
|
|
81
|
-
} catch {
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn('swallowed in memory reindex marker read:', err.message);
|
|
83
|
+
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
res.json({
|
|
@@ -486,7 +488,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
486
488
|
conflicts.push({ relPath: note.relPath, reason: 'git conflict markers' });
|
|
487
489
|
}
|
|
488
490
|
}
|
|
489
|
-
} catch {
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.warn('swallowed in memory conflict scan:', err.message);
|
|
493
|
+
}
|
|
490
494
|
|
|
491
495
|
res.json({ conflicts });
|
|
492
496
|
}));
|
|
@@ -513,11 +517,19 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
513
517
|
const { searchVault, queryLightRAG } = memoryStore;
|
|
514
518
|
const lexical = searchVault(projectRoot, q, { limit: topK });
|
|
515
519
|
let semantic = null;
|
|
520
|
+
const startedAt = Date.now();
|
|
516
521
|
try {
|
|
517
522
|
semantic = await queryLightRAG(projectRoot, q, { topK });
|
|
518
523
|
} catch (err) {
|
|
519
524
|
semantic = { ok: false, error: err.message };
|
|
520
525
|
}
|
|
526
|
+
// Record the query duration for the LightRAG stats panel. Best-effort.
|
|
527
|
+
try {
|
|
528
|
+
const { recordQuery } = await getMemoryLightrag();
|
|
529
|
+
recordQuery(projectRoot, Date.now() - startedAt);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
console.warn('swallowed in memory recordQuery:', err.message);
|
|
532
|
+
}
|
|
521
533
|
res.json({ ok: true, q, lexical, semantic });
|
|
522
534
|
}));
|
|
523
535
|
|
|
@@ -599,7 +611,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
599
611
|
const content = readFileSync(logFile, 'utf8');
|
|
600
612
|
const lines = content.split('\n');
|
|
601
613
|
logTail.push(...lines.slice(-30));
|
|
602
|
-
} catch {
|
|
614
|
+
} catch (err) {
|
|
615
|
+
console.warn('swallowed in memory log tail:', err.message);
|
|
616
|
+
}
|
|
603
617
|
}
|
|
604
618
|
|
|
605
619
|
let lastError = null;
|
|
@@ -894,5 +908,426 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
894
908
|
});
|
|
895
909
|
}));
|
|
896
910
|
|
|
911
|
+
// ── v4.7.0 — Memory tab endpoints ─────────────────────────────────────────
|
|
912
|
+
//
|
|
913
|
+
// These endpoints are the canonical surface for the dedicated Memory
|
|
914
|
+
// tab. They sit alongside the per-resource endpoints above and return
|
|
915
|
+
// the shapes the React panels expect (always objects, never arrays).
|
|
916
|
+
|
|
917
|
+
// GET /memory/health — composite health score (0..100) + breakdown.
|
|
918
|
+
// Components:
|
|
919
|
+
// - vaultExists (vault dir on disk)
|
|
920
|
+
// - vaultWritable (can write a temp file and unlink it)
|
|
921
|
+
// - gitClean (mode is managed/linked AND `git status` is clean)
|
|
922
|
+
// - lightragRunning (server up + health check ok)
|
|
923
|
+
// - schemaValid (no validation errors across the vault)
|
|
924
|
+
// - secretsClean (no HIGH-severity secret findings)
|
|
925
|
+
router.get('/memory/health', wrap(async (_req, res) => {
|
|
926
|
+
const { resolveVault, loadConfig, listNotes, validateAll, scanForSecrets } = memoryStore;
|
|
927
|
+
const { isGitInstalled, status: gitStatus } = memoryGit;
|
|
928
|
+
|
|
929
|
+
const checks = [];
|
|
930
|
+
let score = 0;
|
|
931
|
+
|
|
932
|
+
const { exists, config } = loadConfig(projectRoot);
|
|
933
|
+
if (!exists) {
|
|
934
|
+
res.json({
|
|
935
|
+
score: 0,
|
|
936
|
+
status: 'unconfigured',
|
|
937
|
+
checks: [{ name: 'config', pass: false, detail: 'memory not initialised' }],
|
|
938
|
+
message: 'memory not initialised — run `bizar memory init`',
|
|
939
|
+
});
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const { vaultRoot, mode } = resolveVault(projectRoot);
|
|
944
|
+
const vaultExists = existsSync(vaultRoot);
|
|
945
|
+
checks.push({
|
|
946
|
+
name: 'vault_exists',
|
|
947
|
+
pass: vaultExists,
|
|
948
|
+
detail: vaultExists ? vaultRoot : 'vault directory missing',
|
|
949
|
+
});
|
|
950
|
+
if (vaultExists) score += 20;
|
|
951
|
+
|
|
952
|
+
// Writable?
|
|
953
|
+
let writable = false;
|
|
954
|
+
if (vaultExists) {
|
|
955
|
+
try {
|
|
956
|
+
const probe = join(vaultRoot, '.health-probe.tmp');
|
|
957
|
+
writeFileSync(probe, 'ok');
|
|
958
|
+
try {
|
|
959
|
+
const { unlinkSync } = await import('node:fs');
|
|
960
|
+
unlinkSync(probe);
|
|
961
|
+
} catch { /* best effort */ }
|
|
962
|
+
writable = true;
|
|
963
|
+
} catch {
|
|
964
|
+
writable = false;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
checks.push({ name: 'vault_writable', pass: writable, detail: writable ? 'yes' : 'no' });
|
|
968
|
+
if (writable) score += 10;
|
|
969
|
+
|
|
970
|
+
// Git clean?
|
|
971
|
+
let gitClean = null;
|
|
972
|
+
if ((mode === 'managed' || mode === 'linked') && vaultExists && isGitInstalled()) {
|
|
973
|
+
const gs = gitStatus(vaultRoot);
|
|
974
|
+
gitClean = gs.clean;
|
|
975
|
+
checks.push({
|
|
976
|
+
name: 'git_clean',
|
|
977
|
+
pass: gs.clean,
|
|
978
|
+
detail: gs.clean ? 'working tree clean' : `${(gs.modified?.length || 0) + (gs.untracked?.length || 0)} pending`,
|
|
979
|
+
});
|
|
980
|
+
if (gs.clean) score += 20;
|
|
981
|
+
} else if (mode === 'local-only') {
|
|
982
|
+
checks.push({ name: 'git_clean', pass: true, detail: 'local-only mode (no git)' });
|
|
983
|
+
// local-only mode shouldn't penalise — count as clean
|
|
984
|
+
score += 20;
|
|
985
|
+
} else {
|
|
986
|
+
checks.push({ name: 'git_clean', pass: false, detail: 'git not installed or vault missing' });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// LightRAG running?
|
|
990
|
+
let lightragRunning = false;
|
|
991
|
+
try {
|
|
992
|
+
const { resolveLightRAGConfig, isRunning } = await getMemoryLightrag();
|
|
993
|
+
const cfg = resolveLightRAGConfig(projectRoot);
|
|
994
|
+
lightragRunning = await isRunning(cfg);
|
|
995
|
+
} catch {
|
|
996
|
+
lightragRunning = false;
|
|
997
|
+
}
|
|
998
|
+
checks.push({
|
|
999
|
+
name: 'lightrag_running',
|
|
1000
|
+
pass: lightragRunning,
|
|
1001
|
+
detail: lightragRunning ? 'yes' : 'stopped or disabled',
|
|
1002
|
+
});
|
|
1003
|
+
if (lightragRunning) score += 20;
|
|
1004
|
+
|
|
1005
|
+
// Schema valid?
|
|
1006
|
+
let invalidCount = 0;
|
|
1007
|
+
if (vaultExists) {
|
|
1008
|
+
const validationResults = validateAll(projectRoot);
|
|
1009
|
+
invalidCount = validationResults.length;
|
|
1010
|
+
}
|
|
1011
|
+
checks.push({
|
|
1012
|
+
name: 'schema_valid',
|
|
1013
|
+
pass: invalidCount === 0,
|
|
1014
|
+
detail: invalidCount === 0 ? 'all notes valid' : `${invalidCount} invalid`,
|
|
1015
|
+
});
|
|
1016
|
+
if (invalidCount === 0 && vaultExists) score += 15;
|
|
1017
|
+
|
|
1018
|
+
// Secrets clean?
|
|
1019
|
+
let highFindings = 0;
|
|
1020
|
+
if (vaultExists) {
|
|
1021
|
+
for (const n of listNotes(projectRoot)) {
|
|
1022
|
+
const r = scanForSecrets(projectRoot, n.relPath);
|
|
1023
|
+
if (!r.safe) {
|
|
1024
|
+
for (const f of r.findings) {
|
|
1025
|
+
if (f.severity === 'HIGH') highFindings++;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
checks.push({
|
|
1031
|
+
name: 'secrets_clean',
|
|
1032
|
+
pass: highFindings === 0,
|
|
1033
|
+
detail: highFindings === 0 ? 'no HIGH-severity secrets' : `${highFindings} HIGH finding(s)`,
|
|
1034
|
+
});
|
|
1035
|
+
if (highFindings === 0) score += 15;
|
|
1036
|
+
|
|
1037
|
+
const status = score >= 80 ? 'healthy' : score >= 50 ? 'degraded' : 'unhealthy';
|
|
1038
|
+
res.json({
|
|
1039
|
+
score,
|
|
1040
|
+
status,
|
|
1041
|
+
checks,
|
|
1042
|
+
message:
|
|
1043
|
+
status === 'healthy' ? 'memory system healthy' :
|
|
1044
|
+
status === 'degraded' ? 'one or more subsystems need attention' :
|
|
1045
|
+
'multiple subsystems failing',
|
|
1046
|
+
});
|
|
1047
|
+
}));
|
|
1048
|
+
|
|
1049
|
+
// GET /memory/storage — disk-usage summary for the Memory tab.
|
|
1050
|
+
// Walks the vault + .bizar/memory-cache + lightrag working dir.
|
|
1051
|
+
router.get('/memory/storage', wrap(async (_req, res) => {
|
|
1052
|
+
const { resolveVault } = memoryStore;
|
|
1053
|
+
const { vaultRoot, mode } = resolveVault(projectRoot);
|
|
1054
|
+
|
|
1055
|
+
const targets = [];
|
|
1056
|
+
if (existsSync(vaultRoot)) targets.push({ name: 'vault', path: vaultRoot });
|
|
1057
|
+
const cacheDir = join(projectRoot, '.bizar', 'memory-cache');
|
|
1058
|
+
if (existsSync(cacheDir)) targets.push({ name: 'memory-cache', path: cacheDir });
|
|
1059
|
+
const lightragDir = join(projectRoot, '.bizar', 'lightrag');
|
|
1060
|
+
if (existsSync(lightragDir)) targets.push({ name: 'lightrag', path: lightragDir });
|
|
1061
|
+
|
|
1062
|
+
let total = 0;
|
|
1063
|
+
const breakdown = targets.map((t) => {
|
|
1064
|
+
const size = dirSize(t.path);
|
|
1065
|
+
total += size;
|
|
1066
|
+
return { name: t.name, path: t.path, size };
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
res.json({
|
|
1070
|
+
total,
|
|
1071
|
+
breakdown,
|
|
1072
|
+
mode,
|
|
1073
|
+
vaultRoot,
|
|
1074
|
+
message: total === 0 ? 'no memory data on disk yet' : `${formatBytes(total)} on disk`,
|
|
1075
|
+
});
|
|
1076
|
+
}));
|
|
1077
|
+
|
|
1078
|
+
// GET /memory/git/diff — textual diff of the working tree (last commit vs HEAD).
|
|
1079
|
+
// Returns { hasDiff, lines, files } — `lines` is a flat unified-diff-ish view.
|
|
1080
|
+
router.get('/memory/git/diff', wrap(async (_req, res) => {
|
|
1081
|
+
const { resolveVault } = memoryStore;
|
|
1082
|
+
const { isGitInstalled } = memoryGit;
|
|
1083
|
+
const { vaultRoot, mode } = resolveVault(projectRoot);
|
|
1084
|
+
|
|
1085
|
+
if (mode === 'local-only') {
|
|
1086
|
+
res.json({ hasDiff: false, lines: [], files: [], mode: 'local-only' });
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (!isGitInstalled()) {
|
|
1090
|
+
res.status(503).json({ error: 'git_not_installed' });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
let raw = '';
|
|
1095
|
+
try {
|
|
1096
|
+
raw = execFileSync('git', ['diff', '--no-color', '--no-ext-diff'], {
|
|
1097
|
+
cwd: vaultRoot,
|
|
1098
|
+
encoding: 'utf8',
|
|
1099
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1100
|
+
});
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
res.status(500).json({ error: 'diff_failed', message: err.message });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Also include untracked file names.
|
|
1107
|
+
let untracked = [];
|
|
1108
|
+
try {
|
|
1109
|
+
const statusRaw = execFileSync('git', ['status', '--porcelain'], {
|
|
1110
|
+
cwd: vaultRoot,
|
|
1111
|
+
encoding: 'utf8',
|
|
1112
|
+
});
|
|
1113
|
+
for (const line of statusRaw.split('\n')) {
|
|
1114
|
+
if (line.startsWith('??')) untracked.push(line.slice(3).trim());
|
|
1115
|
+
}
|
|
1116
|
+
} catch {
|
|
1117
|
+
/* ignore */
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const lines = raw.split('\n');
|
|
1121
|
+
const files = [];
|
|
1122
|
+
for (const line of lines) {
|
|
1123
|
+
if (line.startsWith('diff --git ')) {
|
|
1124
|
+
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
1125
|
+
if (m) files.push(m[2]);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
for (const u of untracked) {
|
|
1129
|
+
if (!files.includes(u)) files.push(u);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
res.json({
|
|
1133
|
+
hasDiff: lines.length > 1 || untracked.length > 0,
|
|
1134
|
+
lines,
|
|
1135
|
+
files,
|
|
1136
|
+
mode,
|
|
1137
|
+
});
|
|
1138
|
+
}));
|
|
1139
|
+
|
|
1140
|
+
// GET /memory/lightrag/stats — aggregate stats for the LightRAG panel.
|
|
1141
|
+
router.get('/memory/lightrag/stats', wrap(async (_req, res) => {
|
|
1142
|
+
try {
|
|
1143
|
+
const { stats } = await getMemoryLightrag();
|
|
1144
|
+
const data = await stats(projectRoot);
|
|
1145
|
+
res.json(data);
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
res.status(500).json({ error: 'stats_failed', message: err.message });
|
|
1148
|
+
}
|
|
1149
|
+
}));
|
|
1150
|
+
|
|
1151
|
+
// POST /memory/lightrag/reindex — alias of /memory/reindex (the canonical
|
|
1152
|
+
// path lives there for backwards compat). Both return the same shape.
|
|
1153
|
+
router.post('/memory/lightrag/reindex', wrap(async (_req, res) => {
|
|
1154
|
+
const { reindexVault } = memoryStore;
|
|
1155
|
+
const result = await reindexVault(projectRoot, {});
|
|
1156
|
+
res.json(result);
|
|
1157
|
+
}));
|
|
1158
|
+
|
|
1159
|
+
// POST /memory/lightrag/rebuild-graph — nuke the working dir + reindex.
|
|
1160
|
+
router.post('/memory/lightrag/rebuild-graph', wrap(async (_req, res) => {
|
|
1161
|
+
try {
|
|
1162
|
+
const { rebuildGraph } = await getMemoryLightrag();
|
|
1163
|
+
const result = await rebuildGraph(projectRoot, {});
|
|
1164
|
+
res.json(result);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
res.status(500).json({ error: 'rebuild_failed', message: err.message });
|
|
1167
|
+
}
|
|
1168
|
+
}));
|
|
1169
|
+
|
|
1170
|
+
// GET /memory/obsidian/tree — recursive folder tree for the Memory browser.
|
|
1171
|
+
router.get('/memory/obsidian/tree', wrap(async (_req, res) => {
|
|
1172
|
+
try {
|
|
1173
|
+
const obsidian = await import(`${SERVER_ROOT}/memory-obsidian.mjs`);
|
|
1174
|
+
const node = obsidian.tree(projectRoot);
|
|
1175
|
+
res.json({ tree: node });
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
res.status(500).json({ error: 'tree_failed', message: err.message });
|
|
1178
|
+
}
|
|
1179
|
+
}));
|
|
1180
|
+
|
|
1181
|
+
// GET /memory/obsidian/backlinks?note=path/to/note.md
|
|
1182
|
+
router.get('/memory/obsidian/backlinks', wrap(async (req, res) => {
|
|
1183
|
+
const note = String(req.query.note || '').trim();
|
|
1184
|
+
if (!note) {
|
|
1185
|
+
res.status(400).json({ error: 'bad_request', message: 'note query param required' });
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
const obsidian = await import(`${SERVER_ROOT}/memory-obsidian.mjs`);
|
|
1190
|
+
const links = obsidian.listBacklinks(projectRoot, note);
|
|
1191
|
+
res.json({ note, backlinks: links });
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
res.status(500).json({ error: 'backlinks_failed', message: err.message });
|
|
1194
|
+
}
|
|
1195
|
+
}));
|
|
1196
|
+
|
|
1197
|
+
// GET /memory/obsidian/notes?path=...&limit=...
|
|
1198
|
+
// Query-string variant of the canonical /memory/notes endpoint. Used by
|
|
1199
|
+
// the ObsidianPanel folder browser which knows the parent path.
|
|
1200
|
+
router.get('/memory/obsidian/notes', wrap(async (req, res) => {
|
|
1201
|
+
const { listNotes } = memoryStore;
|
|
1202
|
+
const all = listNotes(projectRoot);
|
|
1203
|
+
const path = String(req.query.path || '').trim();
|
|
1204
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500);
|
|
1205
|
+
let notes = all;
|
|
1206
|
+
if (path) {
|
|
1207
|
+
notes = all.filter((n) => n.relPath.startsWith(path));
|
|
1208
|
+
}
|
|
1209
|
+
notes = notes.slice(0, limit);
|
|
1210
|
+
res.json({ path: path || null, count: notes.length, notes });
|
|
1211
|
+
}));
|
|
1212
|
+
|
|
1213
|
+
// PUT /memory/notes/* — update an existing note (or create).
|
|
1214
|
+
router.put('/memory/notes/*', wrap(async (req, res) => {
|
|
1215
|
+
const { writeNote } = memoryStore;
|
|
1216
|
+
const relPath = req.params[0];
|
|
1217
|
+
const { frontmatter, body } = req.body || {};
|
|
1218
|
+
if (!relPath) {
|
|
1219
|
+
res.status(400).json({ error: 'bad_request', message: 'path is required' });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (!relPath.endsWith('.md')) {
|
|
1223
|
+
res.status(400).json({ error: 'bad_request', message: 'path must end in .md' });
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
const note = writeNote(projectRoot, relPath, { frontmatter: frontmatter || {}, body: body || '' });
|
|
1228
|
+
res.json(note);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
if (err.code === 'SCHEMA_VALIDATION_FAILED' || err.code === 'SECRET_DETECTED') {
|
|
1231
|
+
res.status(400).json({ error: err.code, message: err.message, findings: err.findings });
|
|
1232
|
+
} else {
|
|
1233
|
+
res.status(400).json({ error: 'bad_request', message: err.message });
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}));
|
|
1237
|
+
|
|
1238
|
+
// POST /memory/semantic-search — cross-source search.
|
|
1239
|
+
// Body: { query: string, limit?: number, sources?: Array<'lightrag'|'obsidian'> }
|
|
1240
|
+
router.post('/memory/semantic-search', wrap(async (req, res) => {
|
|
1241
|
+
const body = req.body || {};
|
|
1242
|
+
const query = String(body.query || '').trim();
|
|
1243
|
+
if (!query) {
|
|
1244
|
+
res.status(400).json({ error: 'bad_request', message: 'query is required' });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const limit = Math.min(parseInt(body.limit, 10) || 10, 50);
|
|
1248
|
+
const requestedSources = Array.isArray(body.sources) && body.sources.length > 0
|
|
1249
|
+
? new Set(body.sources.map((s) => String(s).toLowerCase()))
|
|
1250
|
+
: new Set(['lightrag', 'obsidian']);
|
|
1251
|
+
|
|
1252
|
+
const results = [];
|
|
1253
|
+
if (requestedSources.has('obsidian')) {
|
|
1254
|
+
const { searchVault } = memoryStore;
|
|
1255
|
+
const lex = searchVault(projectRoot, query, { limit });
|
|
1256
|
+
for (const r of lex) {
|
|
1257
|
+
results.push({
|
|
1258
|
+
source: 'obsidian',
|
|
1259
|
+
relPath: r.relPath,
|
|
1260
|
+
snippet: r.snippet,
|
|
1261
|
+
score: r.score,
|
|
1262
|
+
mtime: r.mtime,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
if (requestedSources.has('lightrag')) {
|
|
1267
|
+
try {
|
|
1268
|
+
const { resolveLightRAGConfig, query: lightragQuery } = await getMemoryLightrag();
|
|
1269
|
+
const cfg = resolveLightRAGConfig(projectRoot);
|
|
1270
|
+
const r = await lightragQuery(cfg, query, { topK: limit });
|
|
1271
|
+
if (r.ok && r.response) {
|
|
1272
|
+
// LightRAG's response shape varies by mode; coerce to a snippet.
|
|
1273
|
+
const text = typeof r.response === 'string'
|
|
1274
|
+
? r.response
|
|
1275
|
+
: (r.response?.response || r.response?.answer || JSON.stringify(r.response));
|
|
1276
|
+
results.push({
|
|
1277
|
+
source: 'lightrag',
|
|
1278
|
+
relPath: null,
|
|
1279
|
+
snippet: String(text).slice(0, 500),
|
|
1280
|
+
score: 1,
|
|
1281
|
+
mtime: null,
|
|
1282
|
+
raw: r.response,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
results.push({ source: 'lightrag', error: err.message });
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Dedupe by (source, relPath), keep highest score.
|
|
1291
|
+
const seen = new Map();
|
|
1292
|
+
for (const r of results) {
|
|
1293
|
+
const key = `${r.source}|${r.relPath || '_query_'}`;
|
|
1294
|
+
const prev = seen.get(key);
|
|
1295
|
+
if (!prev || (r.score || 0) > (prev.score || 0)) {
|
|
1296
|
+
seen.set(key, r);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const deduped = [...seen.values()].sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
1300
|
+
|
|
1301
|
+
res.json({ query, count: deduped.length, results: deduped });
|
|
1302
|
+
}));
|
|
1303
|
+
|
|
897
1304
|
return router;
|
|
898
1305
|
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Recursively sum file sizes under `dir`. Returns 0 if dir doesn't exist.
|
|
1309
|
+
*/
|
|
1310
|
+
function dirSize(dir) {
|
|
1311
|
+
let total = 0;
|
|
1312
|
+
function walk(d) {
|
|
1313
|
+
let st;
|
|
1314
|
+
try { st = statSync(d); } catch { return; }
|
|
1315
|
+
if (st.isFile()) { total += st.size; return; }
|
|
1316
|
+
if (!st.isDirectory()) return;
|
|
1317
|
+
let entries;
|
|
1318
|
+
try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
1319
|
+
for (const e of entries) {
|
|
1320
|
+
if (e.name.startsWith('.')) continue;
|
|
1321
|
+
walk(join(d, e.name));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
walk(dir);
|
|
1325
|
+
return total;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function formatBytes(n) {
|
|
1329
|
+
if (n < 1024) return `${n} B`;
|
|
1330
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
1331
|
+
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
1332
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
1333
|
+
}
|
|
@@ -34,8 +34,8 @@ function logLine(line) {
|
|
|
34
34
|
try {
|
|
35
35
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
36
36
|
appendFileSync(LOG_FILE, line + '\n', 'utf8');
|
|
37
|
-
} catch {
|
|
38
|
-
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.warn('swallowed in schedule log append:', err.message);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -288,8 +288,8 @@ export const schedulesRunner = {
|
|
|
288
288
|
const projectIds = new Set(reg.projects.map((project) => project.id));
|
|
289
289
|
try {
|
|
290
290
|
if (schedulesStore.list('default').length > 0) projectIds.add('default');
|
|
291
|
-
} catch {
|
|
292
|
-
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.warn('swallowed in schedules due-list:', err.message);
|
|
293
293
|
}
|
|
294
294
|
let skipped = 0;
|
|
295
295
|
for (const projectId of projectIds) {
|
|
@@ -24,6 +24,7 @@ import { agentsStore } from './agents-store.mjs';
|
|
|
24
24
|
import { tasksStore } from './tasks-store.mjs';
|
|
25
25
|
import { schedulesStore } from './schedules-store.mjs';
|
|
26
26
|
import { providersStore, mcpsStore } from './providers-store.mjs';
|
|
27
|
+
import { readOpencodeJsonCached } from './providers-store.mjs';
|
|
27
28
|
import { homedir } from 'node:os';
|
|
28
29
|
import { startBgPoller, stopBgPoller } from './bg-poller.mjs';
|
|
29
30
|
import { startBgRetryLoop, stopBgRetryLoop } from './bg-retry.mjs';
|
|
@@ -281,8 +282,8 @@ export async function createServer({
|
|
|
281
282
|
if (client.bufferedAmount > WS_BACKPRESSURE_LIMIT_BYTES) {
|
|
282
283
|
try {
|
|
283
284
|
client.terminate();
|
|
284
|
-
} catch {
|
|
285
|
-
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.warn('swallowed in terminate (backpressure):', err.message);
|
|
286
287
|
}
|
|
287
288
|
return false;
|
|
288
289
|
}
|
|
@@ -315,6 +316,22 @@ export async function createServer({
|
|
|
315
316
|
broadcast: localBroadcast,
|
|
316
317
|
});
|
|
317
318
|
|
|
319
|
+
// v5.0.0 — Headroom startup hook. Runs after api.mjs is loaded so the
|
|
320
|
+
// headroom routes are registered. Errors are caught and logged — startup
|
|
321
|
+
// must not fail if Headroom has issues.
|
|
322
|
+
const { headroomStartupHook } = await import('./headroom.mjs');
|
|
323
|
+
const { readSettings } = await import('./routes/_shared.mjs');
|
|
324
|
+
try {
|
|
325
|
+
const settings = readSettings();
|
|
326
|
+
if (settings?.data?.headroom) {
|
|
327
|
+
headroomStartupHook(settings.data.headroom).catch((err) => {
|
|
328
|
+
console.warn('[bizar-dash] headroomStartupHook error:', err?.message || err);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.warn('[bizar-dash] headroom startup hook skipped:', err?.message || err);
|
|
333
|
+
}
|
|
334
|
+
|
|
318
335
|
// All /api/* routes go through apiRouter (after mod routes are checked).
|
|
319
336
|
// IMPORTANT: mount v2 router FIRST so `/api/v2/*` matches before
|
|
320
337
|
// apiRouter's internal 404 catch-all (api.mjs line ~109) can swallow it.
|
|
@@ -380,8 +397,8 @@ export async function createServer({
|
|
|
380
397
|
try {
|
|
381
398
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
382
399
|
socket.destroy();
|
|
383
|
-
} catch {
|
|
384
|
-
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.warn('swallowed in 403 destroy:', err.message);
|
|
385
402
|
}
|
|
386
403
|
return;
|
|
387
404
|
}
|
|
@@ -389,8 +406,8 @@ export async function createServer({
|
|
|
389
406
|
try {
|
|
390
407
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
391
408
|
socket.destroy();
|
|
392
|
-
} catch {
|
|
393
|
-
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.warn('swallowed in 401 destroy:', err.message);
|
|
394
411
|
}
|
|
395
412
|
return;
|
|
396
413
|
}
|
|
@@ -404,8 +421,8 @@ export async function createServer({
|
|
|
404
421
|
if (client.isAlive === false) {
|
|
405
422
|
try {
|
|
406
423
|
client.terminate();
|
|
407
|
-
} catch {
|
|
408
|
-
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.warn('swallowed in heartbeat terminate:', err.message);
|
|
409
426
|
}
|
|
410
427
|
return;
|
|
411
428
|
}
|
|
@@ -415,8 +432,8 @@ export async function createServer({
|
|
|
415
432
|
} catch {
|
|
416
433
|
try {
|
|
417
434
|
client.terminate();
|
|
418
|
-
} catch {
|
|
419
|
-
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.warn('swallowed in ping-fail terminate:', err.message);
|
|
420
437
|
}
|
|
421
438
|
}
|
|
422
439
|
});
|
|
@@ -561,8 +578,8 @@ export async function createServer({
|
|
|
561
578
|
}
|
|
562
579
|
fileSize = newSize;
|
|
563
580
|
}
|
|
564
|
-
} catch {
|
|
565
|
-
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.warn('swallowed in sendLogChunk:', err.message);
|
|
566
583
|
}
|
|
567
584
|
}
|
|
568
585
|
|
|
@@ -601,8 +618,8 @@ export async function createServer({
|
|
|
601
618
|
data: buildSnapshotSafe(state, opencodeConfigDir),
|
|
602
619
|
}),
|
|
603
620
|
);
|
|
604
|
-
} catch {
|
|
605
|
-
|
|
621
|
+
} catch (err) {
|
|
622
|
+
console.warn('swallowed in ws snapshot send:', err.message);
|
|
606
623
|
}
|
|
607
624
|
|
|
608
625
|
ws.on('message', (raw) => {
|
|
@@ -662,34 +679,34 @@ export async function createServer({
|
|
|
662
679
|
function close() {
|
|
663
680
|
try {
|
|
664
681
|
stopBgPoller();
|
|
665
|
-
} catch {
|
|
666
|
-
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.warn('swallowed in stopBgPoller:', err.message);
|
|
667
684
|
}
|
|
668
685
|
try {
|
|
669
686
|
stopBgRetryLoop();
|
|
670
|
-
} catch {
|
|
671
|
-
|
|
687
|
+
} catch (err) {
|
|
688
|
+
console.warn('swallowed in stopBgRetryLoop:', err.message);
|
|
672
689
|
}
|
|
673
690
|
try {
|
|
674
691
|
watcher.stop();
|
|
675
|
-
} catch {
|
|
676
|
-
|
|
692
|
+
} catch (err) {
|
|
693
|
+
console.warn('swallowed in watcher.stop:', err.message);
|
|
677
694
|
}
|
|
678
695
|
try {
|
|
679
696
|
wss.clients.forEach((c) => c.terminate());
|
|
680
697
|
wss.close();
|
|
681
|
-
} catch {
|
|
682
|
-
|
|
698
|
+
} catch (err) {
|
|
699
|
+
console.warn('swallowed in wss.close:', err.message);
|
|
683
700
|
}
|
|
684
701
|
try {
|
|
685
702
|
clearInterval(heartbeatInterval);
|
|
686
|
-
} catch {
|
|
687
|
-
|
|
703
|
+
} catch (err) {
|
|
704
|
+
console.warn('swallowed in clearInterval:', err.message);
|
|
688
705
|
}
|
|
689
706
|
try {
|
|
690
707
|
server.close();
|
|
691
|
-
} catch {
|
|
692
|
-
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.warn('swallowed in server.close:', err.message);
|
|
693
710
|
}
|
|
694
711
|
currentBroadcast = () => {};
|
|
695
712
|
}
|
|
@@ -707,14 +724,15 @@ function buildSnapshotSafe(state, opencodeConfigDir) {
|
|
|
707
724
|
|
|
708
725
|
function buildSnapshot(state, opencodeConfigDir) {
|
|
709
726
|
const cfgFile = join(opencodeConfigDir, 'opencode.json');
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
727
|
+
// v5.0.0 — Bug S2: read opencode.json via the 1s-debounced cache in
|
|
728
|
+
// providers-store.mjs. WS clients connect on snapshot delivery; with N
|
|
729
|
+
// clients the readFileSync here was the main per-connection blocker.
|
|
730
|
+
// The cache collapses all reads within a 1-second window to a single
|
|
731
|
+
// disk hit. Writes (settings change, providers-store mutations)
|
|
732
|
+
// invalidate the cache, so the snapshot always reflects the latest
|
|
733
|
+
// state within at most 1s.
|
|
734
|
+
const exists = existsSync(cfgFile);
|
|
735
|
+
const cfg = readOpencodeJsonCached();
|
|
718
736
|
const activeProject = projectsStore.active();
|
|
719
737
|
return {
|
|
720
738
|
overview: state.getOverview(),
|
|
@@ -724,9 +742,9 @@ function buildSnapshot(state, opencodeConfigDir) {
|
|
|
724
742
|
activeProject,
|
|
725
743
|
config: {
|
|
726
744
|
path: cfgFile,
|
|
727
|
-
data: cfg,
|
|
728
|
-
raw: cfg ? JSON.stringify(cfg, null, 2) : '',
|
|
729
|
-
exists
|
|
745
|
+
data: exists ? cfg : null,
|
|
746
|
+
raw: exists && cfg ? JSON.stringify(cfg, null, 2) : '',
|
|
747
|
+
exists,
|
|
730
748
|
},
|
|
731
749
|
settings: readSettings(),
|
|
732
750
|
tasks: activeProject ? tasksStore.loadTasks(activeProject.id) : [],
|