import path from 'path'; import fs from 'fs'; import { getWorkspacePath } from '../config/paths.js'; import { ToolResult } from '../types.js'; function isPathInsideDir(base: string, target: string): boolean { const resolvedBase = path.resolve(base); const resolvedTarget = path.resolve(target); if (resolvedBase === resolvedTarget) return true; const rel = path.relative(resolvedBase, resolvedTarget); return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel); } function formatTable(columns: string[], rows: any[][]): string { if (!rows.length) return '(no rows)'; const widths = columns.map((c, i) => Math.min(40, Math.max(c.length, ...rows.map(r => String(r[i] ?? '').length))) ); const pad = (s: string, w: number) => String(s ?? '').padEnd(w).slice(0, w); const sep = widths.map(w => '-'.repeat(w)).join('-+-'); const header = columns.map((c, i) => pad(c, widths[i])).join(' | '); return [header, sep, ...rows.map(r => r.map((cell, i) => pad(String(cell ?? ''), widths[i])).join(' | '))].join('\n'); } const WRITE_RE = /^\s*(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|REPLACE|TRUNCATE|ATTACH|DETACH)\s/i; export const sqliteTool = { name: 'sqlite_query', description: 'Execute SQL queries on a SQLite database file inside the workspace. SELECT queries are allowed by default. Set write=true to run INSERT/UPDATE/DELETE/CREATE/DROP.', schema: { db_path: 'Path to the .db file (relative to workspace, or absolute)', query: 'SQL query to execute', write: 'Allow write queries: INSERT/UPDATE/DELETE/CREATE/DROP (default: false)', max_rows: 'Maximum rows to return for SELECT (default: 100)', }, jsonSchema: { type: 'object', properties: { db_path: { type: 'string', description: 'Path to the SQLite database file' }, query: { type: 'string', description: 'SQL query' }, write: { type: 'boolean', description: 'Allow write operations (default: false)' }, max_rows: { type: 'number', description: 'Max rows returned for SELECT (default: 100)' }, }, required: ['db_path', 'query'], additionalProperties: false, }, execute: async (args: any): Promise => { const dbPath = String(args?.db_path || '').trim(); const query = String(args?.query || '').trim(); if (!dbPath) return { success: false, error: 'db_path is required' }; if (!query) return { success: false, error: 'query is required' }; const workspacePath = getWorkspacePath(args); let resolved: string; if (path.isAbsolute(dbPath)) { resolved = dbPath; } else { resolved = path.resolve(workspacePath, dbPath); if (!isPathInsideDir(workspacePath, resolved)) { return { success: false, error: 'relative db_path must stay inside workspace (use absolute path for shared databases)' }; } } const allowWrite = Boolean(args?.write); const isWrite = WRITE_RE.test(query); if (isWrite && !allowWrite) { return { success: false, error: 'Write query requires write=true. Set write=true to allow INSERT/UPDATE/DELETE/CREATE.' }; } let Database: any; try { const mod = await import('better-sqlite3'); Database = mod.default ?? mod; } catch (err: any) { return { success: false, error: `better-sqlite3 unavailable: ${err.message}` }; } let db: any; try { db = new Database(resolved, { readonly: !allowWrite }); } catch (err: any) { return { success: false, error: `Cannot open database: ${err.message}` }; } try { const maxRows = Math.min(1_000, Math.max(1, Number(args?.max_rows ?? 100))); const stmt = db.prepare(query); if (isWrite || !query.trim().toUpperCase().startsWith('SELECT')) { const info = stmt.run(); return { success: true, stdout: `OK\nRows affected: ${info.changes} | Last insert rowid: ${info.lastInsertRowid}`, data: { changes: info.changes, lastInsertRowid: info.lastInsertRowid }, }; } const rows: any[] = stmt.all().slice(0, maxRows); if (!rows.length) return { success: true, stdout: '(no rows returned)', data: { rows: [] } }; const columns = Object.keys(rows[0]); const table = formatTable(columns, rows.map((r: any) => columns.map(c => r[c]))); let total = rows.length; try { // Wrap query in subquery to get total row count const countRow = db.prepare(`SELECT COUNT(*) AS _cnt FROM (${query})`).get(); total = Number(countRow?._cnt ?? rows.length); } catch {} const note = rows.length < total ? `\n\n[Showing ${rows.length} of ${total} rows. Use max_rows to increase limit.]` : ''; return { success: true, stdout: table + note, data: { columns, row_count: rows.length, total, rows }, }; } catch (err: any) { return { success: false, error: `Query error: ${err.message}` }; } finally { try { db.close(); } catch {} } }, };