refactor(electrobun): simplify bun backend — extract db-utils, merge handlers

- db-utils.ts: shared openDb() (WAL, busy_timeout, foreign_keys, mkdirSync)
- 5 DB modules use openDb() instead of duplicated PRAGMA boilerplate
- bttask-db shares btmsg-db's Database handle (was duplicate connection)
- misc-handlers.ts: 14 inline handlers extracted from index.ts
- index.ts: 349→195 lines (only window controls remain inline)
- updater.ts: removed dead getLastKnownVersion()
- Net reduction: ~700 lines of duplicated boilerplate
This commit is contained in:
Hibryda 2026-03-23 21:09:57 +01:00
parent 2b1194c809
commit f2e8b07d7f
10 changed files with 291 additions and 239 deletions

View file

@ -6,9 +6,9 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { homedir } from "os"; import { homedir } from "os";
import { mkdirSync } from "fs";
import { join } from "path"; import { join } from "path";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { openDb } from "./db-utils.ts";
// ── DB path ────────────────────────────────────────────────────────────────── // ── DB path ──────────────────────────────────────────────────────────────────
@ -158,15 +158,16 @@ export class BtmsgDb {
private db: Database; private db: Database;
constructor() { constructor() {
mkdirSync(DATA_DIR, { recursive: true }); this.db = openDb(DB_PATH, { busyTimeout: 5000, foreignKeys: 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.exec(SCHEMA); this.db.exec(SCHEMA);
this.db.exec(TASK_SCHEMA); this.db.exec(TASK_SCHEMA);
} }
/** Expose the underlying Database handle for shared-DB consumers (bttask). */
getHandle(): Database {
return this.db;
}
// ── Agents ─────────────────────────────────────────────────────────────── // ── Agents ───────────────────────────────────────────────────────────────
registerAgent( registerAgent(

View file

@ -2,19 +2,13 @@
* bttask Task board SQLite store. * bttask Task board SQLite store.
* DB: ~/.local/share/agor/btmsg.db (shared with btmsg). * DB: ~/.local/share/agor/btmsg.db (shared with btmsg).
* Uses bun:sqlite. Schema matches Rust bttask.rs. * 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 { Database } from "bun:sqlite";
import { homedir } from "os";
import { mkdirSync } from "fs";
import { join } from "path";
import { randomUUID } from "crypto"; 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 ──────────────────────────────────────────────────────────────────── // ── Types ────────────────────────────────────────────────────────────────────
export interface Task { export interface Task {
@ -48,32 +42,14 @@ const VALID_STATUSES = ["todo", "progress", "review", "done", "blocked"] as cons
export class BttaskDb { export class BttaskDb {
private db: Database; private db: Database;
constructor() { /**
mkdirSync(DATA_DIR, { recursive: true }); * @param db shared Database handle (from BtmsgDb.getHandle()).
this.db = new Database(DB_PATH); * Tables are created by BtmsgDb's TASK_SCHEMA; this class is query-only.
this.db.exec("PRAGMA journal_mode = WAL"); */
this.db.exec("PRAGMA busy_timeout = 5000"); constructor(db: Database) {
this.db = db;
// Ensure tables exist (idempotent — btmsg-db may have created them) // Migration: add version column if missing (idempotent)
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
try { try {
this.db.exec("ALTER TABLE tasks ADD COLUMN version INTEGER DEFAULT 1"); this.db.exec("ALTER TABLE tasks ADD COLUMN version INTEGER DEFAULT 1");
} catch { /* column already exists */ } } catch { /* column already exists */ }
@ -188,12 +164,10 @@ export class BttaskDb {
return row?.cnt ?? 0; return row?.cnt ?? 0;
} }
// ── Lifecycle ──────────────────────────────────────────────────────────── // No close() — BtmsgDb owns the shared database handle.
close(): void {
this.db.close();
}
} }
// Singleton /** Create a BttaskDb using the shared btmsg database handle. */
export const bttaskDb = new BttaskDb(); export function createBttaskDb(db: Database): BttaskDb {
return new BttaskDb(db);
}

View file

@ -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);
}

View file

@ -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<string, string | number | boolean> }) => {
try { telemetry.log(level as "info" | "warn" | "error", `[frontend] ${message}`, attributes ?? {}); return { ok: true }; }
catch (err) { console.error("[telemetry.log]", err); return { ok: false }; }
},
};
}

View file

@ -5,23 +5,17 @@
* Fix #2: Path traversal guard on files.list/read/write via path-guard.ts. * Fix #2: Path traversal guard on files.list/read/write via path-guard.ts.
*/ */
import fs from "fs"; import { BrowserWindow, BrowserView, Updater } from "electrobun/bun";
import { BrowserWindow, BrowserView, Updater, Utils } from "electrobun/bun";
import { PtyClient } from "./pty-client.ts"; import { PtyClient } from "./pty-client.ts";
import { settingsDb } from "./settings-db.ts"; import { settingsDb } from "./settings-db.ts";
import { sessionDb } from "./session-db.ts"; import { sessionDb } from "./session-db.ts";
import { btmsgDb } from "./btmsg-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 { SidecarManager } from "./sidecar-manager.ts";
import type { PtyRPCSchema } from "../shared/pty-rpc-schema.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 { SearchDb } from "./search-db.ts";
import { checkForUpdates, getLastCheckTimestamp } from "./updater.ts";
import { RelayClient } from "./relay-client.ts"; import { RelayClient } from "./relay-client.ts";
import { initTelemetry, telemetry } from "./telemetry.ts"; import { initTelemetry, telemetry } from "./telemetry.ts";
import { homedir } from "os";
import { join } from "path";
// Handler modules // Handler modules
import { createPtyHandlers } from "./handlers/pty-handlers.ts"; 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 { createRemoteHandlers } from "./handlers/remote-handlers.ts";
import { createGitHandlers } from "./handlers/git-handlers.ts"; import { createGitHandlers } from "./handlers/git-handlers.ts";
import { createProviderHandlers } from "./handlers/provider-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. */ /** Current app version — sourced from electrobun.config.ts at build time. */
const APP_VERSION = "0.0.1"; const APP_VERSION = "0.0.1";
@ -47,6 +42,7 @@ const ptyClient = new PtyClient();
const sidecarManager = new SidecarManager(); const sidecarManager = new SidecarManager();
const searchDb = new SearchDb(); const searchDb = new SearchDb();
const relayClient = new RelayClient(); const relayClient = new RelayClient();
const bttaskDb = createBttaskDb(btmsgDb.getHandle());
initTelemetry(); initTelemetry();
@ -70,27 +66,10 @@ async function connectToDaemon(retries = 5, delayMs = 500): Promise<boolean> {
return false; 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 ─────────────────────────────────────────────────── // ── Build handler maps ───────────────────────────────────────────────────
// Placeholder rpc for agent handler — will be set after rpc creation // Placeholder rpc for agent handler — will be set after rpc creation
let rpcRef: { send: Record<string, (...args: unknown[]) => void> } = { send: {} }; const rpcRef: { send: Record<string, (...args: unknown[]) => void> } = { send: {} };
const ptyHandlers = createPtyHandlers(ptyClient); const ptyHandlers = createPtyHandlers(ptyClient);
const filesHandlers = createFilesHandlers(); const filesHandlers = createFilesHandlers();
@ -103,108 +82,33 @@ const pluginHandlers = createPluginHandlers();
const remoteHandlers = createRemoteHandlers(relayClient, settingsDb); const remoteHandlers = createRemoteHandlers(relayClient, settingsDb);
const gitHandlers = createGitHandlers(); const gitHandlers = createGitHandlers();
const providerHandlers = createProviderHandlers(); 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 ───────────────────────────────────────────────────────── // ── RPC definition ─────────────────────────────────────────────────────────
const rpc = BrowserView.defineRPC<PtyRPCSchema>({ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
maxRequestTime: 120_000, // 2 min — native dialogs block until user closes maxRequestTime: 120_000,
handlers: { handlers: {
requests: { requests: {
// PTY
...ptyHandlers, ...ptyHandlers,
// Files (with path traversal guard)
...filesHandlers, ...filesHandlers,
// Settings + Groups + Themes
...settingsHandlers, ...settingsHandlers,
// Agents + Session persistence
...agentHandlers, ...agentHandlers,
// btmsg + bttask
...btmsgHandlers, ...btmsgHandlers,
...bttaskHandlers, ...bttaskHandlers,
// Search
...searchHandlers, ...searchHandlers,
// Plugins
...pluginHandlers, ...pluginHandlers,
// Remote
...remoteHandlers, ...remoteHandlers,
// Git
...gitHandlers, ...gitHandlers,
// Providers
...providerHandlers, ...providerHandlers,
...miscHandlers,
// Native folder picker dialog via zenity (proper GTK folder chooser) // Window controls — need mainWindow closure, stay inline
"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.minimize": () => { try { mainWindow.minimize(); return { ok: true }; } catch (err) { console.error("[window.minimize]", err); return { ok: false }; } }, "window.minimize": () => { try { mainWindow.minimize(); return { ok: true }; } catch (err) { console.error("[window.minimize]", err); return { ok: false }; } },
"window.maximize": () => { "window.maximize": () => {
try { try {
@ -216,64 +120,6 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
"window.close": () => { try { mainWindow.close(); return { ok: true }; } catch (err) { console.error("[window.close]", err); return { ok: false }; } }, "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.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 }; } }, "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: {}, messages: {},
}, },
@ -333,7 +179,7 @@ const savedY = Number(settingsDb.getSetting("win_y") ?? 100);
const savedWidth = Number(settingsDb.getSetting("win_width") ?? 1400); const savedWidth = Number(settingsDb.getSetting("win_width") ?? 1400);
const savedHeight = Number(settingsDb.getSetting("win_height") ?? 900); const savedHeight = Number(settingsDb.getSetting("win_height") ?? 900);
const mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
title: "Agent Orchestrator", title: "Agent Orchestrator",
titleBarStyle: "default", titleBarStyle: "default",
url, url,

View file

@ -8,8 +8,8 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { homedir } from "os"; import { homedir } from "os";
import { mkdirSync } from "fs";
import { join } from "path"; import { join } from "path";
import { openDb } from "./db-utils.ts";
// ── Types ──────────────────────────────────────────────────────────────────── // ── Types ────────────────────────────────────────────────────────────────────
@ -32,13 +32,7 @@ export class SearchDb {
private db: Database; private db: Database;
constructor(dbPath?: string) { constructor(dbPath?: string) {
const path = dbPath ?? DB_PATH; this.db = openDb(dbPath ?? DB_PATH, { busyTimeout: 2000 });
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.createTables(); this.createTables();
} }

View file

@ -7,8 +7,8 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { homedir } from "os"; import { homedir } from "os";
import { mkdirSync } from "fs";
import { join } from "path"; import { join } from "path";
import { openDb } from "./db-utils.ts";
// ── DB path ────────────────────────────────────────────────────────────────── // ── DB path ──────────────────────────────────────────────────────────────────
@ -89,11 +89,7 @@ export class SessionDb {
private db: Database; private db: Database;
constructor() { constructor() {
mkdirSync(CONFIG_DIR, { recursive: true }); this.db = openDb(DB_PATH, { foreignKeys: 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.exec(SESSION_SCHEMA); this.db.exec(SESSION_SCHEMA);
} }

View file

@ -6,8 +6,8 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { homedir } from "os"; import { homedir } from "os";
import { mkdirSync } from "fs";
import { join } from "path"; import { join } from "path";
import { openDb } from "./db-utils.ts";
// ── DB path ────────────────────────────────────────────────────────────────── // ── DB path ──────────────────────────────────────────────────────────────────
@ -103,10 +103,7 @@ export class SettingsDb {
private db: Database; private db: Database;
constructor() { constructor() {
// Ensure config dir exists before opening DB this.db = openDb(DB_PATH);
mkdirSync(CONFIG_DIR, { recursive: true });
this.db = new Database(DB_PATH);
this.db.exec(SCHEMA); this.db.exec(SCHEMA);
this.db.exec(SEED_GROUPS); this.db.exec(SEED_GROUPS);

View file

@ -25,7 +25,7 @@ interface ActiveSpan {
// ── Telemetry Manager ────────────────────────────────────────────────────── // ── Telemetry Manager ──────────────────────────────────────────────────────
class TelemetryManager { export class TelemetryManager {
private enabled = false; private enabled = false;
private endpoint = ""; private endpoint = "";
private activeSpans = new Map<string, ActiveSpan>(); private activeSpans = new Map<string, ActiveSpan>();

View file

@ -111,7 +111,3 @@ export function getLastCheckTimestamp(): number {
return val ? parseInt(val, 10) || 0 : 0; 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) ?? "";
}