@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.
Files changed (44) 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 +142 -0
  6. package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
  7. package/bizar-dash/src/server/memory-store.mjs +129 -10
  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-readlistdelete.test.mjs +405 -0
  14. package/bizar-dash/tests/memory-cli-setup.test.mjs +382 -0
  15. package/bizar-dash/tests/memory-cli.test.mjs +542 -0
  16. package/bizar-dash/tests/memory-config.test.mjs +422 -0
  17. package/bizar-dash/tests/memory-conflicts.test.mjs +229 -0
  18. package/bizar-dash/tests/memory-git.test.mjs +109 -1
  19. package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
  20. package/bizar-dash/tests/memory-namespace.test.mjs +404 -0
  21. package/bizar-dash/tests/memory-path-safety.test.mjs +427 -0
  22. package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
  23. package/bizar-dash/tests/memory-roundtrip.test.mjs +219 -0
  24. package/cli/banner.mjs +1 -1
  25. package/cli/bin.mjs +4 -4
  26. package/cli/bootstrap.mjs +1 -1
  27. package/cli/copy.mjs +22 -16
  28. package/cli/doctor.mjs +4 -4
  29. package/cli/doctor.test.mjs +2 -2
  30. package/cli/init.mjs +2 -2
  31. package/cli/install.mjs +21 -16
  32. package/cli/memory.mjs +952 -37
  33. package/cli/utils.mjs +6 -3
  34. package/config/AGENTS.md +7 -7
  35. package/config/agents/_shared/AGENT_BASELINE.md +59 -61
  36. package/config/opencode.json +13 -38
  37. package/config/skills/memory-protocol/SKILL.md +105 -0
  38. package/config/skills/obsidian/SKILL.md +58 -1
  39. package/install.sh +11 -1
  40. package/package.json +2 -2
  41. package/plugins/bizar/index.ts +7 -0
  42. package/plugins/bizar/src/commands.ts +42 -1
  43. package/plugins/bizar/src/tools/open-kb.ts +191 -0
  44. package/plugins/bizar/tests/commands.test.ts +36 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * tests/memory-cli-readlistdelete.test.mjs
3
+ *
4
+ * Integration tests for the `bizar memory read`, `bizar memory list`, and
5
+ * `bizar memory delete` CLI subcommands.
6
+ *
7
+ * The CLI calls `process.exit(1)` on errors, so we spawn the `bizar` binary
8
+ * as a subprocess in a temp dir, assert on exit code + stdout/stderr, and
9
+ * clean up after.
10
+ *
11
+ * These tests are designed to pass once Tyr lands the cmdRead / cmdList /
12
+ * cmdDelete implementations. They validate the expected CLI interface
13
+ * (exit codes, output shapes, and file-system side-effects) rather than
14
+ * the internal store API directly.
15
+ */
16
+
17
+ import { test, describe, beforeEach, afterEach } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import { tmpdir } from 'node:os';
20
+ import { join, dirname } from 'node:path';
21
+ import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
22
+ import { execFileSync } from 'node:child_process';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+
28
+ // Resolve path to the `bizar` CLI bin — invoke via `node <absolute path>`
29
+ // so the test does not depend on PATH.
30
+ const CLI_BIN = join(__dirname, '..', '..', 'cli', 'bin.mjs');
31
+
32
+ /**
33
+ * Spawn `bizar memory <subcommand> …` in the given cwd and capture exit
34
+ * code, stdout, and stderr. Returns `{ code, stdout, stderr }`.
35
+ */
36
+ function runBizarMemory(subcmd, args, cwd) {
37
+ try {
38
+ const stdout = execFileSync('node', [CLI_BIN, 'memory', subcmd, ...args], {
39
+ cwd,
40
+ encoding: 'utf8',
41
+ stdio: 'pipe',
42
+ timeout: 15_000,
43
+ });
44
+ return { code: 0, stdout, stderr: '' };
45
+ } catch (err) {
46
+ return {
47
+ code: err.status ?? 1,
48
+ stdout: err.stdout ? err.stdout.toString() : '',
49
+ stderr: err.stderr ? err.stderr.toString() : (err.message ?? ''),
50
+ };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Write a minimal `.bizar/memory.json` for local-only operation.
56
+ */
57
+ function writeMemoryConfig(cwd, overrides = {}) {
58
+ const projectId = overrides.projectId ?? `rlt-${Date.now()}`;
59
+ const config = {
60
+ version: 1,
61
+ backend: 'bizar-local',
62
+ projectId,
63
+ memoryRepo: {
64
+ mode: 'local-only',
65
+ path: join(cwd, '.obsidian'),
66
+ remote: null,
67
+ branch: 'main',
68
+ namespace: null,
69
+ },
70
+ namespaces: {
71
+ project: `projects/${projectId}`,
72
+ global: 'global/bizar',
73
+ user: 'users/tester',
74
+ },
75
+ lightrag: {
76
+ enabled: false,
77
+ host: '127.0.0.1',
78
+ port: 9621,
79
+ workingDir: join(cwd, '.bizar', 'lightrag'),
80
+ },
81
+ git: {
82
+ autoPullOnSessionStart: false,
83
+ autoCommitOnMemoryWrite: false,
84
+ autoPushOnSessionEnd: false,
85
+ commitAuthor: 'Bizar Memory <bizar-memory@local>',
86
+ commitMessageTemplate: `memory(${projectId}): {summary}`,
87
+ },
88
+ };
89
+ mkdirSync(join(cwd, '.bizar'), { recursive: true });
90
+ writeFileSync(join(cwd, '.bizar', 'memory.json'), JSON.stringify(config, null, 2));
91
+ }
92
+
93
+ // ─── bizar memory read ────────────────────────────────────────────────────────
94
+
95
+ describe('bizar memory read', () => {
96
+ let projectRoot;
97
+
98
+ beforeEach(() => {
99
+ projectRoot = join(
100
+ tmpdir(),
101
+ `bizar-memread-${Date.now()}-${Math.random().toString(36).slice(2)}`,
102
+ );
103
+ mkdirSync(projectRoot, { recursive: true });
104
+ mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
105
+ mkdirSync(join(projectRoot, '.obsidian'), { recursive: true });
106
+ writeMemoryConfig(projectRoot);
107
+ });
108
+
109
+ afterEach(() => {
110
+ try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
111
+ });
112
+
113
+ test('happy path — prints frontmatter + body to stdout and exits 0', () => {
114
+ // First write a note via the CLI (validates the full chain)
115
+ const relPath = 'decisions/0001-read-test.md';
116
+ const writeRes = runBizarMemory(
117
+ 'write',
118
+ [
119
+ relPath,
120
+ '--type', 'architecture_decision',
121
+ '--status', 'active',
122
+ '--confidence', 'verified',
123
+ '--title', 'Read test note',
124
+ '--body', '# Read test note\n\nBody content here.',
125
+ ],
126
+ projectRoot,
127
+ );
128
+ assert.strictEqual(
129
+ writeRes.code,
130
+ 0,
131
+ `write failed: ${writeRes.stderr}`,
132
+ );
133
+
134
+ // Now read it back
135
+ const r = runBizarMemory('read', [relPath], projectRoot);
136
+ assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
137
+ assert.ok(r.stdout.includes('Body content here.'), 'body should appear in stdout');
138
+ assert.ok(
139
+ r.stdout.includes('Read test note') || r.stdout.includes('architecture_decision'),
140
+ 'stdout should include title or type from frontmatter',
141
+ );
142
+ });
143
+
144
+ test('not found — exits 1 and reports the missing path', () => {
145
+ const r = runBizarMemory('read', ['does-not-exist.md'], projectRoot);
146
+ assert.strictEqual(r.code, 1, `expected exit 1, got ${r.code}`);
147
+ const out = `${r.stdout}\n${r.stderr}`;
148
+ assert.ok(
149
+ /not found|does.not.exist|no such file/i.test(out),
150
+ `expected a "not found" message, got: ${out}`,
151
+ );
152
+ });
153
+ });
154
+
155
+ // ─── bizar memory list ─────────────────────────────────────────────────────────
156
+
157
+ describe('bizar memory list', () => {
158
+ let projectRoot;
159
+
160
+ beforeEach(() => {
161
+ projectRoot = join(
162
+ tmpdir(),
163
+ `bizar-memlist-${Date.now()}-${Math.random().toString(36).slice(2)}`,
164
+ );
165
+ mkdirSync(projectRoot, { recursive: true });
166
+ mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
167
+ mkdirSync(join(projectRoot, '.obsidian'), { recursive: true });
168
+ writeMemoryConfig(projectRoot);
169
+ });
170
+
171
+ afterEach(() => {
172
+ try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
173
+ });
174
+
175
+ test('empty vault — exits 0 and prints nothing (or a zero count)', () => {
176
+ const r = runBizarMemory('list', [], projectRoot);
177
+ assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
178
+ // stdout may be empty or show "0 notes" — either is acceptable
179
+ const out = r.stdout.trim();
180
+ // If non-empty it should not contain any .md paths
181
+ if (out.length > 0) {
182
+ assert.ok(
183
+ !/\.md/.test(out),
184
+ `empty vault should not list .md files; got: ${out}`,
185
+ );
186
+ }
187
+ });
188
+
189
+ test('with notes — lists all top-level notes', () => {
190
+ const notes = [
191
+ 'decisions/0001-list-test-a.md',
192
+ 'decisions/0001-list-test-b.md',
193
+ 'decisions/0001-list-test-c.md',
194
+ ];
195
+ for (const relPath of notes) {
196
+ const r = runBizarMemory(
197
+ 'write',
198
+ [relPath, '--type', 'architecture_decision', '--body', `# ${relPath}`],
199
+ projectRoot,
200
+ );
201
+ assert.strictEqual(r.code, 0, `write failed for ${relPath}: ${r.stderr}`);
202
+ }
203
+
204
+ const listRes = runBizarMemory('list', [], projectRoot);
205
+ assert.strictEqual(listRes.code, 0, `list failed: ${listRes.stderr}`);
206
+ for (const relPath of notes) {
207
+ assert.ok(
208
+ listRes.stdout.includes(relPath),
209
+ `list output should contain ${relPath}; got: ${listRes.stdout}`,
210
+ );
211
+ }
212
+ });
213
+
214
+ test('with subdir filter — only lists notes under the specified directory', () => {
215
+ // Write notes in two different directories
216
+ const decisionsNotes = [
217
+ 'decisions/0001-subdir-test.md',
218
+ 'decisions/0002-subdir-test.md',
219
+ ];
220
+ const tasksNotes = ['tasks/0001-subdir-test.md'];
221
+
222
+ for (const relPath of [...decisionsNotes, ...tasksNotes]) {
223
+ const r = runBizarMemory(
224
+ 'write',
225
+ [relPath, '--type', relPath.startsWith('decisions') ? 'architecture_decision' : 'session_summary', '--body', `# ${relPath}`],
226
+ projectRoot,
227
+ );
228
+ assert.strictEqual(r.code, 0, `write failed for ${relPath}: ${r.stderr}`);
229
+ }
230
+
231
+ // List only decisions/
232
+ const listRes = runBizarMemory('list', ['decisions/'], projectRoot);
233
+ assert.strictEqual(listRes.code, 0, `list failed: ${listRes.stderr}`);
234
+ for (const relPath of decisionsNotes) {
235
+ assert.ok(
236
+ listRes.stdout.includes(relPath),
237
+ `list decisions/ output should contain ${relPath}; got: ${listRes.stdout}`,
238
+ );
239
+ }
240
+ for (const relPath of tasksNotes) {
241
+ assert.ok(
242
+ !listRes.stdout.includes(relPath),
243
+ `list decisions/ output should NOT contain ${relPath}; got: ${listRes.stdout}`,
244
+ );
245
+ }
246
+ });
247
+ });
248
+
249
+ // ─── bizar memory delete ───────────────────────────────────────────────────────
250
+
251
+ describe('bizar memory delete', () => {
252
+ let projectRoot;
253
+
254
+ beforeEach(() => {
255
+ projectRoot = join(
256
+ tmpdir(),
257
+ `bizar-memdel-${Date.now()}-${Math.random().toString(36).slice(2)}`,
258
+ );
259
+ mkdirSync(projectRoot, { recursive: true });
260
+ mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
261
+ mkdirSync(join(projectRoot, '.obsidian'), { recursive: true });
262
+ writeMemoryConfig(projectRoot);
263
+ });
264
+
265
+ afterEach(() => {
266
+ try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
267
+ });
268
+
269
+ test('happy path — deletes the note and exits 0', () => {
270
+ const relPath = 'decisions/0001-delete-test.md';
271
+ // First write
272
+ const writeRes = runBizarMemory(
273
+ 'write',
274
+ [relPath, '--type', 'architecture_decision', '--body', '# To be deleted'],
275
+ projectRoot,
276
+ );
277
+ assert.strictEqual(writeRes.code, 0, `write failed: ${writeRes.stderr}`);
278
+
279
+ const filePath = join(projectRoot, '.obsidian', relPath);
280
+ assert.ok(
281
+ existsSync(filePath),
282
+ `file should exist before delete: ${filePath}`,
283
+ );
284
+
285
+ // Delete
286
+ const r = runBizarMemory('delete', [relPath], projectRoot);
287
+ assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
288
+
289
+ // File should be gone
290
+ assert.ok(
291
+ !existsSync(filePath),
292
+ `file should NOT exist after delete: ${filePath}`,
293
+ );
294
+ });
295
+
296
+ test('not found — exits 1 and reports the missing path', () => {
297
+ const r = runBizarMemory('delete', ['nonexistent-note.md'], projectRoot);
298
+ assert.strictEqual(r.code, 1, `expected exit 1, got ${r.code}`);
299
+ const out = `${r.stdout}\n${r.stderr}`;
300
+ assert.ok(
301
+ /not found|no such file|does.not.exist/i.test(out),
302
+ `expected a "not found" message, got: ${out}`,
303
+ );
304
+ });
305
+
306
+ test('nested path — deletes a note in a subdirectory', () => {
307
+ const relPath = 'decisions/nested/deeply/note.md';
308
+ // Write
309
+ const writeRes = runBizarMemory(
310
+ 'write',
311
+ [relPath, '--type', 'architecture_decision', '--body', '# Nested note'],
312
+ projectRoot,
313
+ );
314
+ assert.strictEqual(writeRes.code, 0, `write failed: ${writeRes.stderr}`);
315
+
316
+ const filePath = join(projectRoot, '.obsidian', relPath);
317
+ assert.ok(existsSync(filePath), `file should exist before delete: ${filePath}`);
318
+
319
+ // Delete
320
+ const r = runBizarMemory('delete', [relPath], projectRoot);
321
+ assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
322
+
323
+ // File should be gone
324
+ assert.ok(
325
+ !existsSync(filePath),
326
+ `file should NOT exist after delete: ${filePath}`,
327
+ );
328
+ });
329
+ });
330
+
331
+ // ─── end-to-end: write → read → list → delete → list → read ──────────────────
332
+
333
+ describe('bizar memory E2E — write → read → list → delete → list → read', () => {
334
+ let projectRoot;
335
+
336
+ beforeEach(() => {
337
+ projectRoot = join(
338
+ tmpdir(),
339
+ `bizar-meme2e-${Date.now()}-${Math.random().toString(36).slice(2)}`,
340
+ );
341
+ mkdirSync(projectRoot, { recursive: true });
342
+ mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
343
+ mkdirSync(join(projectRoot, '.obsidian'), { recursive: true });
344
+ writeMemoryConfig(projectRoot);
345
+ });
346
+
347
+ afterEach(() => {
348
+ try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
349
+ });
350
+
351
+ test('full round-trip through the CLI', () => {
352
+ const relPath = 'decisions/0001-e2e.md';
353
+ const bodyText = `# E2E test note
354
+
355
+ This body should survive the full round-trip through the CLI.`;
356
+
357
+ // Step 1: write
358
+ const writeRes = runBizarMemory('write', [
359
+ relPath,
360
+ '--type', 'architecture_decision',
361
+ '--status', 'active',
362
+ '--confidence', 'verified',
363
+ '--title', 'E2E test note',
364
+ '--body', bodyText,
365
+ ], projectRoot);
366
+ assert.strictEqual(writeRes.code, 0, `write failed: ${writeRes.stderr}`);
367
+
368
+ // Step 2: read — should succeed and contain the body
369
+ const readRes = runBizarMemory('read', [relPath], projectRoot);
370
+ assert.strictEqual(readRes.code, 0, `read failed: ${readRes.stderr}`);
371
+ assert.ok(
372
+ readRes.stdout.includes('round-trip'),
373
+ `read output should contain body text; got: ${readRes.stdout}`,
374
+ );
375
+
376
+ // Step 3: list — should show the note
377
+ const listRes = runBizarMemory('list', [], projectRoot);
378
+ assert.strictEqual(listRes.code, 0, `list failed: ${listRes.stderr}`);
379
+ assert.ok(
380
+ listRes.stdout.includes(relPath),
381
+ `list should contain ${relPath}; got: ${listRes.stdout}`,
382
+ );
383
+
384
+ // Step 4: delete
385
+ const deleteRes = runBizarMemory('delete', [relPath], projectRoot);
386
+ assert.strictEqual(deleteRes.code, 0, `delete failed: ${deleteRes.stderr}`);
387
+
388
+ // Step 5: list again — should NOT show the note
389
+ const listAfterRes = runBizarMemory('list', [], projectRoot);
390
+ assert.strictEqual(listAfterRes.code, 0, `list after delete failed: ${listAfterRes.stderr}`);
391
+ assert.ok(
392
+ !listAfterRes.stdout.includes(relPath),
393
+ `list after delete should NOT contain ${relPath}; got: ${listAfterRes.stdout}`,
394
+ );
395
+
396
+ // Step 6: read — should fail with not-found
397
+ const readAfterRes = runBizarMemory('read', [relPath], projectRoot);
398
+ assert.strictEqual(readAfterRes.code, 1, `read after delete should exit 1, got ${readAfterRes.code}`);
399
+ const out = `${readAfterRes.stdout}\n${readAfterRes.stderr}`;
400
+ assert.ok(
401
+ /not found|no such file|does.not.exist/i.test(out),
402
+ `read after delete should report not found; got: ${out}`,
403
+ );
404
+ });
405
+ });