diff --git a/extensions/boltbot/dashboard/src/App.tsx b/extensions/boltbot/dashboard/src/App.tsx
index 4bbbfd627..4fbfec23b 100644
--- a/extensions/boltbot/dashboard/src/App.tsx
+++ b/extensions/boltbot/dashboard/src/App.tsx
@@ -169,6 +169,8 @@ export default function App() {
)}
diff --git a/extensions/boltbot/dashboard/src/components/OnboardingCard.tsx b/extensions/boltbot/dashboard/src/components/OnboardingCard.tsx
new file mode 100644
index 000000000..aecf94b48
--- /dev/null
+++ b/extensions/boltbot/dashboard/src/components/OnboardingCard.tsx
@@ -0,0 +1,35 @@
+import { CheckCircle, Loader2, XCircle } from "lucide-react";
+
+interface Props {
+ isLoading: boolean;
+ error: unknown;
+}
+
+export default function OnboardingCard({ isLoading, error }: Props) {
+ if (isLoading) {
+ return (
+
+
+
Connecting to Boltbot...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
Connection failed
+
Make sure the gateway is running
+
+ );
+ }
+
+ return (
+
+
+
Boltbot is running
+
Waiting for agent activity โ send a message to your agent through any channel
+
+ );
+}
diff --git a/extensions/boltbot/dashboard/src/components/ReceiptList.tsx b/extensions/boltbot/dashboard/src/components/ReceiptList.tsx
index d371d7d18..eda98fe51 100644
--- a/extensions/boltbot/dashboard/src/components/ReceiptList.tsx
+++ b/extensions/boltbot/dashboard/src/components/ReceiptList.tsx
@@ -1,6 +1,7 @@
import { CheckCircle, XCircle, AlertTriangle } from "lucide-react";
import type { ActionReceipt } from "../types";
import { cn, formatRelativeTime } from "../utils";
+import OnboardingCard from "./OnboardingCard";
interface Props {
receipts: ActionReceipt[];
@@ -29,30 +30,8 @@ export default function ReceiptList({
onLoadMore,
loadingMore = false,
}: Props) {
- if (error) {
- return (
-
- Failed to load receipts: {error instanceof Error ? error.message : "Unknown error"}
-
- );
- }
-
- if (isLoading && receipts.length === 0) {
- return (
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
- );
- }
-
if (receipts.length === 0) {
- return (
-
- No actions recorded yet. Receipts appear here when your agent uses tools.
-
- );
+ return ;
}
return (
diff --git a/extensions/boltbot/dashboard/src/components/SessionView.tsx b/extensions/boltbot/dashboard/src/components/SessionView.tsx
index a8942783d..d70cde423 100644
--- a/extensions/boltbot/dashboard/src/components/SessionView.tsx
+++ b/extensions/boltbot/dashboard/src/components/SessionView.tsx
@@ -2,10 +2,13 @@ import { useMemo, useState } from "react";
import { ChevronDown, CheckCircle, XCircle, AlertTriangle } from "lucide-react";
import type { ActionReceipt } from "../types";
import { cn, formatRelativeTime } from "../utils";
+import OnboardingCard from "./OnboardingCard";
interface Props {
receipts: ActionReceipt[];
onSelectReceipt: (r: ActionReceipt) => void;
+ isLoading: boolean;
+ error: unknown;
}
const tierBadge: Record = {
@@ -156,15 +159,11 @@ function SessionCard({
);
}
-export default function SessionView({ receipts, onSelectReceipt }: Props) {
+export default function SessionView({ receipts, onSelectReceipt, isLoading, error }: Props) {
const groups = useMemo(() => groupBySession(receipts), [receipts]);
if (groups.length === 0) {
- return (
-
- No sessions recorded yet. Sessions appear here when your agent processes conversations.
-
- );
+ return ;
}
return (
diff --git a/extensions/boltbot/index.ts b/extensions/boltbot/index.ts
index f2d3badfd..a7c17710d 100644
--- a/extensions/boltbot/index.ts
+++ b/extensions/boltbot/index.ts
@@ -5,6 +5,8 @@ import { createReceiptStore } from "./src/receipt-store.js";
import { registerBoltbotApi } from "./src/api.js";
import { registerDashboardRoutes } from "./src/dashboard-serve.js";
+const STATS_TIMEOUT_MS = 5000;
+
export default {
id: "boltbot",
name: "Boltbot โ Audit Dashboard",
@@ -18,5 +20,31 @@ export default {
registerBoltbotApi(api, store);
registerDashboardRoutes(api);
+
+ const dashboardUrl = process.env.BOLTBOT_DASHBOARD_URL || "/boltbot/dashboard/";
+
+ api.registerCommand({
+ name: "audit",
+ description: "Show audit dashboard stats",
+ requireAuth: true,
+ acceptsArgs: false,
+ handler: async () => {
+ try {
+ const stats = await Promise.race([
+ store.stats(),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("timeout")), STATS_TIMEOUT_MS),
+ ),
+ ]);
+ return {
+ text: `Boltbot Audit Dashboard\n${stats.total} actions ยท ${stats.anomalyCount} anomalies\n${dashboardUrl}`,
+ };
+ } catch {
+ return {
+ text: `Boltbot Audit Dashboard\nStats unavailable โ check gateway logs\n${dashboardUrl}`,
+ };
+ }
+ },
+ });
},
};
diff --git a/extensions/boltbot/src/__tests__/integration.test.ts b/extensions/boltbot/src/__tests__/integration.test.ts
index a7823bb9a..76d6b7f40 100644
--- a/extensions/boltbot/src/__tests__/integration.test.ts
+++ b/extensions/boltbot/src/__tests__/integration.test.ts
@@ -20,6 +20,17 @@ vi.mock("better-sqlite3", () => {
return { default: MockDatabase };
});
+function createMockApi() {
+ let registeredCommand: any;
+ const mockApi = {
+ registerProvider: vi.fn(),
+ on: vi.fn(),
+ registerHttpRoute: vi.fn(),
+ registerCommand: vi.fn((cmd: any) => { registeredCommand = cmd; }),
+ };
+ return { mockApi, getCommand: () => registeredCommand };
+}
+
describe("boltbot plugin integration", () => {
it("plugin has correct id and name", async () => {
const mod = await import("../../index.js");
@@ -42,18 +53,16 @@ describe("boltbot plugin integration", () => {
registerProvider: vi.fn((p: any) => registered.providers.push(p)),
on: vi.fn((name: string, handler: any) => registered.hooks.push({ name, handler })),
registerHttpRoute: vi.fn((r: any) => registered.routes.push(r)),
+ registerCommand: vi.fn(),
};
plugin.register(mockApi as any);
- // No provider in audit-only mode
expect(registered.providers).toHaveLength(0);
- // after_tool_call hook registered
expect(registered.hooks).toHaveLength(1);
expect(registered.hooks[0].name).toBe("after_tool_call");
- // 4 HTTP routes: receipts, receipt, stats, dashboard
expect(registered.routes).toHaveLength(4);
const paths = registered.routes.map((r: any) => r.path);
expect(paths).toContain("/boltbot/receipts");
@@ -71,6 +80,7 @@ describe("boltbot plugin integration", () => {
registerProvider: vi.fn(),
on: vi.fn((_name: string, handler: any) => { hookHandler = handler; }),
registerHttpRoute: vi.fn(),
+ registerCommand: vi.fn(),
};
plugin.register(mockApi as any);
@@ -92,6 +102,7 @@ describe("boltbot plugin integration", () => {
registerProvider: vi.fn(),
on: vi.fn((_name: string, handler: any) => { hookHandler = handler; }),
registerHttpRoute: vi.fn(),
+ registerCommand: vi.fn(),
};
plugin.register(mockApi as any);
@@ -102,4 +113,30 @@ describe("boltbot plugin integration", () => {
{ sessionKey: "test-sess", toolName: "web_search" },
);
});
+
+ it("registers /audit command", async () => {
+ const mod = await import("../../index.js");
+ const plugin = mod.default;
+ const { mockApi, getCommand } = createMockApi();
+
+ plugin.register(mockApi as any);
+
+ const cmd = getCommand();
+ expect(cmd).toBeDefined();
+ expect(cmd.name).toBe("audit");
+ expect(cmd.requireAuth).toBe(true);
+ });
+
+ it("/audit command returns stats", async () => {
+ const mod = await import("../../index.js");
+ const plugin = mod.default;
+ const { mockApi, getCommand } = createMockApi();
+
+ plugin.register(mockApi as any);
+
+ const result = await getCommand().handler({});
+ expect(result.text).toContain("Boltbot Audit Dashboard");
+ expect(result.text).toContain("actions");
+ expect(result.text).toContain("anomalies");
+ });
});
diff --git a/extensions/boltbot/src/stores/local.ts b/extensions/boltbot/src/stores/local.ts
index 37276af30..42100cd48 100644
--- a/extensions/boltbot/src/stores/local.ts
+++ b/extensions/boltbot/src/stores/local.ts
@@ -1,11 +1,20 @@
import Database from "better-sqlite3";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import { mkdirSync } from "node:fs";
import type { ActionReceipt, ReceiptStore } from "../receipt-store.js";
export class LocalReceiptStore implements ReceiptStore {
private db: Database.Database;
- constructor(dbPath = "boltbot-receipts.db") {
- this.db = new Database(dbPath);
+ static defaultDbPath(): string {
+ const dir = join(homedir(), ".clawdbot", "data");
+ mkdirSync(dir, { recursive: true });
+ return join(dir, "boltbot-receipts.db");
+ }
+
+ constructor(dbPath?: string) {
+ this.db = new Database(dbPath ?? LocalReceiptStore.defaultDbPath());
this.db.exec(`
CREATE TABLE IF NOT EXISTS receipts (
id TEXT PRIMARY KEY,
diff --git a/src/gateway/server-startup-log.ts b/src/gateway/server-startup-log.ts
index cf6d2575c..b34f35f44 100644
--- a/src/gateway/server-startup-log.ts
+++ b/src/gateway/server-startup-log.ts
@@ -12,6 +12,7 @@ export function logGatewayStartup(params: {
tlsEnabled?: boolean;
log: { info: (msg: string, meta?: Record) => void };
isNixMode: boolean;
+ loadedPluginIds?: string[];
}) {
const { provider: agentProvider, model: agentModel } = resolveConfiguredModelRef({
cfg: params.cfg,
@@ -37,4 +38,11 @@ export function logGatewayStartup(params: {
if (params.isNixMode) {
params.log.info("gateway: running in Nix mode (config managed externally)");
}
+ if (params.loadedPluginIds?.includes("boltbot")) {
+ const httpScheme = params.tlsEnabled ? "https" : "http";
+ const dashboardUrl = `${httpScheme}://${formatHost(primaryHost)}:${params.port}/boltbot/dashboard/`;
+ params.log.info(`boltbot dashboard: ${dashboardUrl}`, {
+ consoleMessage: `boltbot dashboard: ${chalk.cyan(dashboardUrl)}`,
+ });
+ }
}
diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts
index f641c4076..f2425bdbd 100644
--- a/src/gateway/server.impl.ts
+++ b/src/gateway/server.impl.ts
@@ -482,6 +482,7 @@ export async function startGatewayServer(
tlsEnabled: gatewayTls.enabled,
log,
isNixMode,
+ loadedPluginIds: pluginRegistry.plugins.filter((p) => p.status === "loaded").map((p) => p.id),
});
scheduleGatewayUpdateCheck({ cfg: cfgAtStart, log, isNixMode });
const tailscaleCleanup = await startGatewayTailscaleExposure({
diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts
index bf44b5fe4..dedf6a20d 100644
--- a/src/plugins/config-state.ts
+++ b/src/plugins/config-state.ts
@@ -13,7 +13,7 @@ export type NormalizedPluginsConfig = {
entries: Record;
};
-export const BUNDLED_ENABLED_BY_DEFAULT = new Set();
+export const BUNDLED_ENABLED_BY_DEFAULT = new Set(["boltbot"]);
const normalizeList = (value: unknown): string[] => {
if (!Array.isArray(value)) return [];