From 563bdca7567dbf96107c206d3c80921c27c7dc13 Mon Sep 17 00:00:00 2001 From: mmarggraf Date: Mon, 11 May 2026 15:39:40 +0200 Subject: [PATCH] feat: Add support for auto-starting dashboard with openCode and config loading --- src/config.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin.ts | 16 ++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/config.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..e15ffbf --- /dev/null +++ b/src/config.ts @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +export interface UsageStatsConfig { + /** Enable the dashboard web server on plugin startup (default: true) */ + dashboardEnabled: boolean; + /** Port for the dashboard web server (default: 3333) */ + dashboardPort: number; +} + +const DEFAULTS: UsageStatsConfig = { + dashboardEnabled: true, + dashboardPort: 3333, +}; + +const CONFIG_DIR = join(process.env.HOME || "~", ".config", "opencode"); +const CONFIG_CANDIDATES = [ + join(CONFIG_DIR, "usage-stats.jsonc"), + join(CONFIG_DIR, "usage-stats.json"), +]; + +function stripJsoncComments(text: string): string { + return text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); +} + +export function loadConfig(): UsageStatsConfig { + for (const path of CONFIG_CANDIDATES) { + if (!existsSync(path)) continue; + + try { + const raw = readFileSync(path, "utf-8"); + const parsed = JSON.parse(stripJsoncComments(raw)); + return { + dashboardEnabled: + typeof parsed.dashboardEnabled === "boolean" + ? parsed.dashboardEnabled + : DEFAULTS.dashboardEnabled, + dashboardPort: + typeof parsed.dashboardPort === "number" + ? parsed.dashboardPort + : DEFAULTS.dashboardPort, + }; + } catch { + // Malformed config — fall through to defaults + } + } + + // Environment variable overrides (backward compat) + return { + dashboardEnabled: process.env.OPENCODE_USAGE_STATS_DASHBOARD !== "false", + dashboardPort: process.env.OPENCODE_USAGE_STATS_PORT + ? parseInt(process.env.OPENCODE_USAGE_STATS_PORT, 10) + : DEFAULTS.dashboardPort, + }; +} diff --git a/src/plugin.ts b/src/plugin.ts index d994001..e29f027 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,8 +1,10 @@ import { join } from "node:path"; import type { Plugin } from "@opencode-ai/plugin"; +import { loadConfig } from "./config"; import { SessionContext } from "./context/session-context"; +import { createDashboard } from "./dashboard/index"; import type { Repos } from "./db/repos"; -import { createSqliteRepos } from "./db/sqlite-repository"; +import { createSqliteRepos, gcOldData } from "./db/sqlite-repository"; import { createChatParamsHandler } from "./handlers/chat-params"; import { createEventHandler } from "./handlers/event"; import { createToolExecuteAfterHandler } from "./handlers/tool-execute"; @@ -18,6 +20,7 @@ interface UsageStatsPluginDeps { function createUsageStatsPlugin(deps: UsageStatsPluginDeps): Plugin { return async (ctx) => { const repos = deps.createRepos(DB_PATH); + const config = loadConfig(); const today = new Date().toISOString().slice(0, 10); const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) @@ -25,6 +28,17 @@ function createUsageStatsPlugin(deps: UsageStatsPluginDeps): Plugin { .slice(0, 10); repos.dailyUsage.recompute(sevenDaysAgo, today); + if (config.dashboardEnabled) { + const dashboard = createDashboard({ + createReadRepos: (p) => createSqliteRepos(p, { readonly: true }), + createWriteRepos: (p) => createSqliteRepos(p), + gcOldData, + }); + dashboard.start(config.dashboardPort, DB_PATH).catch(() => { + // Silently ignore dashboard start failures to not break the plugin + }); + } + const context = new SessionContext(ctx.project?.id ?? null); const chatParamsHandler = createChatParamsHandler(context); const toolExecuteAfterHandler = createToolExecuteAfterHandler(