feat(rcal): MI calendar awareness, tags, saved views, MCP server
Phase 1: MI now knows the current date/time and upcoming events (14-day lookahead, 5 events max) via direct Automerge read — no HTTP overhead. Phase 2: Tags (string[] | null) on CalendarEvent for first-class filtering. Saved views (named filter presets) with full CRUD API. Tag filter on GET /api/events via comma-separated AND logic. Demo events seeded with tags. Phase 3: Calendar MCP server (5 tools: cal_list_events, cal_get_event, cal_list_sources, cal_list_views, cal_create_event) registered in .mcp.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f2c3245240
commit
2dd5e764cd
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"rspace-calendar": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/home/jeffe/.claude/mcp-servers/calendar/index.js"],
|
||||||
|
"env": {
|
||||||
|
"RSPACE_BASE_URL": "http://localhost:3000",
|
||||||
|
"RSPACE_DEFAULT_SPACE": "demo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ import { verifyToken, extractToken } from "../../server/auth";
|
||||||
import { renderLanding } from "./landing";
|
import { renderLanding } from "./landing";
|
||||||
import type { SyncServer } from '../../server/local-first/sync-server';
|
import type { SyncServer } from '../../server/local-first/sync-server';
|
||||||
import { calendarSchema, calendarDocId } from './schemas';
|
import { calendarSchema, calendarDocId } from './schemas';
|
||||||
import type { CalendarDoc, CalendarEvent, CalendarSource, ScheduledItemMetadata } from './schemas';
|
import type { CalendarDoc, CalendarEvent, CalendarSource, SavedCalendarView, ScheduledItemMetadata } from './schemas';
|
||||||
|
|
||||||
let _syncServer: SyncServer | null = null;
|
let _syncServer: SyncServer | null = null;
|
||||||
|
|
||||||
|
|
@ -39,9 +39,17 @@ function ensureDoc(space: string): CalendarDoc {
|
||||||
d.meta.spaceSlug = space;
|
d.meta.spaceSlug = space;
|
||||||
d.sources = {};
|
d.sources = {};
|
||||||
d.events = {};
|
d.events = {};
|
||||||
|
d.views = {};
|
||||||
});
|
});
|
||||||
_syncServer!.setDoc(docId, doc);
|
_syncServer!.setDoc(docId, doc);
|
||||||
}
|
}
|
||||||
|
// Backward compat: old docs may lack views
|
||||||
|
if (!doc.views) {
|
||||||
|
_syncServer!.changeDoc<CalendarDoc>(docId, 'add views map', (d) => {
|
||||||
|
d.views = {};
|
||||||
|
});
|
||||||
|
doc = _syncServer!.getDoc<CalendarDoc>(docId)!;
|
||||||
|
}
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +92,7 @@ function eventToRow(ev: CalendarEvent, sources: Record<string, CalendarSource>)
|
||||||
virtual_platform: ev.virtualPlatform,
|
virtual_platform: ev.virtualPlatform,
|
||||||
r_tool_source: ev.rToolSource,
|
r_tool_source: ev.rToolSource,
|
||||||
r_tool_entity_id: ev.rToolEntityId,
|
r_tool_entity_id: ev.rToolEntityId,
|
||||||
|
tags: ev.tags ?? null,
|
||||||
attendees: ev.attendees,
|
attendees: ev.attendees,
|
||||||
attendee_count: ev.attendeeCount,
|
attendee_count: ev.attendeeCount,
|
||||||
metadata: ev.metadata,
|
metadata: ev.metadata,
|
||||||
|
|
@ -163,12 +172,14 @@ function seedDemoIfEmpty(space: string) {
|
||||||
locationId?: string; locationName?: string;
|
locationId?: string; locationName?: string;
|
||||||
locationLat?: number; locationLng?: number; locationGranularity?: string;
|
locationLat?: number; locationLng?: number; locationGranularity?: string;
|
||||||
isVirtual?: boolean; virtualUrl?: string; virtualPlatform?: string;
|
isVirtual?: boolean; virtualUrl?: string; virtualPlatform?: string;
|
||||||
|
tags?: string[];
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
title: "rSpace Launch Party",
|
title: "rSpace Launch Party",
|
||||||
desc: "Celebrating the launch of the unified rSpace platform with all 22 modules live.",
|
desc: "Celebrating the launch of the unified rSpace platform with all 22 modules live.",
|
||||||
start: daysFromNow(-21, 18, 0), end: daysFromNow(-21, 22, 0),
|
start: daysFromNow(-21, 18, 0), end: daysFromNow(-21, 22, 0),
|
||||||
sourceId: communityId, locationName: "Radiant Hall, Pittsburgh",
|
sourceId: communityId, locationName: "Radiant Hall, Pittsburgh",
|
||||||
|
tags: ["launch", "community"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Provider Onboarding Workshop",
|
title: "Provider Onboarding Workshop",
|
||||||
|
|
@ -176,6 +187,7 @@ function seedDemoIfEmpty(space: string) {
|
||||||
start: daysFromNow(-12, 14, 0), end: daysFromNow(-12, 17, 0),
|
start: daysFromNow(-12, 14, 0), end: daysFromNow(-12, 17, 0),
|
||||||
sourceId: communityId, isVirtual: true,
|
sourceId: communityId, isVirtual: true,
|
||||||
virtualUrl: "https://meet.jit.si/rspace-providers", virtualPlatform: "Jitsi",
|
virtualUrl: "https://meet.jit.si/rspace-providers", virtualPlatform: "Jitsi",
|
||||||
|
tags: ["onboarding", "cosmolocal"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Weekly Community Standup",
|
title: "Weekly Community Standup",
|
||||||
|
|
@ -183,12 +195,14 @@ function seedDemoIfEmpty(space: string) {
|
||||||
start: daysFromNow(0, 16, 0), end: daysFromNow(0, 16, 45),
|
start: daysFromNow(0, 16, 0), end: daysFromNow(0, 16, 45),
|
||||||
sourceId: communityId, isVirtual: true,
|
sourceId: communityId, isVirtual: true,
|
||||||
virtualUrl: "https://meet.jit.si/rspace-standup", virtualPlatform: "Jitsi",
|
virtualUrl: "https://meet.jit.si/rspace-standup", virtualPlatform: "Jitsi",
|
||||||
|
tags: ["recurring", "community"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Sprint: Module Seeding & Polish",
|
title: "Sprint: Module Seeding & Polish",
|
||||||
desc: "Focus sprint on populating demo data and improving UX across all modules.",
|
desc: "Focus sprint on populating demo data and improving UX across all modules.",
|
||||||
start: daysFromNow(0, 9, 0), end: daysFromNow(5, 18, 0),
|
start: daysFromNow(0, 9, 0), end: daysFromNow(5, 18, 0),
|
||||||
sourceId: sprintsId, allDay: true,
|
sourceId: sprintsId, allDay: true,
|
||||||
|
tags: ["sprint", "dev"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "rFlows Budget Review",
|
title: "rFlows Budget Review",
|
||||||
|
|
@ -196,6 +210,7 @@ function seedDemoIfEmpty(space: string) {
|
||||||
start: daysFromNow(6, 15, 0), end: daysFromNow(6, 17, 0),
|
start: daysFromNow(6, 15, 0), end: daysFromNow(6, 17, 0),
|
||||||
sourceId: communityId, isVirtual: true,
|
sourceId: communityId, isVirtual: true,
|
||||||
virtualUrl: "https://meet.jit.si/rflows-review", virtualPlatform: "Jitsi",
|
virtualUrl: "https://meet.jit.si/rflows-review", virtualPlatform: "Jitsi",
|
||||||
|
tags: ["governance", "finance"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Cosmolocal Design Sprint",
|
title: "Cosmolocal Design Sprint",
|
||||||
|
|
@ -204,6 +219,7 @@ function seedDemoIfEmpty(space: string) {
|
||||||
sourceId: sprintsId,
|
sourceId: sprintsId,
|
||||||
locationId: berlinLocId, locationName: "Druckwerkstatt Berlin",
|
locationId: berlinLocId, locationName: "Druckwerkstatt Berlin",
|
||||||
locationLat: 52.52, locationLng: 13.405, locationGranularity: "city",
|
locationLat: 52.52, locationLng: 13.405, locationGranularity: "city",
|
||||||
|
tags: ["sprint", "design", "travel"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Q1 Retrospective",
|
title: "Q1 Retrospective",
|
||||||
|
|
@ -211,6 +227,7 @@ function seedDemoIfEmpty(space: string) {
|
||||||
start: daysFromNow(21, 16, 0), end: daysFromNow(21, 18, 0),
|
start: daysFromNow(21, 16, 0), end: daysFromNow(21, 18, 0),
|
||||||
sourceId: communityId, isVirtual: true,
|
sourceId: communityId, isVirtual: true,
|
||||||
virtualUrl: "https://meet.jit.si/rspace-retro", virtualPlatform: "Jitsi",
|
virtualUrl: "https://meet.jit.si/rspace-retro", virtualPlatform: "Jitsi",
|
||||||
|
tags: ["recurring", "community", "retrospective"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -244,6 +261,7 @@ function seedDemoIfEmpty(space: string) {
|
||||||
rToolEntityId: null,
|
rToolEntityId: null,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
attendeeCount: 0,
|
attendeeCount: 0,
|
||||||
|
tags: e.tags || null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
likelihood: null,
|
likelihood: null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
@ -266,12 +284,21 @@ function seedDemoIfEmpty(space: string) {
|
||||||
routes.get("/api/events", async (c) => {
|
routes.get("/api/events", async (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
const dataSpace = c.get("effectiveSpace") || space;
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
const { start, end, source, search, rTool, rEntityId, upcoming } = c.req.query();
|
const { start, end, source, search, rTool, rEntityId, upcoming, tags: tagsParam } = c.req.query();
|
||||||
|
|
||||||
const doc = ensureDoc(dataSpace);
|
const doc = ensureDoc(dataSpace);
|
||||||
let events = Object.values(doc.events);
|
let events = Object.values(doc.events);
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
|
if (tagsParam) {
|
||||||
|
const requiredTags = tagsParam.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
|
||||||
|
if (requiredTags.length > 0) {
|
||||||
|
events = events.filter((e) => {
|
||||||
|
const eTags = (e.tags ?? []).map((t) => t.toLowerCase());
|
||||||
|
return requiredTags.every((rt) => eTags.includes(rt));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
if (start) {
|
if (start) {
|
||||||
const startMs = new Date(start).getTime();
|
const startMs = new Date(start).getTime();
|
||||||
events = events.filter((e) => e.startTime >= startMs);
|
events = events.filter((e) => e.startTime >= startMs);
|
||||||
|
|
@ -322,7 +349,7 @@ routes.post("/api/events", async (c) => {
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { title, description, start_time, end_time, all_day, timezone, source_id, location_id, location_name,
|
const { title, description, start_time, end_time, all_day, timezone, source_id, location_id, location_name,
|
||||||
is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id,
|
is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id,
|
||||||
is_scheduled_item, provenance, item_preview } = body;
|
is_scheduled_item, provenance, item_preview, tags: bodyTags } = body;
|
||||||
if (!title?.trim() || !start_time) return c.json({ error: "Title and start_time required" }, 400);
|
if (!title?.trim() || !start_time) return c.json({ error: "Title and start_time required" }, 400);
|
||||||
|
|
||||||
const docId = calendarDocId(dataSpace);
|
const docId = calendarDocId(dataSpace);
|
||||||
|
|
@ -389,6 +416,7 @@ routes.post("/api/events", async (c) => {
|
||||||
rToolEntityId: r_tool_entity_id || null,
|
rToolEntityId: r_tool_entity_id || null,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
attendeeCount: 0,
|
attendeeCount: 0,
|
||||||
|
tags: Array.isArray(bodyTags) ? bodyTags : null,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
likelihood: null,
|
likelihood: null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
@ -509,6 +537,7 @@ routes.post("/api/import-ics", async (c) => {
|
||||||
rToolEntityId: null,
|
rToolEntityId: null,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
attendeeCount: 0,
|
attendeeCount: 0,
|
||||||
|
tags: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
likelihood: null,
|
likelihood: null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
@ -597,6 +626,7 @@ routes.patch("/api/events/:id", async (c) => {
|
||||||
location_name: 'locationName',
|
location_name: 'locationName',
|
||||||
is_virtual: 'isVirtual',
|
is_virtual: 'isVirtual',
|
||||||
virtual_url: 'virtualUrl',
|
virtual_url: 'virtualUrl',
|
||||||
|
tags: 'tags',
|
||||||
metadata: 'metadata',
|
metadata: 'metadata',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -843,6 +873,106 @@ routes.get("/api/context/:tool", async (c) => {
|
||||||
return c.json({ count: rows.length, results: rows });
|
return c.json({ count: rows.length, results: rows });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── API: Saved Views ──
|
||||||
|
|
||||||
|
function viewToRow(v: SavedCalendarView) {
|
||||||
|
return {
|
||||||
|
id: v.id,
|
||||||
|
name: v.name,
|
||||||
|
icon: v.icon,
|
||||||
|
filter: v.filter,
|
||||||
|
default_view_mode: v.defaultViewMode,
|
||||||
|
created_at: v.createdAt ? new Date(v.createdAt).toISOString() : null,
|
||||||
|
updated_at: v.updatedAt ? new Date(v.updatedAt).toISOString() : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.get("/api/views", async (c) => {
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const doc = ensureDoc(dataSpace);
|
||||||
|
|
||||||
|
const views = Object.values(doc.views ?? {}).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return c.json({ count: views.length, results: views.map(viewToRow) });
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.post("/api/views", async (c) => {
|
||||||
|
const token = extractToken(c.req.raw.headers);
|
||||||
|
if (!token) return c.json({ error: "Authentication required" }, 401);
|
||||||
|
try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
|
||||||
|
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const body = await c.req.json();
|
||||||
|
if (!body.name?.trim()) return c.json({ error: "name required" }, 400);
|
||||||
|
|
||||||
|
const docId = calendarDocId(dataSpace);
|
||||||
|
ensureDoc(dataSpace);
|
||||||
|
const viewId = crypto.randomUUID();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
_syncServer!.changeDoc<CalendarDoc>(docId, `create view ${viewId}`, (d) => {
|
||||||
|
d.views[viewId] = {
|
||||||
|
id: viewId,
|
||||||
|
name: body.name.trim(),
|
||||||
|
icon: body.icon || null,
|
||||||
|
filter: body.filter || {},
|
||||||
|
defaultViewMode: body.default_view_mode || null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = _syncServer!.getDoc<CalendarDoc>(docId)!;
|
||||||
|
return c.json(viewToRow(updated.views[viewId]), 201);
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.patch("/api/views/:id", async (c) => {
|
||||||
|
const token = extractToken(c.req.raw.headers);
|
||||||
|
if (!token) return c.json({ error: "Authentication required" }, 401);
|
||||||
|
try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
|
||||||
|
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const id = c.req.param("id");
|
||||||
|
const body = await c.req.json();
|
||||||
|
|
||||||
|
const docId = calendarDocId(dataSpace);
|
||||||
|
const doc = ensureDoc(dataSpace);
|
||||||
|
if (!doc.views?.[id]) return c.json({ error: "View not found" }, 404);
|
||||||
|
|
||||||
|
_syncServer!.changeDoc<CalendarDoc>(docId, `update view ${id}`, (d) => {
|
||||||
|
const v = d.views[id];
|
||||||
|
if (body.name !== undefined) v.name = body.name.trim();
|
||||||
|
if (body.icon !== undefined) v.icon = body.icon;
|
||||||
|
if (body.filter !== undefined) v.filter = body.filter;
|
||||||
|
if (body.default_view_mode !== undefined) v.defaultViewMode = body.default_view_mode;
|
||||||
|
v.updatedAt = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = _syncServer!.getDoc<CalendarDoc>(docId)!;
|
||||||
|
return c.json(viewToRow(updated.views[id]));
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.delete("/api/views/:id", async (c) => {
|
||||||
|
const token = extractToken(c.req.raw.headers);
|
||||||
|
if (!token) return c.json({ error: "Authentication required" }, 401);
|
||||||
|
try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
|
||||||
|
|
||||||
|
const space = c.req.param("space") || "demo";
|
||||||
|
const dataSpace = c.get("effectiveSpace") || space;
|
||||||
|
const id = c.req.param("id");
|
||||||
|
|
||||||
|
const docId = calendarDocId(dataSpace);
|
||||||
|
const doc = ensureDoc(dataSpace);
|
||||||
|
if (!doc.views?.[id]) return c.json({ error: "View not found" }, 404);
|
||||||
|
|
||||||
|
_syncServer!.changeDoc<CalendarDoc>(docId, `delete view ${id}`, (d) => {
|
||||||
|
delete d.views[id];
|
||||||
|
});
|
||||||
|
return c.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
// ── Page route ──
|
// ── Page route ──
|
||||||
routes.get("/", (c) => {
|
routes.get("/", (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
|
|
@ -860,6 +990,47 @@ routes.get("/", (c) => {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── MI Integration ──
|
||||||
|
|
||||||
|
/** Lean event shape for MI system prompt (minimal token footprint) */
|
||||||
|
export interface MICalendarEvent {
|
||||||
|
title: string;
|
||||||
|
start: string; // ISO date string
|
||||||
|
end: string; // ISO date string
|
||||||
|
allDay: boolean;
|
||||||
|
location: string | null;
|
||||||
|
isVirtual: boolean;
|
||||||
|
tags: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read upcoming events directly from Automerge (no HTTP overhead).
|
||||||
|
* Returns [] if sync server not initialized or no events exist.
|
||||||
|
*/
|
||||||
|
export function getUpcomingEventsForMI(space: string, days = 14, limit = 5): MICalendarEvent[] {
|
||||||
|
if (!_syncServer) return [];
|
||||||
|
const docId = calendarDocId(space);
|
||||||
|
const doc = _syncServer.getDoc<CalendarDoc>(docId);
|
||||||
|
if (!doc) return [];
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const futureMs = now + days * 86400000;
|
||||||
|
|
||||||
|
return Object.values(doc.events)
|
||||||
|
.filter((e) => e.startTime >= now && e.startTime <= futureMs)
|
||||||
|
.sort((a, b) => a.startTime - b.startTime)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map((e) => ({
|
||||||
|
title: e.title,
|
||||||
|
start: new Date(e.startTime).toISOString(),
|
||||||
|
end: e.endTime ? new Date(e.endTime).toISOString() : new Date(e.startTime).toISOString(),
|
||||||
|
allDay: e.allDay,
|
||||||
|
location: e.locationName || null,
|
||||||
|
isVirtual: e.isVirtual,
|
||||||
|
tags: e.tags ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export const calModule: RSpaceModule = {
|
export const calModule: RSpaceModule = {
|
||||||
id: "rcal",
|
id: "rcal",
|
||||||
name: "rCal",
|
name: "rCal",
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,31 @@ export interface CalendarEvent {
|
||||||
rToolEntityId: string | null;
|
rToolEntityId: string | null;
|
||||||
attendees: unknown[];
|
attendees: unknown[];
|
||||||
attendeeCount: number;
|
attendeeCount: number;
|
||||||
|
tags: string[] | null;
|
||||||
metadata: unknown | null;
|
metadata: unknown | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SavedCalendarViewFilter {
|
||||||
|
tags?: string[];
|
||||||
|
sourceIds?: string[];
|
||||||
|
search?: string;
|
||||||
|
locationName?: string;
|
||||||
|
isVirtual?: boolean;
|
||||||
|
rToolSource?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedCalendarView {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string | null;
|
||||||
|
filter: SavedCalendarViewFilter;
|
||||||
|
defaultViewMode: string | null;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CalendarDoc {
|
export interface CalendarDoc {
|
||||||
meta: {
|
meta: {
|
||||||
module: string;
|
module: string;
|
||||||
|
|
@ -67,6 +87,7 @@ export interface CalendarDoc {
|
||||||
};
|
};
|
||||||
sources: Record<string, CalendarSource>;
|
sources: Record<string, CalendarSource>;
|
||||||
events: Record<string, CalendarEvent>;
|
events: Record<string, CalendarEvent>;
|
||||||
|
views: Record<string, SavedCalendarView>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Schema registration ──
|
// ── Schema registration ──
|
||||||
|
|
@ -85,6 +106,7 @@ export const calendarSchema: DocSchema<CalendarDoc> = {
|
||||||
},
|
},
|
||||||
sources: {},
|
sources: {},
|
||||||
events: {},
|
events: {},
|
||||||
|
views: {},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,7 @@ async function executeCalendarEvent(
|
||||||
rToolEntityId: job.id,
|
rToolEntityId: job.id,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
attendeeCount: 0,
|
attendeeCount: 0,
|
||||||
|
tags: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
likelihood: null,
|
likelihood: null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
@ -1087,6 +1088,7 @@ function syncReminderToCalendar(reminder: Reminder, space: string): string | nul
|
||||||
rToolEntityId: reminder.id,
|
rToolEntityId: reminder.id,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
attendeeCount: 0,
|
attendeeCount: 0,
|
||||||
|
tags: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
likelihood: null,
|
likelihood: null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
@ -1696,6 +1698,7 @@ async function executeWorkflowNode(
|
||||||
rToolEntityId: node.id,
|
rToolEntityId: node.id,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
attendeeCount: 0,
|
attendeeCount: 0,
|
||||||
|
tags: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
likelihood: null,
|
likelihood: null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { verifyToken, extractToken } from "./auth";
|
||||||
import type { EncryptIDClaims } from "./auth";
|
import type { EncryptIDClaims } from "./auth";
|
||||||
import { buildModuleCapabilities, MODULE_ROUTES } from "../lib/mi-module-routes";
|
import { buildModuleCapabilities, MODULE_ROUTES } from "../lib/mi-module-routes";
|
||||||
import type { MiAction } from "../lib/mi-actions";
|
import type { MiAction } from "../lib/mi-actions";
|
||||||
|
import { getUpcomingEventsForMI } from "../modules/rcal/mod";
|
||||||
|
|
||||||
const mi = new Hono();
|
const mi = new Hono();
|
||||||
|
|
||||||
|
|
@ -128,10 +129,34 @@ mi.post("/ask", async (c) => {
|
||||||
admin: "+ enable/disable modules, manage members",
|
admin: "+ enable/disable modules, manage members",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Build time + calendar context ──
|
||||||
|
const now = new Date();
|
||||||
|
const weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||||
|
const timeContext = `${weekdays[now.getUTCDay()]}, ${now.toISOString().split("T")[0]} at ${now.toISOString().split("T")[1].split(".")[0]} UTC`;
|
||||||
|
|
||||||
|
let calendarContext = "";
|
||||||
|
if (space) {
|
||||||
|
const upcoming = getUpcomingEventsForMI(space);
|
||||||
|
if (upcoming.length > 0) {
|
||||||
|
const lines = upcoming.map((e) => {
|
||||||
|
const date = e.allDay ? e.start.split("T")[0] : e.start;
|
||||||
|
let line = `- ${date}: ${e.title}`;
|
||||||
|
if (e.location) line += ` (${e.location})`;
|
||||||
|
else if (e.isVirtual) line += ` (virtual)`;
|
||||||
|
if (e.tags?.length) line += ` [${e.tags.join(", ")}]`;
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
calendarContext = `\n- Upcoming events (next 14 days):\n${lines.join("\n")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const systemPrompt = `You are mi (mycelial intelligence), the intelligent assistant for rSpace — a self-hosted, community-run platform.
|
const systemPrompt = `You are mi (mycelial intelligence), the intelligent assistant for rSpace — a self-hosted, community-run platform.
|
||||||
You help users navigate, understand, and get the most out of the platform's apps (rApps).
|
You help users navigate, understand, and get the most out of the platform's apps (rApps).
|
||||||
You understand the full context of what the user has open and can guide them through setup and usage.
|
You understand the full context of what the user has open and can guide them through setup and usage.
|
||||||
|
|
||||||
|
## Current Date & Time
|
||||||
|
${timeContext}
|
||||||
|
|
||||||
## Your Caller's Role: ${callerRole} in space "${space || "none"}"
|
## Your Caller's Role: ${callerRole} in space "${space || "none"}"
|
||||||
- viewer: ${rolePermissions.viewer}
|
- viewer: ${rolePermissions.viewer}
|
||||||
- member: ${rolePermissions.member}
|
- member: ${rolePermissions.member}
|
||||||
|
|
@ -148,7 +173,7 @@ ${moduleCapabilities}
|
||||||
When the user asks to create a social media campaign, use create-content with module rsocials, contentType campaign, body.rawBrief set to the user's description, body.navigateToWizard true.
|
When the user asks to create a social media campaign, use create-content with module rsocials, contentType campaign, body.rawBrief set to the user's description, body.navigateToWizard true.
|
||||||
|
|
||||||
## Current Context
|
## Current Context
|
||||||
${contextSection}
|
${contextSection}${calendarContext}
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
- Be concise and helpful. Keep responses short (2-4 sentences) unless the user asks for detail.
|
- Be concise and helpful. Keep responses short (2-4 sentences) unless the user asks for detail.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue