Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
138 lines
5.5 KiB
JavaScript
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 };
|
|
}
|
|
};
|