diff --git a/ui-electrobun/src/bun/btmsg-db.ts b/ui-electrobun/src/bun/btmsg-db.ts index 5049d97..36f82b3 100644 --- a/ui-electrobun/src/bun/btmsg-db.ts +++ b/ui-electrobun/src/bun/btmsg-db.ts @@ -6,9 +6,9 @@ import { Database } from "bun:sqlite"; import { homedir } from "os"; -import { mkdirSync } from "fs"; import { join } from "path"; import { randomUUID } from "crypto"; +import { openDb } from "./db-utils.ts"; // ── DB path ────────────────────────────────────────────────────────────────── @@ -158,15 +158,16 @@ export class BtmsgDb { private db: Database; constructor() { - mkdirSync(DATA_DIR, { recursive: true }); - this.db = new Database(DB_PATH); - this.db.exec("PRAGMA journal_mode = WAL"); - this.db.exec("PRAGMA busy_timeout = 5000"); - this.db.exec("PRAGMA foreign_keys = ON"); + this.db = openDb(DB_PATH, { busyTimeout: 5000, foreignKeys: true }); this.db.exec(SCHEMA); this.db.exec(TASK_SCHEMA); } + /** Expose the underlying Database handle for shared-DB consumers (bttask). */ + getHandle(): Database { + return this.db; + } + // ── Agents ─────────────────────────────────────────────────────────────── registerAgent( diff --git a/ui-electrobun/src/bun/bttask-db.ts b/ui-electrobun/src/bun/bttask-db.ts index 6b65b2b..24bbaef 100644 --- a/ui-electrobun/src/bun/bttask-db.ts +++ b/ui-electrobun/src/bun/bttask-db.ts @@ -2,19 +2,13 @@ * bttask — Task board SQLite store. * DB: ~/.local/share/agor/btmsg.db (shared with btmsg). * Uses bun:sqlite. Schema matches Rust bttask.rs. + * + * Accepts a Database handle from BtmsgDb to avoid double-opening the same file. */ import { Database } from "bun:sqlite"; -import { homedir } from "os"; -import { mkdirSync } from "fs"; -import { join } from "path"; import { randomUUID } from "crypto"; -// ── DB path (same DB as btmsg) ────────────────────────────────────────────── - -const DATA_DIR = join(homedir(), ".local", "share", "agor"); -const DB_PATH = join(DATA_DIR, "btmsg.db"); - // ── Types ──────────────────────────────────────────────────────────────────── export interface Task { @@ -48,32 +42,14 @@ const VALID_STATUSES = ["todo", "progress", "review", "done", "blocked"] as cons export class BttaskDb { private db: Database; - constructor() { - mkdirSync(DATA_DIR, { recursive: true }); - this.db = new Database(DB_PATH); - this.db.exec("PRAGMA journal_mode = WAL"); - this.db.exec("PRAGMA busy_timeout = 5000"); + /** + * @param db — shared Database handle (from BtmsgDb.getHandle()). + * Tables are created by BtmsgDb's TASK_SCHEMA; this class is query-only. + */ + constructor(db: Database) { + this.db = db; - // Ensure tables exist (idempotent — btmsg-db may have created them) - this.db.exec(` - CREATE TABLE IF NOT EXISTS tasks ( - id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT DEFAULT '', - status TEXT DEFAULT 'todo', priority TEXT DEFAULT 'medium', - assigned_to TEXT, created_by TEXT NOT NULL, group_id TEXT NOT NULL, - parent_task_id TEXT, sort_order INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), version INTEGER DEFAULT 1 - ); - CREATE INDEX IF NOT EXISTS idx_tasks_group ON tasks(group_id); - CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); - CREATE TABLE IF NOT EXISTS task_comments ( - id TEXT PRIMARY KEY, task_id TEXT NOT NULL, agent_id TEXT NOT NULL, - content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id); - `); - - // Migration: add version column if missing + // Migration: add version column if missing (idempotent) try { this.db.exec("ALTER TABLE tasks ADD COLUMN version INTEGER DEFAULT 1"); } catch { /* column already exists */ } @@ -188,12 +164,10 @@ export class BttaskDb { return row?.cnt ?? 0; } - // ── Lifecycle ──────────────────────────────────────────────────────────── - - close(): void { - this.db.close(); - } + // No close() — BtmsgDb owns the shared database handle. } -// Singleton -export const bttaskDb = new BttaskDb(); +/** Create a BttaskDb using the shared btmsg database handle. */ +export function createBttaskDb(db: Database): BttaskDb { + return new BttaskDb(db); +} diff --git a/ui-electrobun/src/bun/db-utils.ts b/ui-electrobun/src/bun/db-utils.ts new file mode 100644 index 0000000..5cc8373 --- /dev/null +++ b/ui-electrobun/src/bun/db-utils.ts @@ -0,0 +1,50 @@ +/** + * Shared SQLite utilities for all DB modules. + * Eliminates duplicated init boilerplate (mkdirSync, WAL, busy_timeout). + */ + +import { Database } from "bun:sqlite"; +import { mkdirSync } from "fs"; +import { dirname } from "path"; + +export interface OpenDbOptions { + /** PRAGMA busy_timeout in ms. Default 500. */ + busyTimeout?: number; + /** Enable foreign keys. Default false. */ + foreignKeys?: boolean; + /** Open in readonly mode. Default false. */ + readonly?: boolean; +} + +/** + * Open (or create) a SQLite database with standard PRAGMA setup. + * Ensures the parent directory exists, enables WAL mode, sets busy_timeout. + */ +export function openDb(dbPath: string, options: OpenDbOptions = {}): Database { + const { busyTimeout = 500, foreignKeys = false, readonly = false } = options; + + if (dbPath !== ":memory:") { + mkdirSync(dirname(dbPath), { recursive: true }); + } + + const db = readonly + ? new Database(dbPath, { readonly: true }) + : new Database(dbPath); + if (!readonly) { + db.exec("PRAGMA journal_mode = WAL"); + } + db.exec(`PRAGMA busy_timeout = ${busyTimeout}`); + if (foreignKeys) { + db.exec("PRAGMA foreign_keys = ON"); + } + + return db; +} + +/** + * Standard error message extraction. + * Returns the error message string from an unknown caught value. + */ +export function errMsg(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} diff --git a/ui-electrobun/src/bun/handlers/misc-handlers.ts b/ui-electrobun/src/bun/handlers/misc-handlers.ts new file mode 100644 index 0000000..683dc62 --- /dev/null +++ b/ui-electrobun/src/bun/handlers/misc-handlers.ts @@ -0,0 +1,198 @@ +/** + * Miscellaneous RPC handlers — memora, keybindings, updater, diagnostics, telemetry, + * files.pickDirectory, files.homeDir, project.templates, project.clone. + * + * These are small handlers that don't warrant their own file. + */ + +import fs from "fs"; +import { Database } from "bun:sqlite"; +import { homedir } from "os"; +import { join } from "path"; +import { randomUUID } from "crypto"; +import type { SettingsDb } from "../settings-db.ts"; +import type { PtyClient } from "../pty-client.ts"; +import type { RelayClient } from "../relay-client.ts"; +import type { SidecarManager } from "../sidecar-manager.ts"; +import { checkForUpdates, getLastCheckTimestamp } from "../updater.ts"; +import type { TelemetryManager } from "../telemetry.ts"; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +const BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/; + +async function gitWorktreeAdd( + mainRepoPath: string, + worktreePath: string, + branchName: string, +): Promise<{ ok: boolean; error?: string }> { + const proc = Bun.spawn( + ["git", "worktree", "add", worktreePath, "-b", branchName], + { cwd: mainRepoPath, stderr: "pipe", stdout: "pipe" }, + ); + const exitCode = await proc.exited; + if (exitCode !== 0) { + const errText = await new Response(proc.stderr).text(); + return { ok: false, error: errText.trim() || `git exited with code ${exitCode}` }; + } + return { ok: true }; +} + +// ── Types ─────────────────────────────────────────────────────────────────── + +interface MiscDeps { + settingsDb: SettingsDb; + ptyClient: PtyClient; + relayClient: RelayClient; + sidecarManager: SidecarManager; + telemetry: TelemetryManager; + appVersion: string; +} + +// ── Handler factory ───────────────────────────────────────────────────────── + +export function createMiscHandlers(deps: MiscDeps) { + const { settingsDb, ptyClient, relayClient, sidecarManager, telemetry, appVersion } = deps; + + return { + // ── Files: picker + homeDir ───────────────────────────────────────── + "files.pickDirectory": async ({ startingFolder }: { startingFolder?: string }) => { + try { + const { execSync } = await import("child_process"); + const start = startingFolder?.replace(/^~/, process.env.HOME || "/home") || process.env.HOME || "/home"; + const result = execSync( + `zenity --file-selection --directory --title="Select Project Folder" --filename="${start}/"`, + { encoding: "utf-8", timeout: 120_000 }, + ).trim(); + return { path: result || null }; + } catch { + return { path: null }; + } + }, + + "files.homeDir": async () => ({ path: process.env.HOME || "/home" }), + + // ── Project templates ─────────────────────────────────────────────── + "project.templates": async () => ({ + templates: [ + { id: "blank", name: "Blank Project", description: "Empty directory with no scaffolding", icon: "📁" }, + { id: "web-app", name: "Web App", description: "HTML/CSS/JS web application starter", icon: "🌐" }, + { id: "api-server", name: "API Server", description: "Node.js/Bun HTTP API server", icon: "⚡" }, + { id: "cli-tool", name: "CLI Tool", description: "Command-line tool with argument parser", icon: "🔧" }, + ], + }), + + // ── Project clone ─────────────────────────────────────────────────── + "project.clone": async ({ projectId, branchName }: { projectId: string; branchName: string }) => { + try { + if (!BRANCH_RE.test(branchName)) { + return { ok: false, error: "Invalid branch name. Use only letters, numbers, /, _, -, ." }; + } + const source = settingsDb.getProject(projectId); + if (!source) return { ok: false, error: `Project not found: ${projectId}` }; + + const mainRepoPath = source.mainRepoPath ?? source.cwd; + const allProjects = settingsDb.listProjects(); + const existingClones = allProjects.filter( + (p) => p.cloneOf === projectId || (source.cloneOf && p.cloneOf === source.cloneOf), + ); + if (existingClones.length >= 3) return { ok: false, error: "Maximum 3 clones per project reached" }; + + const cloneIndex = existingClones.length + 1; + const wtSuffix = randomUUID().slice(0, 8); + const worktreePath = `${mainRepoPath}-wt-${wtSuffix}`; + + const gitResult = await gitWorktreeAdd(mainRepoPath, worktreePath, branchName); + if (!gitResult.ok) return { ok: false, error: gitResult.error }; + + const cloneId = `${projectId}-clone-${cloneIndex}-${randomUUID().slice(0, 8)}`; + const cloneConfig = { + id: cloneId, name: `${source.name} [${branchName}]`, + cwd: worktreePath, accent: source.accent, provider: source.provider, + profile: source.profile, model: source.model, + groupId: source.groupId ?? "dev", mainRepoPath, + cloneOf: projectId, worktreePath, worktreeBranch: branchName, cloneIndex, + }; + settingsDb.setProject(cloneId, cloneConfig); + return { ok: true, project: { id: cloneId, config: JSON.stringify(cloneConfig) } }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[project.clone]", err); + return { ok: false, error }; + } + }, + + // ── Keybindings ───────────────────────────────────────────────────── + "keybindings.getAll": () => { + try { return { keybindings: settingsDb.getKeybindings() }; } + catch (err) { console.error("[keybindings.getAll]", err); return { keybindings: {} }; } + }, + "keybindings.set": ({ id, chord }: { id: string; chord: string }) => { + try { settingsDb.setKeybinding(id, chord); return { ok: true }; } + catch (err) { console.error("[keybindings.set]", err); return { ok: false }; } + }, + "keybindings.reset": ({ id }: { id: string }) => { + try { settingsDb.deleteKeybinding(id); return { ok: true }; } + catch (err) { console.error("[keybindings.reset]", err); return { ok: false }; } + }, + + // ── Updater ───────────────────────────────────────────────────────── + "updater.check": async () => { + try { + const result = await checkForUpdates(appVersion); + return { ...result, error: undefined }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[updater.check]", err); + return { available: false, version: "", downloadUrl: "", releaseNotes: "", checkedAt: Date.now(), error }; + } + }, + "updater.getVersion": () => ({ version: appVersion, lastCheck: getLastCheckTimestamp() }), + + // ── Memora (read-only) ────────────────────────────────────────────── + "memora.search": ({ query, limit }: { query: string; limit?: number }) => { + try { + const dbPath = join(homedir(), ".local", "share", "memora", "memories.db"); + if (!fs.existsSync(dbPath)) return { memories: [] }; + const db = new Database(dbPath, { readonly: true }); + try { + const rows = db.query( + "SELECT id, content, tags, metadata, created_at as createdAt, updated_at as updatedAt FROM memories WHERE content LIKE ? ORDER BY updated_at DESC LIMIT ?", + ).all(`%${query}%`, limit ?? 20); + return { memories: rows as Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string }> }; + } finally { db.close(); } + } catch (err) { console.error("[memora.search]", err); return { memories: [] }; } + }, + "memora.list": ({ limit, tag }: { limit?: number; tag?: string }) => { + try { + const dbPath = join(homedir(), ".local", "share", "memora", "memories.db"); + if (!fs.existsSync(dbPath)) return { memories: [] }; + const db = new Database(dbPath, { readonly: true }); + try { + let sql = "SELECT id, content, tags, metadata, created_at as createdAt, updated_at as updatedAt FROM memories"; + const params: unknown[] = []; + if (tag) { sql += " WHERE tags LIKE ?"; params.push(`%${tag}%`); } + sql += " ORDER BY updated_at DESC LIMIT ?"; + params.push(limit ?? 20); + const rows = db.query(sql).all(...params); + return { memories: rows as Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string }> }; + } finally { db.close(); } + } catch (err) { console.error("[memora.list]", err); return { memories: [] }; } + }, + + // ── Diagnostics ───────────────────────────────────────────────────── + "diagnostics.stats": () => ({ + ptyConnected: ptyClient.isConnected, + relayConnections: relayClient.listMachines().filter((m) => m.status === "connected").length, + activeSidecars: sidecarManager.listSessions().filter((s) => s.status === "running").length, + rpcCallCount: 0, + droppedEvents: 0, + }), + + // ── Telemetry ─────────────────────────────────────────────────────── + "telemetry.log": ({ level, message, attributes }: { level: string; message: string; attributes?: Record }) => { + try { telemetry.log(level as "info" | "warn" | "error", `[frontend] ${message}`, attributes ?? {}); return { ok: true }; } + catch (err) { console.error("[telemetry.log]", err); return { ok: false }; } + }, + }; +} diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index 66ba69e..1bd55df 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -5,23 +5,17 @@ * Fix #2: Path traversal guard on files.list/read/write via path-guard.ts. */ -import fs from "fs"; -import { BrowserWindow, BrowserView, Updater, Utils } from "electrobun/bun"; +import { BrowserWindow, BrowserView, Updater } from "electrobun/bun"; import { PtyClient } from "./pty-client.ts"; import { settingsDb } from "./settings-db.ts"; import { sessionDb } from "./session-db.ts"; import { btmsgDb } from "./btmsg-db.ts"; -import { bttaskDb } from "./bttask-db.ts"; +import { createBttaskDb } from "./bttask-db.ts"; import { SidecarManager } from "./sidecar-manager.ts"; import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts"; -import { Database } from "bun:sqlite"; -import { randomUUID } from "crypto"; import { SearchDb } from "./search-db.ts"; -import { checkForUpdates, getLastCheckTimestamp } from "./updater.ts"; import { RelayClient } from "./relay-client.ts"; import { initTelemetry, telemetry } from "./telemetry.ts"; -import { homedir } from "os"; -import { join } from "path"; // Handler modules import { createPtyHandlers } from "./handlers/pty-handlers.ts"; @@ -34,6 +28,7 @@ import { createPluginHandlers } from "./handlers/plugin-handlers.ts"; import { createRemoteHandlers } from "./handlers/remote-handlers.ts"; import { createGitHandlers } from "./handlers/git-handlers.ts"; import { createProviderHandlers } from "./handlers/provider-handlers.ts"; +import { createMiscHandlers } from "./handlers/misc-handlers.ts"; /** Current app version — sourced from electrobun.config.ts at build time. */ const APP_VERSION = "0.0.1"; @@ -47,6 +42,7 @@ const ptyClient = new PtyClient(); const sidecarManager = new SidecarManager(); const searchDb = new SearchDb(); const relayClient = new RelayClient(); +const bttaskDb = createBttaskDb(btmsgDb.getHandle()); initTelemetry(); @@ -70,27 +66,10 @@ async function connectToDaemon(retries = 5, delayMs = 500): Promise { return false; } -// ── Clone helpers ────────────────────────────────────────────────────────── - -const BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/; - -async function gitWorktreeAdd(mainRepoPath: string, worktreePath: string, branchName: string): Promise<{ ok: boolean; error?: string }> { - const proc = Bun.spawn( - ["git", "worktree", "add", worktreePath, "-b", branchName], - { cwd: mainRepoPath, stderr: "pipe", stdout: "pipe" } - ); - const exitCode = await proc.exited; - if (exitCode !== 0) { - const errText = await new Response(proc.stderr).text(); - return { ok: false, error: errText.trim() || `git exited with code ${exitCode}` }; - } - return { ok: true }; -} - // ── Build handler maps ─────────────────────────────────────────────────── // Placeholder rpc for agent handler — will be set after rpc creation -let rpcRef: { send: Record void> } = { send: {} }; +const rpcRef: { send: Record void> } = { send: {} }; const ptyHandlers = createPtyHandlers(ptyClient); const filesHandlers = createFilesHandlers(); @@ -103,108 +82,33 @@ const pluginHandlers = createPluginHandlers(); const remoteHandlers = createRemoteHandlers(relayClient, settingsDb); const gitHandlers = createGitHandlers(); const providerHandlers = createProviderHandlers(); +const miscHandlers = createMiscHandlers({ + settingsDb, ptyClient, relayClient, sidecarManager, telemetry, appVersion: APP_VERSION, +}); + +// Window ref — handlers use closure; set after mainWindow creation +let mainWindow: BrowserWindow; // ── RPC definition ───────────────────────────────────────────────────────── const rpc = BrowserView.defineRPC({ - maxRequestTime: 120_000, // 2 min — native dialogs block until user closes + maxRequestTime: 120_000, handlers: { requests: { - // PTY ...ptyHandlers, - // Files (with path traversal guard) ...filesHandlers, - // Settings + Groups + Themes ...settingsHandlers, - // Agents + Session persistence ...agentHandlers, - // btmsg + bttask ...btmsgHandlers, ...bttaskHandlers, - // Search ...searchHandlers, - // Plugins ...pluginHandlers, - // Remote ...remoteHandlers, - // Git ...gitHandlers, - // Providers ...providerHandlers, + ...miscHandlers, - // Native folder picker dialog via zenity (proper GTK folder chooser) - "files.pickDirectory": async ({ startingFolder }) => { - try { - const { execSync } = await import("child_process"); - const start = startingFolder?.replace(/^~/, process.env.HOME || "/home") || process.env.HOME || "/home"; - const result = execSync( - `zenity --file-selection --directory --title="Select Project Folder" --filename="${start}/"`, - { encoding: "utf-8", timeout: 120_000 } - ).trim(); - return { path: result || null }; - } catch (e: any) { - // zenity exits with code 1 on cancel, code 5 if not found - // Do NOT fall back to Electrobun dialog — just return null - return { path: null }; - } - }, - - // Home directory - "files.homeDir": async () => { - return { path: process.env.HOME || "/home" }; - }, - - // Project templates (hardcoded list) - "project.templates": async () => ({ - templates: [ - { id: "blank", name: "Blank Project", description: "Empty directory with no scaffolding", icon: "📁" }, - { id: "web-app", name: "Web App", description: "HTML/CSS/JS web application starter", icon: "🌐" }, - { id: "api-server", name: "API Server", description: "Node.js/Bun HTTP API server", icon: "⚡" }, - { id: "cli-tool", name: "CLI Tool", description: "Command-line tool with argument parser", icon: "🔧" }, - ], - }), - - // ── Project clone handler ────────────────────────────────────────── - "project.clone": async ({ projectId, branchName }) => { - try { - if (!BRANCH_RE.test(branchName)) { - return { ok: false, error: "Invalid branch name. Use only letters, numbers, /, _, -, ." }; - } - const source = settingsDb.getProject(projectId); - if (!source) return { ok: false, error: `Project not found: ${projectId}` }; - - const mainRepoPath = source.mainRepoPath ?? source.cwd; - const allProjects = settingsDb.listProjects(); - const existingClones = allProjects.filter( - (p) => p.cloneOf === projectId || (source.cloneOf && p.cloneOf === source.cloneOf) - ); - if (existingClones.length >= 3) return { ok: false, error: "Maximum 3 clones per project reached" }; - - const cloneIndex = existingClones.length + 1; - const wtSuffix = randomUUID().slice(0, 8); - const worktreePath = `${mainRepoPath}-wt-${wtSuffix}`; - - const gitResult = await gitWorktreeAdd(mainRepoPath, worktreePath, branchName); - if (!gitResult.ok) return { ok: false, error: gitResult.error }; - - const cloneId = `${projectId}-clone-${cloneIndex}-${randomUUID().slice(0, 8)}`; - const cloneConfig = { - id: cloneId, name: `${source.name} [${branchName}]`, - cwd: worktreePath, accent: source.accent, provider: source.provider, - profile: source.profile, model: source.model, - groupId: source.groupId ?? "dev", mainRepoPath, - cloneOf: projectId, worktreePath, worktreeBranch: branchName, cloneIndex, - }; - settingsDb.setProject(cloneId, cloneConfig); - return { ok: true, project: { id: cloneId, config: JSON.stringify(cloneConfig) } }; - } catch (err) { - const error = err instanceof Error ? err.message : String(err); - console.error("[project.clone]", err); - return { ok: false, error }; - } - }, - - // ── Window controls ──────────────────────────────────────────────── + // Window controls — need mainWindow closure, stay inline "window.minimize": () => { try { mainWindow.minimize(); return { ok: true }; } catch (err) { console.error("[window.minimize]", err); return { ok: false }; } }, "window.maximize": () => { try { @@ -216,64 +120,6 @@ const rpc = BrowserView.defineRPC({ "window.close": () => { try { mainWindow.close(); return { ok: true }; } catch (err) { console.error("[window.close]", err); return { ok: false }; } }, "window.getFrame": () => { try { return mainWindow.getFrame(); } catch { return { x: 0, y: 0, width: 1400, height: 900 }; } }, "window.setPosition": ({ x, y }: { x: number; y: number }) => { try { mainWindow.setPosition(x, y); return { ok: true }; } catch { return { ok: false }; } }, - - // ── Keybindings ──────────────────────────────────────────────────── - "keybindings.getAll": () => { try { return { keybindings: settingsDb.getKeybindings() }; } catch (err) { console.error("[keybindings.getAll]", err); return { keybindings: {} }; } }, - "keybindings.set": ({ id, chord }) => { try { settingsDb.setKeybinding(id, chord); return { ok: true }; } catch (err) { console.error("[keybindings.set]", err); return { ok: false }; } }, - "keybindings.reset": ({ id }) => { try { settingsDb.deleteKeybinding(id); return { ok: true }; } catch (err) { console.error("[keybindings.reset]", err); return { ok: false }; } }, - - // ── Updater ──────────────────────────────────────────────────────── - "updater.check": async () => { - try { const result = await checkForUpdates(APP_VERSION); return { ...result, error: undefined }; } - catch (err) { const error = err instanceof Error ? err.message : String(err); console.error("[updater.check]", err); return { available: false, version: "", downloadUrl: "", releaseNotes: "", checkedAt: Date.now(), error }; } - }, - "updater.getVersion": () => ({ version: APP_VERSION, lastCheck: getLastCheckTimestamp() }), - - // ── Memora (read-only) ──────────────────────────────────────────── - "memora.search": ({ query, limit }) => { - try { - const dbPath = join(homedir(), ".local", "share", "memora", "memories.db"); - if (!fs.existsSync(dbPath)) return { memories: [] }; - const db = new Database(dbPath, { readonly: true }); - try { - const rows = db.query("SELECT id, content, tags, metadata, created_at as createdAt, updated_at as updatedAt FROM memories WHERE content LIKE ? ORDER BY updated_at DESC LIMIT ?").all(`%${query}%`, limit ?? 20); - return { memories: rows as Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string }> }; - } finally { db.close(); } - } catch (err) { console.error("[memora.search]", err); return { memories: [] }; } - }, - "memora.list": ({ limit, tag }) => { - try { - const dbPath = join(homedir(), ".local", "share", "memora", "memories.db"); - if (!fs.existsSync(dbPath)) return { memories: [] }; - const db = new Database(dbPath, { readonly: true }); - try { - let sql = "SELECT id, content, tags, metadata, created_at as createdAt, updated_at as updatedAt FROM memories"; - const params: unknown[] = []; - if (tag) { sql += " WHERE tags LIKE ?"; params.push(`%${tag}%`); } - sql += " ORDER BY updated_at DESC LIMIT ?"; - params.push(limit ?? 20); - const rows = db.query(sql).all(...params); - return { memories: rows as Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string }> }; - } finally { db.close(); } - } catch (err) { console.error("[memora.list]", err); return { memories: [] }; } - }, - - // ── Feature 8: Diagnostics ───────────────────────────────────────── - "diagnostics.stats": () => { - return { - ptyConnected: ptyClient.isConnected, - relayConnections: relayClient.listMachines().filter(m => m.status === "connected").length, - activeSidecars: sidecarManager.listSessions().filter(s => s.status === "running").length, - rpcCallCount: 0, // Placeholder — Electrobun doesn't expose RPC call count - droppedEvents: 0, - }; - }, - - // ── Telemetry ───────────────────────────────────────────────────── - "telemetry.log": ({ level, message, attributes }) => { - try { telemetry.log(level, `[frontend] ${message}`, attributes ?? {}); return { ok: true }; } - catch (err) { console.error("[telemetry.log]", err); return { ok: false }; } - }, }, messages: {}, }, @@ -333,7 +179,7 @@ const savedY = Number(settingsDb.getSetting("win_y") ?? 100); const savedWidth = Number(settingsDb.getSetting("win_width") ?? 1400); const savedHeight = Number(settingsDb.getSetting("win_height") ?? 900); -const mainWindow = new BrowserWindow({ +mainWindow = new BrowserWindow({ title: "Agent Orchestrator", titleBarStyle: "default", url, diff --git a/ui-electrobun/src/bun/search-db.ts b/ui-electrobun/src/bun/search-db.ts index 276ec2f..6a84110 100644 --- a/ui-electrobun/src/bun/search-db.ts +++ b/ui-electrobun/src/bun/search-db.ts @@ -8,8 +8,8 @@ import { Database } from "bun:sqlite"; import { homedir } from "os"; -import { mkdirSync } from "fs"; import { join } from "path"; +import { openDb } from "./db-utils.ts"; // ── Types ──────────────────────────────────────────────────────────────────── @@ -32,13 +32,7 @@ export class SearchDb { private db: Database; constructor(dbPath?: string) { - const path = dbPath ?? DB_PATH; - const dir = join(path, ".."); - mkdirSync(dir, { recursive: true }); - - this.db = new Database(path); - this.db.run("PRAGMA journal_mode = WAL"); - this.db.run("PRAGMA busy_timeout = 2000"); + this.db = openDb(dbPath ?? DB_PATH, { busyTimeout: 2000 }); this.createTables(); } diff --git a/ui-electrobun/src/bun/session-db.ts b/ui-electrobun/src/bun/session-db.ts index b5c6d2d..e16f7bc 100644 --- a/ui-electrobun/src/bun/session-db.ts +++ b/ui-electrobun/src/bun/session-db.ts @@ -7,8 +7,8 @@ import { Database } from "bun:sqlite"; import { homedir } from "os"; -import { mkdirSync } from "fs"; import { join } from "path"; +import { openDb } from "./db-utils.ts"; // ── DB path ────────────────────────────────────────────────────────────────── @@ -89,11 +89,7 @@ export class SessionDb { private db: Database; constructor() { - mkdirSync(CONFIG_DIR, { recursive: true }); - this.db = new Database(DB_PATH); - this.db.exec("PRAGMA journal_mode = WAL"); - this.db.exec("PRAGMA busy_timeout = 500"); - this.db.exec("PRAGMA foreign_keys = ON"); + this.db = openDb(DB_PATH, { foreignKeys: true }); this.db.exec(SESSION_SCHEMA); } diff --git a/ui-electrobun/src/bun/settings-db.ts b/ui-electrobun/src/bun/settings-db.ts index a3d399a..7d5d513 100644 --- a/ui-electrobun/src/bun/settings-db.ts +++ b/ui-electrobun/src/bun/settings-db.ts @@ -6,8 +6,8 @@ import { Database } from "bun:sqlite"; import { homedir } from "os"; -import { mkdirSync } from "fs"; import { join } from "path"; +import { openDb } from "./db-utils.ts"; // ── DB path ────────────────────────────────────────────────────────────────── @@ -103,10 +103,7 @@ export class SettingsDb { private db: Database; constructor() { - // Ensure config dir exists before opening DB - mkdirSync(CONFIG_DIR, { recursive: true }); - - this.db = new Database(DB_PATH); + this.db = openDb(DB_PATH); this.db.exec(SCHEMA); this.db.exec(SEED_GROUPS); diff --git a/ui-electrobun/src/bun/telemetry.ts b/ui-electrobun/src/bun/telemetry.ts index b6f563a..94cf4cc 100644 --- a/ui-electrobun/src/bun/telemetry.ts +++ b/ui-electrobun/src/bun/telemetry.ts @@ -25,7 +25,7 @@ interface ActiveSpan { // ── Telemetry Manager ────────────────────────────────────────────────────── -class TelemetryManager { +export class TelemetryManager { private enabled = false; private endpoint = ""; private activeSpans = new Map(); diff --git a/ui-electrobun/src/bun/updater.ts b/ui-electrobun/src/bun/updater.ts index a16c8bf..3a7ea02 100644 --- a/ui-electrobun/src/bun/updater.ts +++ b/ui-electrobun/src/bun/updater.ts @@ -111,7 +111,3 @@ export function getLastCheckTimestamp(): number { return val ? parseInt(val, 10) || 0 : 0; } -/** Return the last known remote version (empty string if never checked). */ -export function getLastKnownVersion(): string { - return settingsDb.getSetting(SETTINGS_KEY_LAST_VERSION) ?? ""; -}