@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/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
12 agents across 4 cost tiers. Odin routes, subagents execute, Forseti audits.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/bizar)
|
|
10
|
-
[](https://github.com/headroomlabs-ai/headroom)
|
|
11
11
|
[](https://github.com/semble-ai/semble)
|
|
12
12
|
[](https://www.skills.sh)
|
|
13
13
|
[](LICENSE)
|
|
@@ -201,40 +201,37 @@ bizar graph build
|
|
|
201
201
|
- The `blessed` TUI library may render with quirks inside Windows Terminal; if you see garbled output in the TUI, use the browser UI via `bizar dash start --bg` and open http://localhost:4321 instead.
|
|
202
202
|
- Some shell scripts under `config/skills/embedded-esp-idf/` are bash-only and require Git Bash (or WSL).
|
|
203
203
|
- The bash `install.sh` does not run on Windows cmd/PowerShell; use the `npm install -g` path above.
|
|
204
|
-
-
|
|
204
|
+
- Headroom ships a Python/npm installer. On Windows, install it manually: `pip install "headroom-ai[all]"` or `npm install -g headroom-ai`, then run `headroom wrap opencode`.
|
|
205
205
|
|
|
206
206
|
### Prerequisites
|
|
207
207
|
|
|
208
208
|
- [opencode CLI](https://opencode.ai) installed and on `$PATH`
|
|
209
209
|
- Provider connections (via `/connect` in opencode TUI)
|
|
210
|
-
- [
|
|
210
|
+
- [Headroom](https://github.com/headroomlabs-ai/headroom) (recommended) — CLI proxy that reduces LLM token consumption by 60-90%
|
|
211
211
|
- [Semble](https://github.com/semble-ai/semble) (recommended) — AI-powered code search (used by Mimir agent)
|
|
212
212
|
- [Skills CLI](https://www.skills.sh) (recommended) — Agent skill package manager (`npx skills add <owner/repo>`)
|
|
213
213
|
|
|
214
|
-
###
|
|
214
|
+
### Headroom Setup
|
|
215
215
|
|
|
216
|
-
[
|
|
216
|
+
[Headroom](https://github.com/headroomlabs-ai/headroom) filters and compresses command output before it reaches the LLM context. It saves ~80% on common operations like `ls`, `git status`, `cargo test`, and `git diff`.
|
|
217
217
|
|
|
218
218
|
Install:
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
|
-
#
|
|
222
|
-
|
|
221
|
+
# Python (recommended)
|
|
222
|
+
pip install "headroom-ai[all]"
|
|
223
223
|
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
# Cargo
|
|
228
|
-
cargo install --git https://github.com/rtk-ai/rtk
|
|
224
|
+
# npm
|
|
225
|
+
npm install -g headroom-ai
|
|
229
226
|
```
|
|
230
227
|
|
|
231
228
|
Enable for opencode:
|
|
232
229
|
|
|
233
230
|
```bash
|
|
234
|
-
|
|
231
|
+
headroom wrap opencode
|
|
235
232
|
```
|
|
236
233
|
|
|
237
|
-
After setup,
|
|
234
|
+
After setup, `headroom wrap opencode` injects config into `opencode.json` and starts a proxy. The LLM receives compact output, saving 60-90% on token costs. The wrapping is durable — all subsequent opencode sessions route through the headroom proxy.
|
|
238
235
|
|
|
239
236
|
### Semble Setup
|
|
240
237
|
|
package/bizar-dash/CHANGELOG.md
CHANGED
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
|
|
138
138
|
- **Mod security layer (`src/server/mod-security.mjs`).** Mods declare permissions in `mod.json`; the loader enforces them at every privileged action:
|
|
139
139
|
- **Filesystem sandboxing** — mods get scoped `readFileSync` / `writeFileSync` helpers. Paths outside `fs:read:<scope>` / `fs:write:<scope>` throw. Scope syntax: exact path, directory with implicit recursion, `/*` suffix, or `~/` home-relative.
|
|
140
|
-
|
|
140
|
+
- **Subprocess allowlist** — only whitelisted binaries (`bizar`, `opencode`, `python3`, `git`, `node`, `npm`, `pip`, `uv`, `headroom`, `jq`, `graphify`, `pipx`, `opencode-ai`) can be spawned without an explicit `process:spawn:<bin>` permission. Path-style binaries (`/bin/ls`) require explicit permission.
|
|
141
141
|
- **Integrity hash** — every mod's files are SHA-256-hashed at install time; the hash is stored under `_integrity` in `mod.json`. On every load the hash is recomputed; a mismatch triggers a critical warning and refuses to load the mod's routes until reinstalled.
|
|
142
142
|
- **Audit log** — every privileged action is logged as JSON lines to `~/.cache/bizar/logs/mod-audit.log` with timestamp, mod id, mod version, action, and details. Inspect via `GET /api/mods/audit?mod=<id>&limit=200`.
|
|
143
143
|
- **Public mod registry.** New `bizarre-mods` repo at `DrB0rk/bizarre-mods` is the canonical source of mods. `GET /api/mods/registry` fetches `registry.json` and annotates each entry with installed state and upgrade hints. `POST /api/mods { id }` installs from the registry.
|
|
@@ -59,7 +59,7 @@ import { createMiscRouter } from './routes/misc.mjs';
|
|
|
59
59
|
* @param {Function} [deps.broadcast]
|
|
60
60
|
* @returns {import('express').Router}
|
|
61
61
|
*/
|
|
62
|
-
export function createApiRouter({
|
|
62
|
+
export async function createApiRouter({
|
|
63
63
|
state,
|
|
64
64
|
watcher,
|
|
65
65
|
projectRoot,
|
|
@@ -106,7 +106,7 @@ export function createApiRouter({
|
|
|
106
106
|
router.use(createOpencodeSessionsRouter());
|
|
107
107
|
router.use(createDialogsRouter({ broadcast }));
|
|
108
108
|
router.use(createSkillsRouter({ broadcast }));
|
|
109
|
-
router.use(createObsidianRouter({ projectRoot }));
|
|
109
|
+
router.use(await createObsidianRouter({ projectRoot }));
|
|
110
110
|
// v3.24.0 — Bizar Memory Service (Phase 1). Lazy-imported so that if Thor's
|
|
111
111
|
// memory routes haven't landed yet, the dashboard still boots.
|
|
112
112
|
const { createMemoryRouter } = await import('./routes/memory.mjs');
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* edits so the editor never races the markdown regenerator.
|
|
12
12
|
*
|
|
13
13
|
* Storage locations, in order of preference:
|
|
14
|
-
* 1. <projectRoot
|
|
14
|
+
* 1. <projectRoot>/.bizar/artifacts/<slug> (worktree, project-scoped)
|
|
15
15
|
* 2. ~/.config/opencode/artifacts/<slug> (global fallback)
|
|
16
16
|
*/
|
|
17
17
|
import {
|
|
@@ -102,7 +102,7 @@ export const artifactsStore = {
|
|
|
102
102
|
*/
|
|
103
103
|
resolveDir(slug, projectRoot) {
|
|
104
104
|
const candidates = [];
|
|
105
|
-
if (projectRoot) candidates.push(join(projectRoot, 'artifacts', slug));
|
|
105
|
+
if (projectRoot) candidates.push(join(projectRoot, '.bizar', 'artifacts', slug));
|
|
106
106
|
candidates.push(join(GLOBAL_PLANS_DIR, slug));
|
|
107
107
|
for (const c of candidates) {
|
|
108
108
|
if (existsSync(c)) return c;
|
|
@@ -116,7 +116,7 @@ export const artifactsStore = {
|
|
|
116
116
|
*/
|
|
117
117
|
ensureDir(slug, projectRoot) {
|
|
118
118
|
const target = projectRoot
|
|
119
|
-
? join(projectRoot, 'artifacts', slug)
|
|
119
|
+
? join(projectRoot, '.bizar', 'artifacts', slug)
|
|
120
120
|
: join(GLOBAL_PLANS_DIR, slug);
|
|
121
121
|
mkdirSync(target, { recursive: true });
|
|
122
122
|
return target;
|
|
@@ -126,7 +126,7 @@ export const artifactsStore = {
|
|
|
126
126
|
const seen = new Set();
|
|
127
127
|
const out = [];
|
|
128
128
|
const dirs = [];
|
|
129
|
-
if (projectRoot) dirs.push(join(projectRoot, 'artifacts'));
|
|
129
|
+
if (projectRoot) dirs.push(join(projectRoot, '.bizar', 'artifacts'));
|
|
130
130
|
dirs.push(GLOBAL_PLANS_DIR);
|
|
131
131
|
for (const dir of dirs) {
|
|
132
132
|
if (!existsSync(dir)) continue;
|
|
@@ -247,3 +247,145 @@ export function addAll(dir) {
|
|
|
247
247
|
return { ok: false, error: err.message };
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Set the URL of a git remote, idempotently.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} repoDir — the git repository root
|
|
255
|
+
* @param {string} remoteName — name of the remote (e.g. 'origin')
|
|
256
|
+
* @param {string} url — the remote URL
|
|
257
|
+
* @param {{ overwrite?: boolean }} [opts]
|
|
258
|
+
* @returns {{ ok: true, action: 'added' | 'updated' | 'unchanged' }}
|
|
259
|
+
* @throws if the remote already exists with a different URL and overwrite is false,
|
|
260
|
+
* or if the underlying git command fails.
|
|
261
|
+
*/
|
|
262
|
+
export function addRemote(repoDir, remoteName, url, { overwrite = false } = {}) {
|
|
263
|
+
let existing = null;
|
|
264
|
+
try {
|
|
265
|
+
existing = execFileSync('git', ['remote', 'get-url', remoteName], {
|
|
266
|
+
cwd: repoDir,
|
|
267
|
+
encoding: 'utf8',
|
|
268
|
+
timeout: 5000,
|
|
269
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
270
|
+
}).trim();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
// exit code 2 or 128 means remote doesn't exist; anything else is a real error
|
|
273
|
+
if (err.status !== 2 && err.status !== 128) {
|
|
274
|
+
throw new Error(`failed to query remote '${remoteName}': ${err.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (existing && existing === url) {
|
|
279
|
+
return { ok: true, action: 'unchanged' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (existing && !overwrite) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`remote '${remoteName}' already exists with URL ${existing}.\n` +
|
|
285
|
+
` To update it, call addRemote(dir, name, url, { overwrite: true }).`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (existing) {
|
|
290
|
+
execFileSync('git', ['remote', 'set-url', remoteName, url], {
|
|
291
|
+
cwd: repoDir,
|
|
292
|
+
timeout: 5000,
|
|
293
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
294
|
+
});
|
|
295
|
+
return { ok: true, action: 'updated' };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
execFileSync('git', ['remote', 'add', remoteName, url], {
|
|
299
|
+
cwd: repoDir,
|
|
300
|
+
timeout: 5000,
|
|
301
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
302
|
+
});
|
|
303
|
+
return { ok: true, action: 'added' };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Test whether a git remote is reachable.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} repoDir — the git repository root
|
|
310
|
+
* @param {string} remoteName — name of the remote (e.g. 'origin')
|
|
311
|
+
* @param {{ timeoutMs?: number }} [opts]
|
|
312
|
+
* @returns {string} the trimmed stdout from `git ls-remote` on success,
|
|
313
|
+
* or '' on any failure (network, auth, timeout).
|
|
314
|
+
* Does NOT throw.
|
|
315
|
+
*/
|
|
316
|
+
export function lsRemote(repoDir, remoteName, { timeoutMs = 5000 } = {}) {
|
|
317
|
+
try {
|
|
318
|
+
const stdout = execFileSync('git', ['ls-remote', remoteName], {
|
|
319
|
+
cwd: repoDir,
|
|
320
|
+
timeout: timeoutMs,
|
|
321
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
322
|
+
encoding: 'utf8',
|
|
323
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
324
|
+
});
|
|
325
|
+
return stdout.trim();
|
|
326
|
+
} catch {
|
|
327
|
+
return '';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Ensure the current branch tracks `<remote>/<branch>`. Idempotent.
|
|
333
|
+
*
|
|
334
|
+
* Some workflows create a managed vault before `origin` exists (or before
|
|
335
|
+
* the user pushes for the first time). Without an upstream, `git pull`
|
|
336
|
+
* fails with "There is no tracking information for the current branch."
|
|
337
|
+
* This helper detects that case and sets the upstream, or returns a
|
|
338
|
+
* structured result if it can't.
|
|
339
|
+
*
|
|
340
|
+
* Behaviour:
|
|
341
|
+
* - Upstream already set to <remote>/<branch> → { ok: true, action: 'unchanged' }
|
|
342
|
+
* - Upstream missing → set it, return { ok: true, action: 'set' }
|
|
343
|
+
* - Upstream set to a DIFFERENT remote/branch → set it to the requested one,
|
|
344
|
+
* return { ok: true, action: 'set' }
|
|
345
|
+
* - <remote> does not exist → { ok: false, error: 'remote "<remote>" does not exist' }
|
|
346
|
+
* - git not installed → { ok: false, error: 'git not installed' }
|
|
347
|
+
*
|
|
348
|
+
* @param {string} cwd — git working directory
|
|
349
|
+
* @param {string} branch — local branch name (e.g. 'main')
|
|
350
|
+
* @param {string} [remote='origin'] — remote name
|
|
351
|
+
* @returns {{ ok: boolean, action?: 'unchanged'|'set', error?: string }}
|
|
352
|
+
*/
|
|
353
|
+
export function ensureUpstream(cwd, branch, remote = 'origin') {
|
|
354
|
+
if (!isGitInstalled()) return { ok: false, error: 'git not installed' };
|
|
355
|
+
|
|
356
|
+
// Current upstream — empty string means none configured.
|
|
357
|
+
let current = '';
|
|
358
|
+
try {
|
|
359
|
+
current = execFileSync('git', ['rev-parse', '--abbrev-ref', '@{upstream}'], {
|
|
360
|
+
cwd,
|
|
361
|
+
encoding: 'utf8',
|
|
362
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
363
|
+
}).trim();
|
|
364
|
+
} catch {
|
|
365
|
+
// No upstream set — fall through.
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (current && current === `${remote}/${branch}`) {
|
|
369
|
+
return { ok: true, action: 'unchanged' };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Verify the remote exists before trying to set upstream to it.
|
|
373
|
+
try {
|
|
374
|
+
execFileSync('git', ['rev-parse', '--verify', remote], {
|
|
375
|
+
cwd,
|
|
376
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
377
|
+
});
|
|
378
|
+
} catch {
|
|
379
|
+
return { ok: false, error: `remote '${remote}' does not exist` };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
execFileSync('git', ['branch', '--set-upstream-to', `${remote}/${branch}`], {
|
|
384
|
+
cwd,
|
|
385
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
386
|
+
});
|
|
387
|
+
return { ok: true, action: 'set' };
|
|
388
|
+
} catch (err) {
|
|
389
|
+
return { ok: false, error: err.message };
|
|
390
|
+
}
|
|
391
|
+
}
|