@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.
- 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 +142 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-store.mjs +129 -10
- 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-readlistdelete.test.mjs +405 -0
- package/bizar-dash/tests/memory-cli-setup.test.mjs +382 -0
- 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-conflicts.test.mjs +229 -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-namespace.test.mjs +404 -0
- package/bizar-dash/tests/memory-path-safety.test.mjs +427 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/bizar-dash/tests/memory-roundtrip.test.mjs +219 -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 +952 -37
- 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
|
@@ -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
|
-
|
|
299
|
+
const root = opts.root || vaultRoot;
|
|
300
|
+
if (!existsSync(root)) return [];
|
|
234
301
|
|
|
235
302
|
const namespace = opts.namespace || '';
|
|
236
|
-
const searchRoot = namespace ? join(
|
|
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
|
|
274
|
-
const
|
|
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
|
|
357
|
-
const
|
|
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`, `
|
|
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
|
};
|