@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
package/cli/memory.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* cli/memory.mjs
|
|
3
3
|
*
|
|
4
4
|
* `bizar memory` subcommands. Delegates to memory-store.mjs and memory-git.mjs.
|
|
5
|
-
* Supports: init, status, link, unlink, pull, commit, push, sync,
|
|
6
|
-
* conflicts, doctor.
|
|
5
|
+
* Supports: init, status, link, unlink, write, pull, commit, push, sync,
|
|
6
|
+
* reindex, conflicts, doctor.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import chalk from 'chalk';
|
|
@@ -18,6 +18,7 @@ const memoryStore = await import(`${SERVER_ROOT}/memory-store.mjs`).then((m) =>
|
|
|
18
18
|
const memorySchema = await import(`${SERVER_ROOT}/memory-schema.mjs`).then((m) => m);
|
|
19
19
|
const memorySecrets = await import(`${SERVER_ROOT}/memory-secrets.mjs`).then((m) => m);
|
|
20
20
|
const memoryGit = await import(`${SERVER_ROOT}/memory-git.mjs`).then((m) => m);
|
|
21
|
+
const memoryLightrag = await import(`${SERVER_ROOT}/memory-lightrag.mjs`).then((m) => m);
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Get the project root. Assumes CWD is the project root.
|
|
@@ -26,6 +27,33 @@ function getProjectRoot() {
|
|
|
26
27
|
return process.cwd();
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Validate a git remote URL. Accepts HTTPS, SSH (scp-style), and SSH URL form.
|
|
32
|
+
* Rejects file://, plain paths, and other schemes.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} url
|
|
35
|
+
* @returns {{ valid: true, kind: 'ssh'|'https'|'ssh-url' } | { valid: false, error: string }}
|
|
36
|
+
*/
|
|
37
|
+
export function validateRemoteUrl(url) {
|
|
38
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
39
|
+
return { valid: false, error: 'URL must be a non-empty string' };
|
|
40
|
+
}
|
|
41
|
+
const trimmed = url.trim();
|
|
42
|
+
// SSH scp-style: git@host:path
|
|
43
|
+
if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:.+$/.test(trimmed)) {
|
|
44
|
+
return { valid: true, kind: 'ssh' };
|
|
45
|
+
}
|
|
46
|
+
// SSH URL form: ssh://[user@]host[:port]/path
|
|
47
|
+
if (/^ssh:\/\/(?:[a-zA-Z0-9._-]+@)?[a-zA-Z0-9._-]+(?::\d+)?\/.+$/.test(trimmed)) {
|
|
48
|
+
return { valid: true, kind: 'ssh-url' };
|
|
49
|
+
}
|
|
50
|
+
// HTTPS: https://host/path
|
|
51
|
+
if (/^https:\/\/[a-zA-Z0-9._-]+(?::\d+)?\/.+$/.test(trimmed)) {
|
|
52
|
+
return { valid: true, kind: 'https' };
|
|
53
|
+
}
|
|
54
|
+
return { valid: false, error: 'URL must be ssh://, https://, or git@host:path form' };
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
// ─── Helper formatters ────────────────────────────────────────────────────────
|
|
30
58
|
|
|
31
59
|
function success(msg) { console.log(chalk.green('✓'), msg); }
|
|
@@ -44,10 +72,178 @@ function kv(key, value) {
|
|
|
44
72
|
|
|
45
73
|
// ─── Subcommand implementations ───────────────────────────────────────────────
|
|
46
74
|
|
|
75
|
+
/**
|
|
76
|
+
* `bizar memory write <relpath> [--type …] [--status …] [--confidence …]`
|
|
77
|
+
* `[--tag <tag>]… [--body <body>|--body-file <file>]`
|
|
78
|
+
* `[--title <title>] [--json]`
|
|
79
|
+
*
|
|
80
|
+
* Write a single note to the vault. Validates the path, type, status, and
|
|
81
|
+
* confidence against the schema, builds frontmatter via `defaultFrontmatter`,
|
|
82
|
+
* then delegates to `memoryStore.writeNote`. Designed for agents (and humans)
|
|
83
|
+
* that want to write memory notes without going through the dashboard UI.
|
|
84
|
+
*/
|
|
85
|
+
async function cmdWrite(args) {
|
|
86
|
+
const projectRoot = getProjectRoot();
|
|
87
|
+
const { writeNote, loadConfig } = memoryStore;
|
|
88
|
+
const { defaultFrontmatter, VALID_TYPES, VALID_STATUSES, VALID_CONFIDENCES } = memorySchema;
|
|
89
|
+
|
|
90
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
91
|
+
console.log(`
|
|
92
|
+
Usage: bizar memory write <relpath> [options]
|
|
93
|
+
|
|
94
|
+
Write a single note to the vault. <relpath> must end in .md and is
|
|
95
|
+
resolved relative to the project namespace root (e.g.
|
|
96
|
+
decisions/0001-foo.md), NOT prefixed with the namespace.
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
--type <type> One of: ${VALID_TYPES.join(', ')}
|
|
100
|
+
(default: project_overview)
|
|
101
|
+
--status <status> One of: ${VALID_STATUSES.join(', ')}
|
|
102
|
+
(default: active)
|
|
103
|
+
--confidence <conf> One of: ${VALID_CONFIDENCES.join(', ')}
|
|
104
|
+
(default: verified)
|
|
105
|
+
--tag <tag> Tag to attach (may be passed multiple times)
|
|
106
|
+
--title <title> Frontmatter title field
|
|
107
|
+
--body <text> Note body as a string
|
|
108
|
+
--body-file <path> Path to a file containing the body
|
|
109
|
+
(exactly one of --body / --body-file is allowed)
|
|
110
|
+
--json Emit the full returned note as JSON
|
|
111
|
+
--help, -h Show this help
|
|
112
|
+
|
|
113
|
+
Exactly one of --body or --body-file is required, unless you intend
|
|
114
|
+
to write an empty body (in which case omit both).
|
|
115
|
+
`.trim());
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parse positional relpath (first non-flag arg)
|
|
120
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
121
|
+
const relpath = positional[0];
|
|
122
|
+
if (!relpath) {
|
|
123
|
+
error('relpath is required: `bizar memory write <relpath>`');
|
|
124
|
+
info('run `bizar memory write --help` for usage');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
if (!relpath.endsWith('.md')) {
|
|
128
|
+
error(`relpath must end in .md: ${relpath}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Parse flags ──────────────────────────────────────────────────────────
|
|
133
|
+
const optVal = (name) => {
|
|
134
|
+
const i = args.indexOf(name);
|
|
135
|
+
if (i === -1) return undefined;
|
|
136
|
+
const v = args[i + 1];
|
|
137
|
+
if (!v || v.startsWith('-')) return undefined;
|
|
138
|
+
return v;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const type = optVal('--type') ?? 'project_overview';
|
|
142
|
+
if (!VALID_TYPES.includes(type)) {
|
|
143
|
+
error(`invalid --type: '${type}'`);
|
|
144
|
+
info(`must be one of: ${VALID_TYPES.join(', ')}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const status = optVal('--status') ?? 'active';
|
|
149
|
+
if (!VALID_STATUSES.includes(status)) {
|
|
150
|
+
error(`invalid --status: '${status}'`);
|
|
151
|
+
info(`must be one of: ${VALID_STATUSES.join(', ')}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const confidence = optVal('--confidence') ?? 'verified';
|
|
156
|
+
if (!VALID_CONFIDENCES.includes(confidence)) {
|
|
157
|
+
error(`invalid --confidence: '${confidence}'`);
|
|
158
|
+
info(`must be one of: ${VALID_CONFIDENCES.join(', ')}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const title = optVal('--title');
|
|
163
|
+
|
|
164
|
+
// Multi-value --tag
|
|
165
|
+
const tags = [];
|
|
166
|
+
for (let i = 0; i < args.length; i++) {
|
|
167
|
+
if (args[i] === '--tag' && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
168
|
+
tags.push(args[i + 1]);
|
|
169
|
+
i++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const inlineBody = optVal('--body');
|
|
174
|
+
const bodyFile = optVal('--body-file');
|
|
175
|
+
if (inlineBody !== undefined && bodyFile !== undefined) {
|
|
176
|
+
error('use either --body or --body-file, not both');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let body = '';
|
|
181
|
+
if (bodyFile) {
|
|
182
|
+
try {
|
|
183
|
+
body = readFileSync(bodyFile, 'utf8');
|
|
184
|
+
} catch (err) {
|
|
185
|
+
error(`failed to read --body-file ${bodyFile}: ${err.message}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
} else if (inlineBody !== undefined) {
|
|
189
|
+
body = inlineBody;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Build frontmatter ───────────────────────────────────────────────────
|
|
193
|
+
const { config } = loadConfig(projectRoot);
|
|
194
|
+
const projectId = config.projectId || projectRoot.split('/').pop() || 'unknown';
|
|
195
|
+
|
|
196
|
+
const frontmatter = defaultFrontmatter({
|
|
197
|
+
type,
|
|
198
|
+
project_id: projectId,
|
|
199
|
+
status,
|
|
200
|
+
confidence,
|
|
201
|
+
tags,
|
|
202
|
+
});
|
|
203
|
+
if (title) frontmatter.title = title;
|
|
204
|
+
|
|
205
|
+
// ── Write ───────────────────────────────────────────────────────────────
|
|
206
|
+
let result;
|
|
207
|
+
try {
|
|
208
|
+
result = writeNote(projectRoot, relpath, { frontmatter, body });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (err.code === 'SCHEMA_VALIDATION_FAILED') {
|
|
211
|
+
error(`schema validation failed: ${err.message.replace(/^schema validation failed: /, '')}`);
|
|
212
|
+
} else if (err.code === 'SECRET_DETECTED') {
|
|
213
|
+
error(err.message);
|
|
214
|
+
} else {
|
|
215
|
+
error(err.message);
|
|
216
|
+
}
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Output ──────────────────────────────────────────────────────────────
|
|
221
|
+
if (args.includes('--json')) {
|
|
222
|
+
console.log(JSON.stringify(result, null, 2));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
success(`wrote ${result.relPath}`);
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(chalk.bold(' Note'));
|
|
229
|
+
console.log(' ─────────────────────────────');
|
|
230
|
+
kv('relpath', result.relPath);
|
|
231
|
+
kv('type', String(result.frontmatter.type));
|
|
232
|
+
kv('status', String(result.frontmatter.status));
|
|
233
|
+
kv('confidence', String(result.frontmatter.confidence));
|
|
234
|
+
kv('memory_id', String(result.frontmatter.memory_id));
|
|
235
|
+
if (Array.isArray(result.frontmatter.tags) && result.frontmatter.tags.length > 0) {
|
|
236
|
+
kv('tags', result.frontmatter.tags.join(', '));
|
|
237
|
+
}
|
|
238
|
+
if (title) kv('title', title);
|
|
239
|
+
kv('size', `${result.size} bytes`);
|
|
240
|
+
console.log();
|
|
241
|
+
}
|
|
242
|
+
|
|
47
243
|
/**
|
|
48
244
|
* `bizar memory init`
|
|
49
|
-
* Creates .bizar/memory.json with mode=
|
|
50
|
-
*
|
|
245
|
+
* Creates .bizar/memory.json with mode=managed (default).
|
|
246
|
+
* Pass --memory-mode local-only to use local vault storage instead.
|
|
51
247
|
*
|
|
52
248
|
* Flags (added for install.sh bootstrap):
|
|
53
249
|
* --yes Accept all defaults (skip prompts, skip if already initialized)
|
|
@@ -58,7 +254,25 @@ async function cmdInit(args) {
|
|
|
58
254
|
const projectRoot = getProjectRoot();
|
|
59
255
|
const { loadConfig, saveConfig } = memoryStore;
|
|
60
256
|
|
|
61
|
-
|
|
257
|
+
// ── Help (must come before any work — fixes gap in cmdInit --help coverage) ─
|
|
258
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
259
|
+
console.log(`
|
|
260
|
+
Usage: bizar memory init [options]
|
|
261
|
+
|
|
262
|
+
Initialize memory config for this project.
|
|
263
|
+
|
|
264
|
+
Options:
|
|
265
|
+
--memory-mode <managed|local-only> Vault mode (default: managed)
|
|
266
|
+
--memory-repo-name <name> Vault directory name (managed mode)
|
|
267
|
+
--yes Skip prompts; non-interactive
|
|
268
|
+
--non-interactive Alias for --yes
|
|
269
|
+
--help, -h Show this help
|
|
270
|
+
`.trim());
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --non-interactive is the canonical alias; --yes kept for backward compat.
|
|
275
|
+
const yesFlag = args.includes('--yes') || args.includes('--non-interactive');
|
|
62
276
|
|
|
63
277
|
const existing = loadConfig(projectRoot);
|
|
64
278
|
if (existing.exists) {
|
|
@@ -73,7 +287,7 @@ async function cmdInit(args) {
|
|
|
73
287
|
}
|
|
74
288
|
|
|
75
289
|
// --memory-mode takes precedence over --managed
|
|
76
|
-
let mode = '
|
|
290
|
+
let mode = 'managed';
|
|
77
291
|
const modeIdx = args.indexOf('--memory-mode');
|
|
78
292
|
if (modeIdx !== -1 && args[modeIdx + 1]) {
|
|
79
293
|
mode = args[modeIdx + 1];
|
|
@@ -157,6 +371,356 @@ async function cmdInit(args) {
|
|
|
157
371
|
}
|
|
158
372
|
}
|
|
159
373
|
|
|
374
|
+
/**
|
|
375
|
+
* `bizar memory setup` — configure or reconfigure the memory vault.
|
|
376
|
+
*
|
|
377
|
+
* v4.2.0 — first-class bootstrap entry point. Two flows:
|
|
378
|
+
* - No existing config → behaves like `init` + adds the remote (managed mode).
|
|
379
|
+
* - Existing config → optionally updates the remote URL on a managed vault.
|
|
380
|
+
*
|
|
381
|
+
* Flags:
|
|
382
|
+
* --mode <managed|local-only> Vault mode (default: managed)
|
|
383
|
+
* --remote <url> Git remote URL (required for managed)
|
|
384
|
+
* --repo-name <name> Vault directory name (default: bizar-memory)
|
|
385
|
+
* --non-interactive No prompts; use flags or defaults
|
|
386
|
+
* --yes Same as --non-interactive (backward compat)
|
|
387
|
+
*
|
|
388
|
+
* The remote URL is written to BOTH `memoryRepo.remote` (canonical,
|
|
389
|
+
* read by resolveVault) and the top-level `gitRemote` (read by cmdPush).
|
|
390
|
+
* Both must agree or `bizar memory push` will fail silently.
|
|
391
|
+
*
|
|
392
|
+
* Connectivity is checked via `memoryGit.lsRemote` after `addRemote`. A
|
|
393
|
+
* failure here is informational only — credentials may not be configured
|
|
394
|
+
* yet — and does NOT abort the setup.
|
|
395
|
+
*
|
|
396
|
+
* @param {string[]} argv
|
|
397
|
+
*/
|
|
398
|
+
export async function cmdSetup(argv) {
|
|
399
|
+
const projectRoot = getProjectRoot();
|
|
400
|
+
const { loadConfig, saveConfig, initVault, resolveVault } = memoryStore;
|
|
401
|
+
|
|
402
|
+
// ── Parse flags ──────────────────────────────────────────────────────────
|
|
403
|
+
const optVal = (name) => {
|
|
404
|
+
const i = argv.indexOf(name);
|
|
405
|
+
if (i === -1) return undefined;
|
|
406
|
+
const v = argv[i + 1];
|
|
407
|
+
if (!v || v.startsWith('-')) return undefined;
|
|
408
|
+
return v;
|
|
409
|
+
};
|
|
410
|
+
const hasFlag = (name) => argv.includes(name);
|
|
411
|
+
|
|
412
|
+
const nonInteractive = hasFlag('--non-interactive') || hasFlag('--yes');
|
|
413
|
+
|
|
414
|
+
// --mode (default managed). Accept --memory-mode alias too.
|
|
415
|
+
let mode = 'managed';
|
|
416
|
+
const modeRaw = optVal('--mode') ?? optVal('--memory-mode');
|
|
417
|
+
if (modeRaw) mode = modeRaw;
|
|
418
|
+
else if (hasFlag('--local-only')) mode = 'local-only';
|
|
419
|
+
else if (hasFlag('--managed')) mode = 'managed';
|
|
420
|
+
if (mode !== 'managed' && mode !== 'local-only') {
|
|
421
|
+
error(`invalid --mode: '${mode}' (must be 'managed' or 'local-only')`);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// --remote
|
|
426
|
+
let remote = optVal('--remote');
|
|
427
|
+
if (remote !== undefined) {
|
|
428
|
+
const v = validateRemoteUrl(remote);
|
|
429
|
+
if (!v.valid) {
|
|
430
|
+
error(`invalid --remote URL: ${v.error}`);
|
|
431
|
+
info('examples:');
|
|
432
|
+
info(' --remote git@github.com:user/repo.git');
|
|
433
|
+
info(' --remote ssh://git@github.com/user/repo.git');
|
|
434
|
+
info(' --remote https://github.com/user/repo.git');
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
// Store the trimmed canonical form
|
|
438
|
+
remote = remote.trim();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --repo-name (or --memory-repo-name, --repo)
|
|
442
|
+
const repoName = optVal('--repo-name') ?? optVal('--memory-repo-name') ?? optVal('--repo') ?? 'bizar-memory';
|
|
443
|
+
|
|
444
|
+
// ── Help ────────────────────────────────────────────────────────────────
|
|
445
|
+
if (hasFlag('--help') || hasFlag('-h')) {
|
|
446
|
+
console.log(`
|
|
447
|
+
Usage: bizar memory setup [--remote <url>] [--mode <mode>] [--non-interactive]
|
|
448
|
+
|
|
449
|
+
Configure or reconfigure this project's memory vault.
|
|
450
|
+
|
|
451
|
+
Options:
|
|
452
|
+
--mode <managed|local-only> Vault mode (default: managed)
|
|
453
|
+
--remote <url> Git remote URL (required for managed)
|
|
454
|
+
Accepts: ssh://, https://, git@host:path
|
|
455
|
+
--repo-name <name> Vault directory name (default: bizar-memory)
|
|
456
|
+
--local-only Shortcut for --mode local-only
|
|
457
|
+
--non-interactive Skip prompts; use flags or defaults
|
|
458
|
+
--yes Alias for --non-interactive
|
|
459
|
+
--help, -h Show this help
|
|
460
|
+
|
|
461
|
+
Examples:
|
|
462
|
+
# Bootstrap: first-time init with a remote
|
|
463
|
+
bizar memory setup --non-interactive \\
|
|
464
|
+
--remote git@github.com:org/bizar-memory.git
|
|
465
|
+
|
|
466
|
+
# Reconfigure the remote on an existing managed vault
|
|
467
|
+
bizar memory setup --non-interactive \\
|
|
468
|
+
--remote https://github.com/org/bizar-memory.git
|
|
469
|
+
|
|
470
|
+
# Convert to local-only (removes remote but keeps notes)
|
|
471
|
+
bizar memory setup --non-interactive --local-only
|
|
472
|
+
`.trim());
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Read existing config ────────────────────────────────────────────────
|
|
477
|
+
const { config: existingCfg, exists: configExists } = loadConfig(projectRoot);
|
|
478
|
+
|
|
479
|
+
// Decide whether this is a bootstrap (no config) or reconfigure (config
|
|
480
|
+
// already exists). The branch shape drives the rest of the function.
|
|
481
|
+
const isBootstrap = !configExists;
|
|
482
|
+
|
|
483
|
+
// Reconfigure path: if no --remote was passed AND --mode wasn't changed,
|
|
484
|
+
// just print status and exit. Otherwise apply the change.
|
|
485
|
+
if (!isBootstrap) {
|
|
486
|
+
const existingMode = existingCfg.memoryRepo?.mode || existingCfg.mode || 'local-only';
|
|
487
|
+
const existingRemote = existingCfg.memoryRepo?.remote || existingCfg.gitRemote || '';
|
|
488
|
+
const modeChanged = mode !== existingMode;
|
|
489
|
+
const remoteChanged = remote !== undefined && remote !== existingRemote;
|
|
490
|
+
|
|
491
|
+
if (!modeChanged && !remoteChanged) {
|
|
492
|
+
// Nothing to do — print status and exit
|
|
493
|
+
console.log(chalk.bold('\n Memory setup (no changes)'));
|
|
494
|
+
console.log(' ─────────────────────────────');
|
|
495
|
+
success('already configured');
|
|
496
|
+
kv('mode', existingMode);
|
|
497
|
+
kv('remote', existingRemote || '(none)');
|
|
498
|
+
kv('vault', existingCfg.memoryRepo?.path || existingCfg.repoName || '(unknown)');
|
|
499
|
+
console.log();
|
|
500
|
+
info(`run \`bizar memory setup --remote <url>\` to change the remote`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── Reconfigure path — apply in place ───────────────────────────────
|
|
505
|
+
const updatedCfg = { ...existingCfg };
|
|
506
|
+
|
|
507
|
+
if (modeChanged && remote === undefined) {
|
|
508
|
+
// Switching modes without a new remote is fine, but warn if going
|
|
509
|
+
// managed without a URL to push to.
|
|
510
|
+
if (mode === 'managed' && !existingRemote) {
|
|
511
|
+
warn('switching to managed mode with no remote configured — push will fail');
|
|
512
|
+
info('pass --remote <url> to set one');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (remote !== undefined) {
|
|
517
|
+
// Dual-write: top-level + memoryRepo.*
|
|
518
|
+
updatedCfg.gitRemote = remote;
|
|
519
|
+
updatedCfg.memoryRepo = {
|
|
520
|
+
...(existingCfg.memoryRepo || {}),
|
|
521
|
+
mode: mode === 'managed' ? 'managed' : (updatedCfg.memoryRepo?.mode || 'local-only'),
|
|
522
|
+
remote,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
if (modeChanged) {
|
|
526
|
+
updatedCfg.mode = mode;
|
|
527
|
+
if (updatedCfg.memoryRepo) {
|
|
528
|
+
updatedCfg.memoryRepo.mode = mode;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const saveResult = saveConfig(projectRoot, updatedCfg);
|
|
533
|
+
if (!saveResult.ok) {
|
|
534
|
+
error(`failed to save config: ${saveResult.error}`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// If we now have a remote on a managed vault, register it.
|
|
539
|
+
if (mode === 'managed' && remote) {
|
|
540
|
+
addRemoteToVault(projectRoot, remote);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
printSetupResult({ mode, remote, vaultPath: updatedCfg.memoryRepo?.path });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Bootstrap path (no existing config) ─────────────────────────────────
|
|
548
|
+
|
|
549
|
+
// For managed bootstrap, we need a remote. If missing, prompt or error.
|
|
550
|
+
if (mode === 'managed' && !remote) {
|
|
551
|
+
if (nonInteractive) {
|
|
552
|
+
error('--remote is required for --mode managed in non-interactive setup');
|
|
553
|
+
info('pass --remote <url> (or run without --non-interactive to be prompted)');
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
// Prompt via readline
|
|
557
|
+
const readline = await import('node:readline');
|
|
558
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
559
|
+
const answer = await new Promise((resolve) => {
|
|
560
|
+
rl.question(chalk.cyan(' remote URL (ssh://, https://, or git@host:path): '), resolve);
|
|
561
|
+
});
|
|
562
|
+
rl.close();
|
|
563
|
+
const trimmed = (answer || '').trim();
|
|
564
|
+
if (!trimmed) {
|
|
565
|
+
error('a remote URL is required for managed mode');
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
const v = validateRemoteUrl(trimmed);
|
|
569
|
+
if (!v.valid) {
|
|
570
|
+
error(`invalid remote URL: ${v.error}`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
remote = trimmed;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Build a config object mirroring cmdInit's shape, with the remote set.
|
|
577
|
+
const home = homedir();
|
|
578
|
+
const projectId = projectRoot.split('/').pop() || 'project';
|
|
579
|
+
const vaultPath = mode === 'managed'
|
|
580
|
+
? join(home, '.local', 'share', 'bizar', 'memory', repoName)
|
|
581
|
+
: join(projectRoot, '.obsidian');
|
|
582
|
+
|
|
583
|
+
const cfg = {
|
|
584
|
+
version: 1,
|
|
585
|
+
backend: 'bizar-local',
|
|
586
|
+
projectId,
|
|
587
|
+
memoryRepo: {
|
|
588
|
+
mode,
|
|
589
|
+
path: vaultPath,
|
|
590
|
+
remote: mode === 'managed' ? (remote || null) : null,
|
|
591
|
+
branch: 'main',
|
|
592
|
+
namespace: mode === 'managed' ? `projects/${projectId}` : null,
|
|
593
|
+
},
|
|
594
|
+
namespaces: {
|
|
595
|
+
project: `projects/${projectId}`,
|
|
596
|
+
global: 'global/bizar',
|
|
597
|
+
user: `users/${process.env.USER || process.env.USERNAME || 'local'}`,
|
|
598
|
+
},
|
|
599
|
+
lightrag: {
|
|
600
|
+
enabled: false,
|
|
601
|
+
host: '127.0.0.1',
|
|
602
|
+
port: 9621,
|
|
603
|
+
workingDir: join(projectRoot, '.bizar', 'lightrag'),
|
|
604
|
+
},
|
|
605
|
+
git: {
|
|
606
|
+
autoPullOnSessionStart: false,
|
|
607
|
+
autoCommitOnMemoryWrite: false,
|
|
608
|
+
autoPushOnSessionEnd: false,
|
|
609
|
+
commitAuthor: 'Bizar Memory <bizar-memory@local>',
|
|
610
|
+
commitMessageTemplate: `memory(${projectId}): {summary}`,
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// Top-level gitRemote mirrors memoryRepo.remote so cmdPush works.
|
|
615
|
+
if (mode === 'managed' && remote) {
|
|
616
|
+
cfg.gitRemote = remote;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const saveResult = saveConfig(projectRoot, cfg);
|
|
620
|
+
if (!saveResult.ok) {
|
|
621
|
+
error(`failed to write config: ${saveResult.error}`);
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Initialize the vault on disk.
|
|
626
|
+
const vaultResult = initVault(projectRoot);
|
|
627
|
+
if (!vaultResult.ok) {
|
|
628
|
+
error(`vault init failed: ${vaultResult.error || 'unknown'}`);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
if (vaultResult.created.length > 0) {
|
|
632
|
+
for (const c of vaultResult.created) info(` created: ${c}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// For managed bootstrap with a remote, register it on the new vault.
|
|
636
|
+
if (mode === 'managed' && remote) {
|
|
637
|
+
addRemoteToVault(projectRoot, remote);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
printSetupResult({ mode, remote, vaultPath });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Register `remote` as `origin` on the vault's git repo, then probe
|
|
645
|
+
* connectivity via `lsRemote`. Both operations are best-effort: we
|
|
646
|
+
* warn on failure but never abort the setup.
|
|
647
|
+
*
|
|
648
|
+
* @param {string} projectRoot
|
|
649
|
+
* @param {string} remote
|
|
650
|
+
*/
|
|
651
|
+
function addRemoteToVault(projectRoot, remote) {
|
|
652
|
+
const { resolveVault } = memoryStore;
|
|
653
|
+
const { vaultRoot } = resolveVault(projectRoot);
|
|
654
|
+
|
|
655
|
+
// Sanity check: is the vault a git repo? initVault should have ensured
|
|
656
|
+
// this, but double-check before calling git remote.
|
|
657
|
+
const gitDir = join(vaultRoot, '.git');
|
|
658
|
+
if (!existsSync(gitDir)) {
|
|
659
|
+
warn('vault is not a git repo — skipping remote registration');
|
|
660
|
+
info('run `bizar memory init` or `bizar memory sync` to repair');
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (typeof memoryGit.addRemote !== 'function') {
|
|
665
|
+
warn('memoryGit.addRemote is not available in this build');
|
|
666
|
+
info('(waiting for the addRemote/lsRemote stream to merge) — remote URL saved to config');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const addResult = memoryGit.addRemote(vaultRoot, 'origin', remote);
|
|
671
|
+
if (!addResult || addResult.ok === false) {
|
|
672
|
+
warn(`failed to register remote: ${addResult?.error || 'unknown error'}`);
|
|
673
|
+
info('the remote URL is still saved in .bizar/memory.json');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (addResult.action === 'unchanged') {
|
|
677
|
+
info(`remote 'origin' already set to ${remote}`);
|
|
678
|
+
} else if (addResult.action === 'updated') {
|
|
679
|
+
success(`updated remote 'origin' → ${remote}`);
|
|
680
|
+
} else {
|
|
681
|
+
success(`registered remote 'origin' → ${remote}`);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (typeof memoryGit.lsRemote !== 'function') {
|
|
685
|
+
info('skipping connectivity probe (lsRemote not available yet)');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const probe = memoryGit.lsRemote(vaultRoot, 'origin', { timeoutMs: 5000 });
|
|
690
|
+
if (typeof probe === 'string' && probe.length > 0) {
|
|
691
|
+
const refCount = probe.split('\n').filter(Boolean).length;
|
|
692
|
+
success(`connectivity ok (${refCount} refs reachable)`);
|
|
693
|
+
} else {
|
|
694
|
+
warn('could not reach remote (auth, network, or unknown host)');
|
|
695
|
+
info('the vault is set up; configure credentials and run `bizar memory pull`');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Render the standard "setup complete" report using the existing `kv`
|
|
701
|
+
* style. The connectivity line is omitted if not relevant.
|
|
702
|
+
*
|
|
703
|
+
* @param {{ mode: string, remote: string|undefined|null, vaultPath: string }} opts
|
|
704
|
+
*/
|
|
705
|
+
function printSetupResult({ mode, remote, vaultPath }) {
|
|
706
|
+
console.log();
|
|
707
|
+
success('memory setup complete');
|
|
708
|
+
console.log(' ─────────────────────────────');
|
|
709
|
+
kv('mode', mode);
|
|
710
|
+
kv('vault', String(vaultPath || ''));
|
|
711
|
+
if (remote) kv('remote', remote);
|
|
712
|
+
console.log();
|
|
713
|
+
if (!remote) {
|
|
714
|
+
info('local-only mode — notes are stored in this project only');
|
|
715
|
+
info('run `bizar memory setup --remote <url>` to attach a remote later');
|
|
716
|
+
} else {
|
|
717
|
+
info('next steps:');
|
|
718
|
+
info(' 1. `bizar memory pull` to fetch the existing vault (if any)');
|
|
719
|
+
info(' 2. `bizar memory write <relpath>` to start adding notes');
|
|
720
|
+
info(' 3. `bizar memory push` to publish');
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
160
724
|
/**
|
|
161
725
|
* `bizar memory status`
|
|
162
726
|
*/
|
|
@@ -204,15 +768,28 @@ async function cmdStatus(_args) {
|
|
|
204
768
|
}
|
|
205
769
|
}
|
|
206
770
|
|
|
207
|
-
// LightRAG
|
|
208
|
-
|
|
771
|
+
// LightRAG status
|
|
772
|
+
try {
|
|
773
|
+
const { isLightRAGRunning, isLightRAGInstalled, resolveLightRAGConfig } = memoryStore;
|
|
774
|
+
const cfg = resolveLightRAGConfig(projectRoot);
|
|
775
|
+
if (!cfg.enabled) {
|
|
776
|
+
kv('lightrag', chalk.gray('disabled'));
|
|
777
|
+
} else if (!(await isLightRAGInstalled())) {
|
|
778
|
+
kv('lightrag', chalk.yellow('not installed — `uv tool install "lightrag-hku[api]"`'));
|
|
779
|
+
} else {
|
|
780
|
+
const running = await isLightRAGRunning(cfg);
|
|
781
|
+
kv('lightrag', running ? chalk.green(`${cfg.host}:${cfg.port} (running)`) : chalk.yellow(`${cfg.host}:${cfg.port} (not running — run \`bizar memory reindex\`)`));
|
|
782
|
+
}
|
|
783
|
+
} catch (err) {
|
|
784
|
+
kv('lightrag', chalk.gray(`status unavailable (${err.message})`));
|
|
785
|
+
}
|
|
209
786
|
|
|
210
787
|
// Last reindex attempt
|
|
211
|
-
const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex
|
|
788
|
+
const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex.json');
|
|
212
789
|
if (existsSync(reindexMarker)) {
|
|
213
790
|
try {
|
|
214
|
-
const {
|
|
215
|
-
kv('lastReindex', `${new Date(
|
|
791
|
+
const { finishedAt, ok, inserted, failed, noteCount } = JSON.parse(readFileSync(reindexMarker, 'utf8'));
|
|
792
|
+
kv('lastReindex', `${new Date(finishedAt).toLocaleString()} — ${ok ? chalk.green(`${inserted}/${noteCount} ok`) : chalk.red(`${failed} failed`)}`);
|
|
216
793
|
} catch { /* ignore */ }
|
|
217
794
|
}
|
|
218
795
|
|
|
@@ -320,7 +897,7 @@ async function cmdUnlink(_args) {
|
|
|
320
897
|
async function cmdPull(_args) {
|
|
321
898
|
const projectRoot = getProjectRoot();
|
|
322
899
|
const { loadConfig, resolveVault } = memoryStore;
|
|
323
|
-
const { pull, isGitInstalled } = memoryGit;
|
|
900
|
+
const { pull, isGitInstalled, ensureUpstream } = memoryGit;
|
|
324
901
|
|
|
325
902
|
const { config } = loadConfig(projectRoot);
|
|
326
903
|
if (config.mode === 'local-only') {
|
|
@@ -333,7 +910,18 @@ async function cmdPull(_args) {
|
|
|
333
910
|
process.exit(1);
|
|
334
911
|
}
|
|
335
912
|
|
|
336
|
-
const { vaultRoot } = resolveVault(projectRoot);
|
|
913
|
+
const { vaultRoot, branch } = resolveVault(projectRoot);
|
|
914
|
+
const remoteName = config.gitRemote ? 'origin' : 'origin';
|
|
915
|
+
|
|
916
|
+
// Pre-flight: ensure upstream is set. Without this, `git pull` fails with
|
|
917
|
+
// "no tracking information" on freshly-cloned or locally-initialized vaults.
|
|
918
|
+
const upstreamResult = ensureUpstream(vaultRoot, config.branch || branch || 'main', remoteName);
|
|
919
|
+
if (!upstreamResult.ok) {
|
|
920
|
+
info(`upstream not set (${upstreamResult.error}); pull may fail`);
|
|
921
|
+
} else if (upstreamResult.action === 'set') {
|
|
922
|
+
success(`set upstream to ${remoteName}/${config.branch || branch || 'main'}`);
|
|
923
|
+
}
|
|
924
|
+
|
|
337
925
|
info(`pulling into ${vaultRoot}`);
|
|
338
926
|
const result = pull(vaultRoot);
|
|
339
927
|
if (!result.ok) {
|
|
@@ -426,7 +1014,7 @@ async function cmdPush(_args) {
|
|
|
426
1014
|
async function cmdSync(args) {
|
|
427
1015
|
const projectRoot = getProjectRoot();
|
|
428
1016
|
const { loadConfig, resolveVault, validateAll, scanForSecrets, listNotes } = memoryStore;
|
|
429
|
-
const { pull, addAll, commit: gitCommit, push: gitPush, status: gitStatus, acquireLock, isGitInstalled } = memoryGit;
|
|
1017
|
+
const { pull, addAll, commit: gitCommit, push: gitPush, status: gitStatus, acquireLock, isGitInstalled, ensureUpstream } = memoryGit;
|
|
430
1018
|
|
|
431
1019
|
const { config } = loadConfig(projectRoot);
|
|
432
1020
|
if (config.mode === 'local-only') {
|
|
@@ -439,7 +1027,19 @@ async function cmdSync(args) {
|
|
|
439
1027
|
process.exit(1);
|
|
440
1028
|
}
|
|
441
1029
|
|
|
442
|
-
const { vaultRoot } = resolveVault(projectRoot);
|
|
1030
|
+
const { vaultRoot, branch } = resolveVault(projectRoot);
|
|
1031
|
+
const branchName = config.branch || branch || 'main';
|
|
1032
|
+
|
|
1033
|
+
// Pre-flight: ensure upstream is set. Some workflows create a vault before
|
|
1034
|
+
// origin exists; without upstream, `git pull` fails with "no tracking".
|
|
1035
|
+
if (config.gitRemote) {
|
|
1036
|
+
const upstreamResult = ensureUpstream(vaultRoot, branchName, 'origin');
|
|
1037
|
+
if (!upstreamResult.ok) {
|
|
1038
|
+
info(`upstream not set (${upstreamResult.error}); pull may fail`);
|
|
1039
|
+
} else if (upstreamResult.action === 'set') {
|
|
1040
|
+
success(`set upstream to origin/${branchName}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
443
1043
|
|
|
444
1044
|
// Acquire lock
|
|
445
1045
|
const lock = acquireLock(vaultRoot);
|
|
@@ -543,24 +1143,115 @@ async function cmdSync(args) {
|
|
|
543
1143
|
|
|
544
1144
|
/**
|
|
545
1145
|
* `bizar memory reindex`
|
|
546
|
-
*
|
|
1146
|
+
* v4.1.0 — populates the LightRAG server with every note in the vault.
|
|
1147
|
+
*
|
|
1148
|
+
* Steps:
|
|
1149
|
+
* 1. Resolve LightRAG config from `.bizar/memory.json`.
|
|
1150
|
+
* 2. Ensure `lightrag-server` is installed (searches common paths).
|
|
1151
|
+
* 3. Ensure server is running (auto-starts if not).
|
|
1152
|
+
* 4. List all notes from the vault.
|
|
1153
|
+
* 5. Insert each note via `POST /documents` with stable id
|
|
1154
|
+
* `bizar://<projectId>/<relPath>`.
|
|
1155
|
+
* 6. Write marker file with stats.
|
|
1156
|
+
*
|
|
1157
|
+
* The reindex is idempotent — re-running re-inserts the same notes under
|
|
1158
|
+
* the same ids; LightRAG's id-keyed storage handles this gracefully.
|
|
547
1159
|
*/
|
|
548
|
-
async function cmdReindex(
|
|
1160
|
+
async function cmdReindex(args) {
|
|
549
1161
|
const projectRoot = getProjectRoot();
|
|
550
|
-
const
|
|
1162
|
+
const _atomic = await import('./atomic.mjs').then((m) => m);
|
|
551
1163
|
const cacheDir = join(projectRoot, '.bizar', 'memory-cache');
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1164
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
1165
|
+
|
|
1166
|
+
const { isLightRAGInstalled, reindexVault, resolveLightRAGConfig } = memoryStore;
|
|
1167
|
+
|
|
1168
|
+
// Preflight: is lightrag-server installed?
|
|
1169
|
+
if (!(await isLightRAGInstalled())) {
|
|
1170
|
+
error('lightrag-server not installed');
|
|
1171
|
+
info('install it with: uv tool install "lightrag-hku[api]"');
|
|
1172
|
+
info('then run `bizar memory reindex` again');
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Preflight: is lightrag enabled in config?
|
|
1177
|
+
const config = resolveLightRAGConfig(projectRoot);
|
|
1178
|
+
if (!config.enabled) {
|
|
1179
|
+
warn('lightrag is disabled in .bizar/memory.json (lightrag.enabled = false)');
|
|
1180
|
+
info('set lightrag.enabled = true and run `bizar memory reindex` again');
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
info(`reindexing vault into LightRAG at http://${config.host}:${config.port}…`);
|
|
1185
|
+
const result = await reindexVault(projectRoot, { logger: console });
|
|
1186
|
+
|
|
1187
|
+
if (!result.ok && result.inserted === 0) {
|
|
1188
|
+
error(`reindex failed: ${result.error || 'unknown'}`);
|
|
1189
|
+
if (result.failures?.length > 0) {
|
|
1190
|
+
info(`first failure: ${result.failures[0].relPath} — ${result.failures[0].error}`);
|
|
1191
|
+
}
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if (result.started) {
|
|
1196
|
+
success(`started LightRAG server (pid ${result.pid})`);
|
|
1197
|
+
}
|
|
1198
|
+
success(`reindexed ${result.inserted}/${result.noteCount} notes in ${result.durationMs}ms`);
|
|
1199
|
+
if (result.failed > 0) {
|
|
1200
|
+
warn(`${result.failed} notes failed — see marker for details`);
|
|
1201
|
+
}
|
|
1202
|
+
info(`marker: ${result.markerPath}`);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* `bizar memory search <query>`
|
|
1207
|
+
* v4.1.0 — merged lexical + semantic search.
|
|
1208
|
+
*
|
|
1209
|
+
* - Lexical: token-frequency matching against vault notes (instant, free).
|
|
1210
|
+
* - Semantic: LightRAG `/query` with `mode: 'mix'` (combines local entity
|
|
1211
|
+
* graph + global relation graph). Skipped if LightRAG isn't running.
|
|
1212
|
+
*/
|
|
1213
|
+
async function cmdSearch(args) {
|
|
1214
|
+
const query = args.join(' ').trim();
|
|
1215
|
+
if (!query) {
|
|
1216
|
+
error('search requires a query: `bizar memory search <query>`');
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
}
|
|
1219
|
+
const projectRoot = getProjectRoot();
|
|
1220
|
+
const { searchVault } = memoryStore;
|
|
1221
|
+
|
|
1222
|
+
// Lexical
|
|
1223
|
+
const lexical = searchVault(projectRoot, query, { limit: 10 });
|
|
1224
|
+
console.log(chalk.bold('\n Lexical results (token-frequency):'));
|
|
1225
|
+
if (lexical.length === 0) {
|
|
1226
|
+
console.log(chalk.dim(' (no matches)'));
|
|
1227
|
+
} else {
|
|
1228
|
+
for (const r of lexical.slice(0, 5)) {
|
|
1229
|
+
console.log(` ${chalk.cyan(r.relPath)} ${chalk.dim(`(score ${r.score})`)}`);
|
|
1230
|
+
console.log(chalk.dim(` ${r.snippet}`));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Semantic (best-effort)
|
|
1235
|
+
console.log(chalk.bold('\n Semantic answer (LightRAG):'));
|
|
1236
|
+
try {
|
|
1237
|
+
const cfg = memoryStore.resolveLightRAGConfig(projectRoot);
|
|
1238
|
+
const semantic = await memoryLightrag.query(cfg, query, { topK: 10 });
|
|
1239
|
+
if (!semantic.ok) {
|
|
1240
|
+
console.log(chalk.dim(` ${semantic.error || 'unavailable'}`));
|
|
1241
|
+
if (semantic.error?.includes('not running')) {
|
|
1242
|
+
console.log(chalk.dim(' tip: run `bizar memory reindex` to start + populate LightRAG'));
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
const response = semantic.response?.response || semantic.response?.answer || '';
|
|
1246
|
+
if (response) {
|
|
1247
|
+
console.log(` ${response.split('\n').join('\n ')}`);
|
|
1248
|
+
} else {
|
|
1249
|
+
console.log(chalk.dim(' (no answer returned)'));
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
console.log(chalk.dim(` ${err.message}`));
|
|
1254
|
+
}
|
|
564
1255
|
}
|
|
565
1256
|
|
|
566
1257
|
/**
|
|
@@ -587,7 +1278,7 @@ async function cmdConflicts(_args) {
|
|
|
587
1278
|
const filePath = join(vaultRoot, note.relPath);
|
|
588
1279
|
try {
|
|
589
1280
|
const content = rf(filePath, 'utf8');
|
|
590
|
-
if (/^<{7}\s|^={7}\s
|
|
1281
|
+
if (/^<{7}\s|^={7}\s|^>{7}\s/m.test(content)) {
|
|
591
1282
|
conflicts.push({ relPath: note.relPath, reason: 'git conflict markers detected' });
|
|
592
1283
|
}
|
|
593
1284
|
} catch { /* skip */ }
|
|
@@ -655,17 +1346,35 @@ async function cmdDoctor(_args) {
|
|
|
655
1346
|
allPassed = false;
|
|
656
1347
|
}
|
|
657
1348
|
|
|
658
|
-
// Check 7: last
|
|
659
|
-
const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex
|
|
1349
|
+
// Check 7: last reindex
|
|
1350
|
+
const reindexMarker = join(projectRoot, '.bizar', 'memory-cache', 'last-reindex.json');
|
|
660
1351
|
if (existsSync(reindexMarker)) {
|
|
661
1352
|
try {
|
|
662
|
-
const {
|
|
663
|
-
const ageMs = Date.now() - new Date(
|
|
1353
|
+
const { finishedAt, ok, inserted, failed, noteCount } = JSON.parse(readFileSync(reindexMarker, 'utf8'));
|
|
1354
|
+
const ageMs = Date.now() - new Date(finishedAt).getTime();
|
|
664
1355
|
const ageH = ageMs / 3600000;
|
|
665
|
-
|
|
1356
|
+
const summary = `${inserted}/${noteCount} inserted${failed > 0 ? `, ${failed} failed` : ''}`;
|
|
1357
|
+
const status = ok ? chalk.green(summary) : chalk.red(summary);
|
|
1358
|
+
kv('lastReindex', ageH < 24 ? `${status} ${chalk.dim(`(${Math.round(ageH)}h ago)`)}` : `${status} ${chalk.yellow(`(${Math.round(ageH)}h ago)`)}`);
|
|
666
1359
|
} catch { /* ignore */ }
|
|
667
1360
|
} else {
|
|
668
|
-
kv('lastReindex', chalk.yellow('never'));
|
|
1361
|
+
kv('lastReindex', chalk.yellow('never — run `bizar memory reindex`'));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Check 8: LightRAG status
|
|
1365
|
+
try {
|
|
1366
|
+
const { isLightRAGInstalled, isLightRAGRunning, resolveLightRAGConfig } = memoryStore;
|
|
1367
|
+
const cfg = resolveLightRAGConfig(projectRoot);
|
|
1368
|
+
if (!cfg.enabled) {
|
|
1369
|
+
kv('lightrag', chalk.gray('disabled'));
|
|
1370
|
+
} else if (!(await isLightRAGInstalled())) {
|
|
1371
|
+
kv('lightrag', chalk.yellow('not installed — `uv tool install "lightrag-hku[api]"`'));
|
|
1372
|
+
} else {
|
|
1373
|
+
const running = await isLightRAGRunning(cfg);
|
|
1374
|
+
kv('lightrag', running ? chalk.green(`${cfg.host}:${cfg.port} running`) : chalk.yellow(`not running — run \`bizar memory reindex\``));
|
|
1375
|
+
}
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
kv('lightrag', chalk.gray(`status check failed: ${err.message}`));
|
|
669
1378
|
}
|
|
670
1379
|
}
|
|
671
1380
|
|
|
@@ -678,6 +1387,189 @@ async function cmdDoctor(_args) {
|
|
|
678
1387
|
console.log();
|
|
679
1388
|
}
|
|
680
1389
|
|
|
1390
|
+
// ─── read / list / delete (Fix 4 + Fix 5 plumbing) ────────────────────────────
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Parse `--namespace <ns>` and return the chosen namespace plus the remaining
|
|
1394
|
+
* positional args. Defaults to 'project' when the flag is absent.
|
|
1395
|
+
*/
|
|
1396
|
+
function parseNamespaceFlag(args) {
|
|
1397
|
+
const idx = args.indexOf('--namespace');
|
|
1398
|
+
if (idx === -1) return { namespace: 'project', rest: args };
|
|
1399
|
+
const v = args[idx + 1];
|
|
1400
|
+
if (!v || v.startsWith('-')) return { namespace: 'project', rest: args };
|
|
1401
|
+
// Remove the flag and its value from the rest
|
|
1402
|
+
const rest = args.filter((a, i) => i !== idx && i !== idx + 1);
|
|
1403
|
+
return { namespace: v, rest };
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* `bizar memory read [--namespace <ns>] <relpath>`
|
|
1408
|
+
*
|
|
1409
|
+
* Read a single note and print its raw content (frontmatter + body) to stdout.
|
|
1410
|
+
*/
|
|
1411
|
+
async function cmdRead(args) {
|
|
1412
|
+
const projectRoot = getProjectRoot();
|
|
1413
|
+
const { resolveVault, resolveNamespaceRoot } = memoryStore;
|
|
1414
|
+
|
|
1415
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1416
|
+
console.log(`
|
|
1417
|
+
Usage: bizar memory read [--namespace <ns>] <relpath>
|
|
1418
|
+
|
|
1419
|
+
Read a single note and print it to stdout (YAML frontmatter + body).
|
|
1420
|
+
|
|
1421
|
+
Options:
|
|
1422
|
+
--namespace <project|global|user> Namespace to read from (default: project)
|
|
1423
|
+
--help, -h Show this help
|
|
1424
|
+
`.trim());
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const { namespace, rest } = parseNamespaceFlag(args);
|
|
1429
|
+
const relpath = rest.filter((a) => !a.startsWith('-'))[0];
|
|
1430
|
+
if (!relpath) {
|
|
1431
|
+
error('relpath is required: `bizar memory read <relpath>`');
|
|
1432
|
+
info('run `bizar memory read --help` for usage');
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
let note;
|
|
1437
|
+
if (namespace === 'project') {
|
|
1438
|
+
note = memoryStore.readNote(projectRoot, relpath);
|
|
1439
|
+
} else {
|
|
1440
|
+
const vi = resolveVault(projectRoot);
|
|
1441
|
+
const root = resolveNamespaceRoot(vi, namespace);
|
|
1442
|
+
if (!root) {
|
|
1443
|
+
error(`unknown namespace: ${namespace}`);
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
}
|
|
1446
|
+
note = memoryStore.readNote(projectRoot, relpath, { root });
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (!note) {
|
|
1450
|
+
error(`note not found: ${relpath}`);
|
|
1451
|
+
process.exit(1);
|
|
1452
|
+
}
|
|
1453
|
+
process.stdout.write(note.raw);
|
|
1454
|
+
if (!note.raw.endsWith('\n')) process.stdout.write('\n');
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* `bizar memory list [--namespace <ns>] [--json] [dir]`
|
|
1459
|
+
*
|
|
1460
|
+
* List notes under a directory. Default: walk the project namespace root.
|
|
1461
|
+
* Pass `dir` (e.g. `decisions/`) to scope to a subdirectory. The dir is
|
|
1462
|
+
* resolved RELATIVE TO the namespace root.
|
|
1463
|
+
*/
|
|
1464
|
+
async function cmdList(args) {
|
|
1465
|
+
const projectRoot = getProjectRoot();
|
|
1466
|
+
const { resolveVault, resolveNamespaceRoot } = memoryStore;
|
|
1467
|
+
|
|
1468
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1469
|
+
console.log(`
|
|
1470
|
+
Usage: bizar memory list [--namespace <ns>] [dir]
|
|
1471
|
+
|
|
1472
|
+
List notes under a directory. Default: the project namespace root.
|
|
1473
|
+
|
|
1474
|
+
Options:
|
|
1475
|
+
--namespace <project|global|user> Namespace to list (default: project)
|
|
1476
|
+
--json Emit JSON array
|
|
1477
|
+
[dir] Subdirectory under the namespace (e.g. decisions/)
|
|
1478
|
+
--help, -h Show this help
|
|
1479
|
+
`.trim());
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const { namespace, rest } = parseNamespaceFlag(args);
|
|
1484
|
+
const asJson = rest.includes('--json');
|
|
1485
|
+
const dir = rest.filter((a) => !a.startsWith('-'))[0] || '';
|
|
1486
|
+
|
|
1487
|
+
let notes;
|
|
1488
|
+
if (namespace === 'project') {
|
|
1489
|
+
notes = memoryStore.listNotes(projectRoot, dir ? { namespace: dir } : {});
|
|
1490
|
+
} else {
|
|
1491
|
+
const vi = resolveVault(projectRoot);
|
|
1492
|
+
const root = resolveNamespaceRoot(vi, namespace);
|
|
1493
|
+
if (!root) {
|
|
1494
|
+
error(`unknown namespace: ${namespace}`);
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
notes = memoryStore.listNotes(projectRoot, dir ? { root, namespace: dir } : { root });
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// listNotes returns relPaths RELATIVE to the search root, so when we
|
|
1501
|
+
// filter by `dir` we need to re-attach the dir prefix for display —
|
|
1502
|
+
// otherwise the user sees `0001-foo.md` instead of the more useful
|
|
1503
|
+
// `decisions/0001-foo.md`. Strip a trailing slash for cleanliness.
|
|
1504
|
+
const displayPrefix = dir ? dir.replace(/\/+$/, '') + '/' : '';
|
|
1505
|
+
|
|
1506
|
+
if (asJson) {
|
|
1507
|
+
console.log(JSON.stringify(notes.map((n) => ({
|
|
1508
|
+
relPath: displayPrefix + n.relPath,
|
|
1509
|
+
size: n.size,
|
|
1510
|
+
mtime: n.mtime,
|
|
1511
|
+
})), null, 2));
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (notes.length === 0) {
|
|
1515
|
+
console.log(chalk.dim(' (no notes)'));
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
for (const n of notes) {
|
|
1519
|
+
console.log(` ${displayPrefix}${n.relPath}`);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* `bizar memory delete [--namespace <ns>] <relpath>`
|
|
1525
|
+
*
|
|
1526
|
+
* Delete a single note from the vault.
|
|
1527
|
+
*/
|
|
1528
|
+
async function cmdDelete(args) {
|
|
1529
|
+
const projectRoot = getProjectRoot();
|
|
1530
|
+
const { resolveVault, resolveNamespaceRoot } = memoryStore;
|
|
1531
|
+
|
|
1532
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1533
|
+
console.log(`
|
|
1534
|
+
Usage: bizar memory delete [--namespace <ns>] <relpath>
|
|
1535
|
+
|
|
1536
|
+
Delete a single note from the vault.
|
|
1537
|
+
|
|
1538
|
+
Options:
|
|
1539
|
+
--namespace <project|global|user> Namespace to delete from (default: project)
|
|
1540
|
+
--help, -h Show this help
|
|
1541
|
+
`.trim());
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const { namespace, rest } = parseNamespaceFlag(args);
|
|
1546
|
+
const relpath = rest.filter((a) => !a.startsWith('-'))[0];
|
|
1547
|
+
if (!relpath) {
|
|
1548
|
+
error('relpath is required: `bizar memory delete <relpath>`');
|
|
1549
|
+
info('run `bizar memory delete --help` for usage');
|
|
1550
|
+
process.exit(1);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
let ok;
|
|
1554
|
+
if (namespace === 'project') {
|
|
1555
|
+
ok = memoryStore.deleteNote(projectRoot, relpath);
|
|
1556
|
+
} else {
|
|
1557
|
+
const vi = resolveVault(projectRoot);
|
|
1558
|
+
const root = resolveNamespaceRoot(vi, namespace);
|
|
1559
|
+
if (!root) {
|
|
1560
|
+
error(`unknown namespace: ${namespace}`);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
ok = memoryStore.deleteNote(projectRoot, relpath, { root });
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (!ok) {
|
|
1567
|
+
error(`note not found: ${relpath}`);
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
success(`deleted ${relpath}`);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
681
1573
|
// ─── Dispatcher ──────────────────────────────────────────────────────────────
|
|
682
1574
|
|
|
683
1575
|
/**
|
|
@@ -689,6 +1581,9 @@ export async function runMemory(subcommand, args) {
|
|
|
689
1581
|
case 'init':
|
|
690
1582
|
await cmdInit(args);
|
|
691
1583
|
break;
|
|
1584
|
+
case 'setup':
|
|
1585
|
+
await cmdSetup(args);
|
|
1586
|
+
break;
|
|
692
1587
|
case 'status':
|
|
693
1588
|
await cmdStatus(args);
|
|
694
1589
|
break;
|
|
@@ -698,6 +1593,18 @@ export async function runMemory(subcommand, args) {
|
|
|
698
1593
|
case 'unlink':
|
|
699
1594
|
await cmdUnlink(args);
|
|
700
1595
|
break;
|
|
1596
|
+
case 'write':
|
|
1597
|
+
await cmdWrite(args);
|
|
1598
|
+
break;
|
|
1599
|
+
case 'read':
|
|
1600
|
+
await cmdRead(args);
|
|
1601
|
+
break;
|
|
1602
|
+
case 'list':
|
|
1603
|
+
await cmdList(args);
|
|
1604
|
+
break;
|
|
1605
|
+
case 'delete':
|
|
1606
|
+
await cmdDelete(args);
|
|
1607
|
+
break;
|
|
701
1608
|
case 'pull':
|
|
702
1609
|
await cmdPull(args);
|
|
703
1610
|
break;
|
|
@@ -713,6 +1620,9 @@ export async function runMemory(subcommand, args) {
|
|
|
713
1620
|
case 'reindex':
|
|
714
1621
|
await cmdReindex(args);
|
|
715
1622
|
break;
|
|
1623
|
+
case 'search':
|
|
1624
|
+
await cmdSearch(args);
|
|
1625
|
+
break;
|
|
716
1626
|
case 'conflicts':
|
|
717
1627
|
await cmdConflicts(args);
|
|
718
1628
|
break;
|
|
@@ -740,9 +1650,14 @@ function showHelp() {
|
|
|
740
1650
|
|
|
741
1651
|
Subcommands:
|
|
742
1652
|
init Initialize memory config for this project
|
|
1653
|
+
setup Configure or reconfigure the memory vault (mode + remote)
|
|
743
1654
|
status Show current memory configuration
|
|
744
1655
|
link <path> Link to a shared memory repo (clone if URL)
|
|
745
1656
|
unlink Revert to local-only mode
|
|
1657
|
+
write Write a single note to the vault
|
|
1658
|
+
read Read a single note and print it to stdout
|
|
1659
|
+
list List notes (optionally filtered by namespace + dir)
|
|
1660
|
+
delete Delete a single note from the vault
|
|
746
1661
|
pull Git pull from the shared repo
|
|
747
1662
|
commit [-m] Git commit staged changes
|
|
748
1663
|
push Git push to the shared repo
|