godcrm/backend/services/agent-tools/calendar-tools.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

138 lines
5.5 KiB
JavaScript

/**
* Calendar & Search Tool Handlers (ADR-144 P3)
*
* Handles: list_events, create_event, update_event, delete_event, global_search, list_spaces
*/
import { dbGet, dbRun, dbAll, isPostgres, sqlNow } from '../../database/connection.js';
import { parseRowData } from './data-tools.js';
export const calendarToolHandlers = {
async list_events({ start_date, end_date, space_id, limit = 100 }) {
const pg = isPostgres();
let paramIdx = 1;
let query = 'SELECT * FROM calendar_events WHERE 1=1';
const params = [];
if (space_id) {
query += pg ? ` AND space_id = $${paramIdx++}` : ' AND space_id = ?';
params.push(space_id);
}
if (start_date) {
query += pg ? ` AND start_time >= $${paramIdx++}` : ' AND start_time >= ?';
params.push(start_date);
}
if (end_date) {
query += pg ? ` AND start_time <= $${paramIdx++}` : ' AND start_time <= ?';
params.push(end_date);
}
query += ' ORDER BY start_time ASC';
query += pg ? ` LIMIT $${paramIdx++}` : ' LIMIT ?';
params.push(limit);
const events = await dbAll(query, params);
return { events, total: events.length };
},
async create_event({ title, start, end, description = '', space_id, all_day = false }, userId) {
if (!title || !start) return { error: 'title and start are required' };
const result = await dbRun(`
INSERT INTO calendar_events (title, description, start_time, end_time, all_day, space_id, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})
`, [title, description, start, end || null, all_day ? 1 : 0, space_id || null, userId || 1]);
return { success: true, event_id: result.lastInsertRowid, message: `Event "${title}" created` };
},
async update_event({ event_id, title, start, end, description, all_day }) {
const event = await dbGet('SELECT * FROM calendar_events WHERE id = ?', [event_id]);
if (!event) return { error: `Event ${event_id} not found` };
const updates = [];
const params = [];
if (title !== undefined) { updates.push('title = ?'); params.push(title); }
if (start !== undefined) { updates.push('start_time = ?'); params.push(start); }
if (end !== undefined) { updates.push('end_time = ?'); params.push(end); }
if (description !== undefined) { updates.push('description = ?'); params.push(description); }
if (all_day !== undefined) { updates.push('all_day = ?'); params.push(all_day ? 1 : 0); }
if (updates.length === 0) return { error: 'No fields to update' };
updates.push(`updated_at = ${sqlNow()}`);
params.push(event_id);
await dbRun(`UPDATE calendar_events SET ${updates.join(', ')} WHERE id = ?`, params);
return { success: true, message: `Event ${event_id} updated` };
},
async delete_event({ event_id }) {
const event = await dbGet('SELECT id, title FROM calendar_events WHERE id = ?', [event_id]);
if (!event) return { error: `Event ${event_id} not found` };
await dbRun('DELETE FROM calendar_events WHERE id = ?', [event_id]);
return { success: true, message: `Event "${event.title}" deleted` };
},
async global_search({ query, types, space_id, limit = 20, include_archived = false }) {
if (!query) return { error: 'query is required' };
const pg = isPostgres();
const searchPattern = `%${query}%`;
const results = {};
const searchTypes = types || ['tables', 'rows', 'projects', 'conversations'];
if (searchTypes.includes('tables')) {
const tables = await dbAll(
pg ? 'SELECT id, name, icon, description FROM universal_tables WHERE name ILIKE $1 LIMIT $2'
: 'SELECT id, name, icon, description FROM universal_tables WHERE name LIKE ? LIMIT ?',
[searchPattern, limit]
);
results.tables = tables;
}
if (searchTypes.includes('rows')) {
// Archived rows are excluded by default so dead docs don't pollute
// agent context (collegium decision 2026-07-04: archived over tombstone).
const archivedFilter = include_archived ? '' : (pg
? ` AND (data->>'status' IS DISTINCT FROM 'archived')`
: ` AND json_extract(data, '$.status') IS NOT 'archived'`);
const rows = await dbAll(
pg ? `SELECT id, table_id, LEFT(data::text, 200) as data_preview FROM table_rows WHERE data::text ILIKE $1${archivedFilter} LIMIT $2`
: `SELECT id, table_id, substr(data, 1, 200) as data_preview FROM table_rows WHERE data LIKE ?${archivedFilter} LIMIT ?`,
[searchPattern, limit]
);
results.rows = rows;
}
if (searchTypes.includes('projects')) {
let q = pg
? 'SELECT id, name, icon, description FROM projects WHERE name ILIKE $1'
: 'SELECT id, name, icon, description FROM projects WHERE name LIKE ?';
const p = [searchPattern];
if (space_id) {
q += pg ? ` AND space_id = $2` : ' AND space_id = ?';
p.push(space_id);
}
q += pg ? ` LIMIT $${p.length + 1}` : ' LIMIT ?';
p.push(limit);
results.projects = await dbAll(q, p);
}
if (searchTypes.includes('conversations')) {
const convs = await dbAll(
pg ? 'SELECT id, title, type FROM conversations WHERE title ILIKE $1 LIMIT $2'
: 'SELECT id, title, type FROM conversations WHERE title LIKE ? LIMIT ?',
[searchPattern, limit]
);
results.conversations = convs;
}
return { query, results };
},
async list_spaces() {
const spaces = await dbAll('SELECT id, name, type, icon, created_at FROM spaces ORDER BY id');
return { spaces, total: spaces.length };
}
};