@polderlabs/bizar 5.4.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
- package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
- package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
- package/bizar-dash/dist/index.html +8 -8
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/backup-store.mjs +6 -1
- package/bizar-dash/src/server/bg-spawner.mjs +574 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
- package/bizar-dash/src/server/memory-store.mjs +43 -3
- package/bizar-dash/src/server/routes/background.mjs +143 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
- package/bizar-dash/src/server/routes/memory.mjs +8 -0
- package/bizar-dash/src/server/server.mjs +22 -0
- package/bizar-dash/src/web/App.tsx +3 -0
- package/bizar-dash/src/web/MobileApp.tsx +1 -10
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
- package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
- package/bizar-dash/src/web/lib/types.ts +39 -1
- package/bizar-dash/src/web/styles/main.css +43 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
- package/bizar-dash/src/web/styles/settings.css +46 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
- package/bizar-dash/src/web/views/Settings.tsx +12 -12
- package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
- package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
- package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
- package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
- package/bizar-dash/tests/background-steer.test.mjs +42 -0
- package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
- package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
- package/bizar-dash/tests/memory-cli.test.mjs +1 -1
- package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +36 -0
- package/bizar-dash/tests/settings-layout.test.tsx +3 -3
- package/bizar-dash/tests/settings-nav.test.tsx +7 -5
- package/cli/bin.mjs +16 -0
- package/cli/commands/deploy/docker.mjs +2 -1
- package/cli/commands/lightrag.mjs +171 -0
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/util.mjs +8 -1
- package/cli/init.mjs +1 -1
- package/cli/post-install-smoke.mjs +230 -0
- package/cli/provision.mjs +177 -0
- package/cli/provision.test.mjs +180 -0
- package/cli/service-controller.mjs +378 -28
- package/cli/service-controller.test.mjs +100 -0
- package/cli/service-env.mjs +139 -0
- package/cli/service.mjs +23 -0
- package/config/agents/_shared/AGENT_BASELINE.md +6 -6
- package/install.sh +87 -6
- package/package.json +1 -1
- package/plugins/bizar/index.ts +147 -0
- package/plugins/bizar/src/background-state.ts +84 -5
- package/plugins/bizar/src/background.ts +388 -10
- package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
- package/plugins/bizar/src/opencode-runner.ts +87 -1
- package/plugins/bizar/src/tools/bg-pause.ts +77 -0
- package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
- package/plugins/bizar/src/tools/bg-resume.ts +72 -0
- package/plugins/bizar/src/tools/bg-send-message.ts +105 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
- package/plugins/bizar/src/tools/bg-status.ts +10 -0
- package/plugins/bizar/src/tools/memory-list.ts +135 -0
- package/plugins/bizar/src/tools/memory-read.ts +142 -0
- package/plugins/bizar/src/tools/memory-search.ts +228 -0
- package/plugins/bizar/src/tools/memory-write.ts +183 -0
- package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
- package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
- package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +46 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
- package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
- package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
|
@@ -293,4 +293,40 @@ describe('MobileLayout', () => {
|
|
|
293
293
|
);
|
|
294
294
|
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
295
295
|
});
|
|
296
|
+
|
|
297
|
+
it('layout has correct CSS class applied', () => {
|
|
298
|
+
const { container } = render(
|
|
299
|
+
<MobileLayout
|
|
300
|
+
activeTab="activity"
|
|
301
|
+
onTabChange={vi.fn()}
|
|
302
|
+
tabs={TABS}
|
|
303
|
+
fullTabs={TABS}
|
|
304
|
+
>
|
|
305
|
+
<div>Child content</div>
|
|
306
|
+
</MobileLayout>,
|
|
307
|
+
);
|
|
308
|
+
// Verify the layout and content elements exist with correct class names
|
|
309
|
+
const layout = container.querySelector('.mobile-layout');
|
|
310
|
+
expect(layout).toBeInTheDocument();
|
|
311
|
+
const main = container.querySelector('.mobile-content');
|
|
312
|
+
expect(main).toBeInTheDocument();
|
|
313
|
+
const header = container.querySelector('.mobile-header');
|
|
314
|
+
expect(header).toBeInTheDocument();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('renders exactly one header and one main inside the layout', () => {
|
|
318
|
+
const { container } = render(
|
|
319
|
+
<MobileLayout
|
|
320
|
+
activeTab="activity"
|
|
321
|
+
onTabChange={vi.fn()}
|
|
322
|
+
tabs={TABS}
|
|
323
|
+
fullTabs={TABS}
|
|
324
|
+
>
|
|
325
|
+
<div>Child content</div>
|
|
326
|
+
</MobileLayout>,
|
|
327
|
+
);
|
|
328
|
+
const layout = container.querySelector('.mobile-layout');
|
|
329
|
+
expect(layout!.querySelectorAll('header').length).toBe(1);
|
|
330
|
+
expect(layout!.querySelectorAll('main').length).toBe(1);
|
|
331
|
+
});
|
|
296
332
|
});
|
|
@@ -57,7 +57,7 @@ describe('Sidebar — settingsMode', () => {
|
|
|
57
57
|
// Should NOT show the normal tab rail
|
|
58
58
|
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
|
|
59
59
|
// Should show the back button
|
|
60
|
-
expect(screen.getByRole('button', { name: /
|
|
60
|
+
expect(screen.getByRole('button', { name: /exit settings/i })).toBeInTheDocument();
|
|
61
61
|
// Should show section groups (use selector to avoid duplicate text matches)
|
|
62
62
|
expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
63
63
|
expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
@@ -109,8 +109,8 @@ describe('Sidebar — settingsMode', () => {
|
|
|
109
109
|
onExitSettings={onExitSettings}
|
|
110
110
|
/>,
|
|
111
111
|
);
|
|
112
|
-
await user.click(screen.getByRole('button', { name: /
|
|
113
|
-
expect(onSettingsSectionChange).toHaveBeenCalledWith('
|
|
112
|
+
await user.click(screen.getByRole('button', { name: /env vars/i }));
|
|
113
|
+
expect(onSettingsSectionChange).toHaveBeenCalledWith('env-vars');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
it('calls onTabChange when a normal sidebar tab is clicked', async () => {
|
|
@@ -25,7 +25,7 @@ describe('SettingsNav', () => {
|
|
|
25
25
|
onExitSettings={onExitSettings}
|
|
26
26
|
/>,
|
|
27
27
|
);
|
|
28
|
-
expect(screen.getByRole('button', { name: /
|
|
28
|
+
expect(screen.getByRole('button', { name: /exit settings/i })).toBeInTheDocument();
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it('calls onExitSettings when back button is clicked', async () => {
|
|
@@ -52,7 +52,7 @@ describe('SettingsNav', () => {
|
|
|
52
52
|
expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
53
53
|
expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
54
54
|
expect(screen.getByText('Experience', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
|
|
55
|
-
|
|
55
|
+
// Data group was removed - no longer exists in SettingsNav
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
it('renders section items within each group', () => {
|
|
@@ -63,14 +63,16 @@ describe('SettingsNav', () => {
|
|
|
63
63
|
onExitSettings={onExitSettings}
|
|
64
64
|
/>,
|
|
65
65
|
);
|
|
66
|
+
// General group
|
|
66
67
|
expect(screen.getByRole('button', { name: /theme/i })).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByRole('button', { name: /layout/i })).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByRole('button', { name: /general/i })).toBeInTheDocument();
|
|
70
|
+
// Core group
|
|
67
71
|
expect(screen.getByRole('button', { name: /env vars/i })).toBeInTheDocument();
|
|
68
|
-
expect(screen.getByRole('button', { name: /memory/i })).toBeInTheDocument();
|
|
69
72
|
expect(screen.getByRole('button', { name: /system llm/i })).toBeInTheDocument();
|
|
73
|
+
// Experience group
|
|
70
74
|
expect(screen.getByRole('button', { name: /updates/i })).toBeInTheDocument();
|
|
71
|
-
expect(screen.getByRole('button', { name: /skills/i })).toBeInTheDocument();
|
|
72
75
|
expect(screen.getByRole('button', { name: /headroom/i })).toBeInTheDocument();
|
|
73
|
-
expect(screen.getByRole('button', { name: /backup/i })).toBeInTheDocument();
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
it('calls onSectionChange with correct id when a section is clicked', async () => {
|
package/cli/bin.mjs
CHANGED
|
@@ -101,6 +101,7 @@ function showHelp() {
|
|
|
101
101
|
dash <subcommand> Manage the dashboard (start/stop/status/cleanup/tui)
|
|
102
102
|
memory <subcommand> Manage project memory (Bizar Memory Service)
|
|
103
103
|
headroom <subcommand> Manage Headroom context compression
|
|
104
|
+
lightrag <subcommand> Manage the LightRAG knowledge-graph server (status/start/autostart)
|
|
104
105
|
minimax <subcommand> Manage MiniMax Token Plan integration
|
|
105
106
|
tailscale <subcommand> Manage Tailscale integration (auth, serve, status)
|
|
106
107
|
mod <subcommand> Manage mods (install/upgrade/list via the dashboard API)
|
|
@@ -284,6 +285,21 @@ async function main() {
|
|
|
284
285
|
break;
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
case 'lightrag': {
|
|
289
|
+
// v5.x — LightRAG management CLI (issue #6).
|
|
290
|
+
// Subcommands: status, start, autostart.
|
|
291
|
+
const mod = await importCommand('lightrag');
|
|
292
|
+
if (!mod) {
|
|
293
|
+
console.error(chalk.red(` ✗ Could not load lightrag command module`));
|
|
294
|
+
process.exit(EXIT_ERROR);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
dbg('loaded command module:', 'lightrag');
|
|
298
|
+
await mod.run(cmd, cmdArgs, isHelpRequest);
|
|
299
|
+
dbg('command returned:', cmd);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
287
303
|
case 'mod': {
|
|
288
304
|
const mod = await importCommand('mod');
|
|
289
305
|
if (!mod) {
|
|
@@ -46,7 +46,8 @@ function generateComposeScaffold(outDir, opts) {
|
|
|
46
46
|
' - .env',
|
|
47
47
|
' volumes:',
|
|
48
48
|
' - bizar-config:/home/bizar/.config/bizar',
|
|
49
|
-
|
|
49
|
+
// v5.x — default memory vault relocated to ~/.bizar_memory
|
|
50
|
+
' - bizar-memory:/home/bizar/.bizar_memory',
|
|
50
51
|
'',
|
|
51
52
|
'volumes:',
|
|
52
53
|
' bizar-config:',
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/lightrag.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.x — LightRAG management CLI (issue #6).
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* autostart Trigger the dashboard's lightragStartupHook() against
|
|
8
|
+
* the current working directory's project. Used by
|
|
9
|
+
* `install.sh` and by operators who want to manually
|
|
10
|
+
* start LightRAG without opening the dashboard.
|
|
11
|
+
* start Alias for `autostart` (matches `headroom start`).
|
|
12
|
+
* status Print whether the LightRAG server is running.
|
|
13
|
+
*
|
|
14
|
+
* The dashboard server already auto-invokes `lightragStartupHook()` on
|
|
15
|
+
* boot, so this CLI is mostly for ops/debugging. The `autostart` form is
|
|
16
|
+
* the one called by `install.sh` and `bizar update` flows.
|
|
17
|
+
*/
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { process } from 'node:process';
|
|
22
|
+
|
|
23
|
+
export function showLightragHelp() {
|
|
24
|
+
console.log(`
|
|
25
|
+
bizar lightrag — Manage the LightRAG knowledge graph server
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
bizar lightrag status Show whether LightRAG is running
|
|
29
|
+
bizar lightrag start Start LightRAG for the current project
|
|
30
|
+
bizar lightrag autostart Same as \`start\` — runs the startup hook
|
|
31
|
+
bizar lightrag autostart --no-dashboard Run hook without touching the
|
|
32
|
+
running dashboard (skips status check)
|
|
33
|
+
|
|
34
|
+
Description:
|
|
35
|
+
LightRAG is the knowledge-graph indexer for the project's memory vault.
|
|
36
|
+
The dashboard server auto-starts LightRAG on boot (when
|
|
37
|
+
.bizar/memory.json#lightrag.enabled is true). Use this command to:
|
|
38
|
+
- manually trigger the startup hook against the current project
|
|
39
|
+
- check whether the LightRAG server is up
|
|
40
|
+
|
|
41
|
+
Environment:
|
|
42
|
+
BIZAR_LIGHTRAG_AUTOSTART=0|false|no Disable auto-start (default: enabled)
|
|
43
|
+
BIZAR_MEMORY_VAULT=/path/to/vault Override the memory vault location
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the project root for the lightrag command. We use `process.cwd()`
|
|
49
|
+
* — the user is expected to run this from inside a Bizar project. If
|
|
50
|
+
* `.bizar/memory.json` is missing, we still pass the cwd through and let
|
|
51
|
+
* the hook's `resolveLightRAGConfig` fall back to defaults.
|
|
52
|
+
*/
|
|
53
|
+
function resolveProjectRoot() {
|
|
54
|
+
return process.cwd();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runLightragCommand(lightragArgs) {
|
|
58
|
+
const sub = lightragArgs[0];
|
|
59
|
+
const flags = new Set(lightragArgs.slice(1).filter((a) => a.startsWith('-')));
|
|
60
|
+
|
|
61
|
+
if (!sub || sub === '--help' || sub === '-h' || flags.has('--help') || flags.has('-h')) {
|
|
62
|
+
showLightragHelp();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (sub === 'autostart' || sub === 'start') {
|
|
67
|
+
// Lazy import so the help text doesn't pay the cost of pulling in
|
|
68
|
+
// memory-lightrag (and its transitive deps) on every command.
|
|
69
|
+
const { lightragStartupHook, isRunning, resolveLightRAGConfig } = await import('../../bizar-dash/src/server/memory-lightrag.mjs');
|
|
70
|
+
const projectRoot = resolveProjectRoot();
|
|
71
|
+
console.log(chalk.dim(` lightrag autostart: project=${projectRoot}`));
|
|
72
|
+
|
|
73
|
+
// If the dashboard is running, prefer its endpoint (it owns the
|
|
74
|
+
// server's lifecycle). Fall back to a direct hook call otherwise.
|
|
75
|
+
if (!flags.has('--no-dashboard')) {
|
|
76
|
+
try {
|
|
77
|
+
const dashboardResult = await callDashboardAutostart();
|
|
78
|
+
if (dashboardResult && dashboardResult.started) {
|
|
79
|
+
console.log(chalk.green(` ✓ lightrag auto-started via dashboard (pid=${dashboardResult.pid || '?'})`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (dashboardResult && dashboardResult.ok && !dashboardResult.started) {
|
|
83
|
+
console.log(chalk.dim(` · lightrag: ${dashboardResult.reason || 'already running'}`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Dashboard unreachable — fall through to direct call. Common
|
|
88
|
+
// case: user ran `bizar lightrag autostart` from a fresh shell
|
|
89
|
+
// without the dashboard running.
|
|
90
|
+
console.log(chalk.dim(` · dashboard not reachable, running hook locally: ${err?.message || err}`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const r = await lightragStartupHook(projectRoot);
|
|
95
|
+
if (r.ok && r.started) {
|
|
96
|
+
console.log(chalk.green(` ✓ lightrag started (pid=${r.pid || '?'})`));
|
|
97
|
+
} else if (r.ok) {
|
|
98
|
+
console.log(chalk.dim(` · lightrag: ${r.reason || 'not started'}`));
|
|
99
|
+
} else {
|
|
100
|
+
console.log(chalk.yellow(` ! lightrag autostart: ${r.error || r.reason || 'failed'}`));
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (sub === 'status') {
|
|
107
|
+
const { isRunning, resolveLightRAGConfig } = await import('../../bizar-dash/src/server/memory-lightrag.mjs');
|
|
108
|
+
const projectRoot = resolveProjectRoot();
|
|
109
|
+
const cfg = resolveLightRAGConfig(projectRoot);
|
|
110
|
+
const running = await isRunning(cfg);
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.bold(' LightRAG status'));
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(` ${chalk.dim('running:')} ${running ? chalk.green('yes') : chalk.yellow('no')}`);
|
|
115
|
+
console.log(` ${chalk.dim('host:')} ${cfg.host}:${cfg.port}`);
|
|
116
|
+
console.log(` ${chalk.dim('enabled:')} ${cfg.enabled ? chalk.green('yes') : chalk.yellow('no')}`);
|
|
117
|
+
console.log(` ${chalk.dim('workingDir:')} ${cfg.workingDir}`);
|
|
118
|
+
console.log(` ${chalk.dim('llmBinding:')} ${cfg.llmBinding} (${cfg.llmModel})`);
|
|
119
|
+
console.log(` ${chalk.dim('embedding:')} ${cfg.embeddingBinding} (${cfg.embeddingModel})`);
|
|
120
|
+
console.log('');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.error(chalk.red(` ✗ Unknown lightrag subcommand: ${sub}`));
|
|
125
|
+
showLightragHelp();
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Call the dashboard's startup-hook endpoint if the dashboard is up.
|
|
131
|
+
* Returns the JSON body on success, null if the dashboard is unreachable
|
|
132
|
+
* (so the caller can fall back to a direct hook call).
|
|
133
|
+
*/
|
|
134
|
+
async function callDashboardAutostart() {
|
|
135
|
+
// Read dashboard connection from the same files the other CLI commands use.
|
|
136
|
+
const HOME = process.env.HOME || '/';
|
|
137
|
+
const portFile = join(HOME, '.config', 'bizar', 'dashboard.port');
|
|
138
|
+
const authFile = join(HOME, '.config', 'bizar', 'dash-auth.json');
|
|
139
|
+
let port = 4321;
|
|
140
|
+
let secret = '';
|
|
141
|
+
try {
|
|
142
|
+
if (existsSync(portFile)) {
|
|
143
|
+
const parsed = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
|
|
144
|
+
if (Number.isFinite(parsed) && parsed > 0) port = parsed;
|
|
145
|
+
}
|
|
146
|
+
} catch { /* ignore */ }
|
|
147
|
+
try {
|
|
148
|
+
if (existsSync(authFile)) {
|
|
149
|
+
const raw = JSON.parse(readFileSync(authFile, 'utf8'));
|
|
150
|
+
secret = raw?.password || '';
|
|
151
|
+
}
|
|
152
|
+
} catch { /* ignore */ }
|
|
153
|
+
const url = `http://127.0.0.1:${port}/api/lightrag/autostart`;
|
|
154
|
+
const headers = { 'content-type': 'application/json', accept: 'application/json' };
|
|
155
|
+
if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
|
|
156
|
+
const res = await fetch(url, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers,
|
|
159
|
+
body: JSON.stringify({}),
|
|
160
|
+
signal: AbortSignal.timeout(2000),
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const text = await res.text();
|
|
166
|
+
try { return text ? JSON.parse(text) : null; } catch { return null; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function run(name, args, isHelpRequest) {
|
|
170
|
+
await runLightragCommand(args);
|
|
171
|
+
}
|
package/cli/commands/service.mjs
CHANGED
|
@@ -35,6 +35,8 @@ export function showServiceHelp() {
|
|
|
35
35
|
bizar service follow Follow the service log until Ctrl-C
|
|
36
36
|
bizar service install Register with systemd / launchd / scheduled task
|
|
37
37
|
bizar service install --force Re-install even when the unit matches
|
|
38
|
+
bizar service restart Stop, reinstall unit, start (used by update)
|
|
39
|
+
bizar service restart --force Reinstall the unit before starting
|
|
38
40
|
bizar service uninstall Remove the OS-level autostart
|
|
39
41
|
bizar service uninstall --force Force-uninstall even when nothing is registered
|
|
40
42
|
|
|
@@ -50,6 +52,11 @@ export function showServiceHelp() {
|
|
|
50
52
|
install, a normal user does not need to run \`bizar service start\`
|
|
51
53
|
for the dashboard background process — the OS does it at login.
|
|
52
54
|
|
|
55
|
+
restart stops the running daemon, reinstalls the unit (so the
|
|
56
|
+
freshly-installed code is loaded), and starts it again. This is
|
|
57
|
+
what \`bizar update\` uses to pick up the new binary without leaving
|
|
58
|
+
the old one running.
|
|
59
|
+
|
|
53
60
|
Tailscale Integration (v5.2):
|
|
54
61
|
TAILSCALE_AUTHKEY=${tsAuthkey}
|
|
55
62
|
BIZAR_TAILSCALE_AUTOSETUP=${tsAuto}
|
package/cli/commands/util.mjs
CHANGED
|
@@ -265,8 +265,15 @@ export async function run(name, args, isHelpRequest) {
|
|
|
265
265
|
case 'doctor':
|
|
266
266
|
if (isHelpRequest) showDoctorHelp();
|
|
267
267
|
else {
|
|
268
|
-
const { runDoctor } = await import('../doctor.mjs');
|
|
269
268
|
const wantJson = args.includes('--json');
|
|
269
|
+
if (args[0] === 'smoke') {
|
|
270
|
+
// `bizar doctor smoke` — run post-install smoke test
|
|
271
|
+
const { runSmokeTest } = await import('../post-install-smoke.mjs');
|
|
272
|
+
const result = await runSmokeTest();
|
|
273
|
+
if (wantJson) process.stdout.write(JSON.stringify(result) + '\n');
|
|
274
|
+
process.exit(result.ok ? 0 : 1);
|
|
275
|
+
}
|
|
276
|
+
const { runDoctor } = await import('../doctor.mjs');
|
|
270
277
|
const result = await runDoctor({ silent: wantJson, json: wantJson });
|
|
271
278
|
if (wantJson) process.stdout.write(JSON.stringify(result) + '\n');
|
|
272
279
|
if (result.failed > 0) process.exit(1);
|
package/cli/init.mjs
CHANGED
|
@@ -260,7 +260,7 @@ ${stack.runner ? `- Dev: \`${stack.runner}\`` : ''}
|
|
|
260
260
|
name: 'memoryMode',
|
|
261
261
|
message: 'Memory backend',
|
|
262
262
|
choices: [
|
|
263
|
-
{ name: 'managed (shared user-level repo at ~/.
|
|
263
|
+
{ name: 'managed (shared user-level repo at ~/.bizar_memory/bizar-memory/)', value: 'managed' },
|
|
264
264
|
{ name: 'local-only (vault stays in this project at .obsidian/)', value: 'local-only' },
|
|
265
265
|
],
|
|
266
266
|
default: 'managed',
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/post-install-smoke.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.x — Post-install smoke test for BizarHarness.
|
|
5
|
+
*
|
|
6
|
+
* Verifies the installation is functional by checking:
|
|
7
|
+
* 1. Memory vault exists and is git-initialized
|
|
8
|
+
* 2. `bizar doctor` exits 0
|
|
9
|
+
* 3. Dashboard HTTP responds 200
|
|
10
|
+
* 4. WebSocket connects to dashboard
|
|
11
|
+
* 5. `bizar bg list` returns (even if empty)
|
|
12
|
+
* 6. lightrag-server --version works
|
|
13
|
+
*
|
|
14
|
+
* Each check has a 30s timeout. Exits 0 if all pass, 1 otherwise.
|
|
15
|
+
*
|
|
16
|
+
* Public API:
|
|
17
|
+
* runSmokeTest({ bizarHome, repoPath })
|
|
18
|
+
* → Promise<{ ok: boolean, checks: CheckResult[] }>
|
|
19
|
+
*
|
|
20
|
+
* CheckResult: { name: string, ok: boolean, message: string }
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { execSync } from 'node:child_process';
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
30
|
+
const HOME = homedir();
|
|
31
|
+
const DEFAULT_BIZAR_HOME = join(HOME, '.config', 'bizar');
|
|
32
|
+
const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
|
|
33
|
+
const DEFAULT_DASHBOARD_PORT = 4097;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} opts
|
|
37
|
+
* @param {string} [opts.bizarHome]
|
|
38
|
+
* @param {string} [opts.repoPath]
|
|
39
|
+
* @param {number} [opts.timeoutMs]
|
|
40
|
+
*/
|
|
41
|
+
export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
42
|
+
const bh = bizarHome || DEFAULT_BIZAR_HOME;
|
|
43
|
+
const mv = process.env.BIZAR_MEMORY_VAULT || DEFAULT_MEMORY_VAULT;
|
|
44
|
+
const port = parseInt(process.env.BIZAR_DASHBOARD_PORT || String(DEFAULT_DASHBOARD_PORT), 10) || DEFAULT_DASHBOARD_PORT;
|
|
45
|
+
const checks = [];
|
|
46
|
+
|
|
47
|
+
function check(name, fn) {
|
|
48
|
+
try {
|
|
49
|
+
const result = fn();
|
|
50
|
+
checks.push({ name, ok: result.ok, message: result.message || '' });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
checks.push({ name, ok: false, message: err instanceof Error ? err.message : String(err) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── 1. Memory vault exists and is git-initialised ───────────────────────
|
|
57
|
+
check('memory vault exists', () => {
|
|
58
|
+
if (!existsSync(mv)) {
|
|
59
|
+
return { ok: false, message: `${mv} does not exist` };
|
|
60
|
+
}
|
|
61
|
+
const gitDir = join(mv, '.git');
|
|
62
|
+
if (!existsSync(gitDir)) {
|
|
63
|
+
return { ok: false, message: `${mv} is not git-initialized (run: git init)` };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, message: `${mv} present and git-initialized` };
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── 2. `bizar doctor` exits 0 ────────────────────────────────────────────
|
|
69
|
+
check('bizar doctor', () => {
|
|
70
|
+
try {
|
|
71
|
+
execSync('bizar doctor', {
|
|
72
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
|
+
timeout: timeoutMs,
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
});
|
|
76
|
+
return { ok: true, message: 'bizar doctor passed' };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const status = err.status;
|
|
79
|
+
const msg = err.stderr || err.message;
|
|
80
|
+
if (status === 0) return { ok: true, message: 'bizar doctor passed' };
|
|
81
|
+
return { ok: false, message: `bizar doctor failed (exit ${status}): ${msg}`.slice(0, 200) };
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── 3. Dashboard HTTP responds 200 ─────────────────────────────────────────
|
|
86
|
+
check('dashboard HTTP', async () => {
|
|
87
|
+
try {
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`http://127.0.0.1:${port}/`, {
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
method: 'GET',
|
|
94
|
+
});
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
if (res.ok) return { ok: true, message: `HTTP ${res.status} on port ${port}` };
|
|
97
|
+
return { ok: false, message: `HTTP ${res.status} on port ${port} (expected 200)` };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
if (err.name === 'AbortError') {
|
|
101
|
+
return { ok: false, message: `dashboard not responding on port ${port} (timeout)` };
|
|
102
|
+
}
|
|
103
|
+
return { ok: false, message: `dashboard unreachable on port ${port}: ${err.message}` };
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return { ok: false, message: `fetch not available: ${err.message}` };
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── 4. WebSocket connects ──────────────────────────────────────────────────
|
|
111
|
+
check('dashboard WebSocket', async () => {
|
|
112
|
+
try {
|
|
113
|
+
const { WebSocket } = await import('ws' in globalThis
|
|
114
|
+
? { ws: globalThis.ws, WebSocket: globalThis.WebSocket }
|
|
115
|
+
: await import(`ws`).then(m => ({ ws: m, WebSocket: m.WebSocket || m.default }))
|
|
116
|
+
);
|
|
117
|
+
const url = `ws://127.0.0.1:${port}/ws`;
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
120
|
+
try {
|
|
121
|
+
const ws = new WebSocket(url);
|
|
122
|
+
await new Promise((resolve, reject) => {
|
|
123
|
+
ws.on('open', resolve);
|
|
124
|
+
ws.on('error', reject);
|
|
125
|
+
ws.on('close', () => reject(new Error('closed before open')));
|
|
126
|
+
controller.signal.addEventListener('abort', () => {
|
|
127
|
+
ws.close();
|
|
128
|
+
reject(new Error('timeout'));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
ws.close();
|
|
133
|
+
return { ok: true, message: `WebSocket connected at ${url}` };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
if (err.message === 'timeout') {
|
|
137
|
+
return { ok: false, message: `WebSocket timeout on ${url}` };
|
|
138
|
+
}
|
|
139
|
+
return { ok: false, message: `WebSocket failed on ${url}: ${err.message}` };
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// ws module not available in this environment — skip
|
|
143
|
+
if (err.code === 'MODULE_NOT_FOUND' || err.message?.includes('ws')) {
|
|
144
|
+
return { ok: true, message: 'ws module not available — skipping WebSocket check' };
|
|
145
|
+
}
|
|
146
|
+
return { ok: false, message: `WebSocket check failed: ${err.message}` };
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── 5. `bizar bg list` returns ────────────────────────────────────────────
|
|
151
|
+
check('background agents', () => {
|
|
152
|
+
try {
|
|
153
|
+
execSync('bizar bg list', {
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
timeout: timeoutMs,
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
});
|
|
158
|
+
return { ok: true, message: 'bizar bg list succeeded' };
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return { ok: false, message: `bizar bg list failed: ${(err.stderr || err.message || '').slice(0, 200)}` };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── 6. lightrag-server --version ──────────────────────────────────────────
|
|
165
|
+
check('lightrag-server', () => {
|
|
166
|
+
const candidates = ['lightrag-server'];
|
|
167
|
+
if (process.env.HOME) {
|
|
168
|
+
candidates.push(join(process.env.HOME, '.local', 'bin', 'lightrag-server'));
|
|
169
|
+
}
|
|
170
|
+
let found = false;
|
|
171
|
+
for (const cmd of candidates) {
|
|
172
|
+
try {
|
|
173
|
+
const out = execFileSync(cmd, ['--version'], {
|
|
174
|
+
encoding: 'utf8',
|
|
175
|
+
timeout: 5000,
|
|
176
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
177
|
+
});
|
|
178
|
+
found = true;
|
|
179
|
+
return { ok: true, message: `${cmd} responds: ${(out || '').trim().slice(0, 100)}` };
|
|
180
|
+
} catch { /* try next */ }
|
|
181
|
+
}
|
|
182
|
+
return { ok: false, message: 'lightrag-server not found in PATH or ~/.local/bin/' };
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
186
|
+
const passed = checks.filter(c => c.ok).length;
|
|
187
|
+
const failed = checks.filter(c => !c.ok).length;
|
|
188
|
+
const allOk = failed === 0;
|
|
189
|
+
|
|
190
|
+
for (const c of checks) {
|
|
191
|
+
const marker = c.ok ? '✓' : '✗';
|
|
192
|
+
const msg = c.message ? ` ${marker} ${c.name}: ${c.message}` : ` ${marker} ${c.name}`;
|
|
193
|
+
if (c.ok) {
|
|
194
|
+
console.log(`\x1b[32m${msg}\x1b[0m`);
|
|
195
|
+
} else {
|
|
196
|
+
console.log(`\x1b[31m${msg}\x1b[0m`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`\nSmoke test: ${passed} passed, ${failed} failed`);
|
|
201
|
+
return { ok: allOk, checks, passed, failed };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── CLI entry ─────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
const isMain = Boolean(process.argv[1]) && process.argv[1].endsWith('post-install-smoke.mjs');
|
|
207
|
+
if (isMain) {
|
|
208
|
+
const args = process.argv.slice(2);
|
|
209
|
+
const dryRun = args.includes('--dry-run');
|
|
210
|
+
const json = args.includes('--json');
|
|
211
|
+
|
|
212
|
+
if (dryRun) {
|
|
213
|
+
console.log('[dry-run] would run smoke test');
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
runSmokeTest().then(result => {
|
|
218
|
+
if (json) {
|
|
219
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
220
|
+
}
|
|
221
|
+
process.exit(result.ok ? 0 : 1);
|
|
222
|
+
}).catch(err => {
|
|
223
|
+
if (json) {
|
|
224
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
225
|
+
} else {
|
|
226
|
+
console.error(`Smoke test error: ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
}
|