diff --git a/services/mcp-server/src/lib/chronomind-client.ts b/services/mcp-server/src/lib/chronomind-client.ts index 4c1bdb65..4eabc2ad 100644 --- a/services/mcp-server/src/lib/chronomind-client.ts +++ b/services/mcp-server/src/lib/chronomind-client.ts @@ -260,3 +260,98 @@ export function chronomindAgentActionApprove( ): Promise { return chronomindFetch(`/agent-actions/${actionId}/approve`, { method: 'POST' }, opts); } + +// ── Phase B.2b: Day Planner MCP client functions ──────────────────────── + +export interface ProposedTimer { + label: string; + startTime: string; + endTime: string; + durationMinutes: number; + priority: 'high' | 'medium' | 'low'; + urgency: string; + category?: string; + cascade: string; +} + +export interface PlanDayResponse { + planId: string; + date: string; + proposed: ProposedTimer[]; + overflow: ProposedTimer[]; + totalMinutesPlanned: number; + totalMinutesOverflow: number; + existingTimerCount: number; +} + +export interface PlanNlResponse extends PlanDayResponse { + parsedActivities: Array<{ + label: string; + durationMinutes: number; + priority: string; + category?: string; + }>; + unparsed: string[]; +} + +export function chronomindPlannerPlanDay( + input: { + date: string; + dayStartHour?: number; + dayEndHour?: number; + activities: Array<{ + label: string; + durationMinutes: number; + priority?: string; + category?: string; + }>; + prepTimeMinutes?: number; + }, + opts: ChronoMindClientOptions +): Promise { + return chronomindFetch( + '/planner/plan-day', + { method: 'POST', body: JSON.stringify(input) }, + opts + ); +} + +export function chronomindPlannerPlanNl( + input: { + text: string; + date?: string; + dayStartHour?: number; + dayEndHour?: number; + prepTimeMinutes?: number; + }, + opts: ChronoMindClientOptions +): Promise { + return chronomindFetch('/planner/plan-nl', { method: 'POST', body: JSON.stringify(input) }, opts); +} + +export function chronomindPlannerApply( + input: { planId: string; proposed: ProposedTimer[] }, + opts: ChronoMindClientOptions +): Promise<{ planId: string; created: string[] }> { + return chronomindFetch( + '/planner/apply-plan', + { method: 'POST', body: JSON.stringify(input) }, + opts + ); +} + +export function chronomindPlannerReplan( + input: { + planId: string; + date: string; + keep?: string[]; + drop?: string[]; + add?: Array<{ label: string; durationMinutes: number; priority?: string; category?: string }>; + dayStartHour?: number; + dayEndHour?: number; + prepTimeMinutes?: number; + }, + opts: ChronoMindClientOptions +): Promise { + return chronomindFetch('/planner/replan', { method: 'POST', body: JSON.stringify(input) }, opts); +} diff --git a/services/mcp-server/src/modules/chronomind/chronomind-tools.ts b/services/mcp-server/src/modules/chronomind/chronomind-tools.ts index 2db07841..1576e0aa 100644 --- a/services/mcp-server/src/modules/chronomind/chronomind-tools.ts +++ b/services/mcp-server/src/modules/chronomind/chronomind-tools.ts @@ -23,6 +23,8 @@ import { chronomindAgentActionCreate, chronomindAgentActionsList, chronomindAgentActionApprove, + chronomindPlannerPlanDay, + chronomindPlannerPlanNl, } from '../../lib/chronomind-client.js'; import type { McpToolRequest } from '../tools/types.js'; @@ -362,3 +364,100 @@ registerTool({ return chronomindAgentActionApprove(args.actionId, { token: tokenOf(req), requestId: req.id }); }, }); + +// ══════════════════════════════════════════════════════════════════════════════ +// Phase B.2b — Day Planner MCP tools +// ══════════════════════════════════════════════════════════════════════════════ + +// ── chronomind.planner.planDay ─────────────────────────────────────────────── + +registerTool({ + name: 'chronomind.planner.planDay', + description: + 'Generate a proposed day plan from a list of activities. The engine computes free slots around existing timers, fits activities by priority, assigns urgency and cascade presets, and handles overflow. Returns proposed timers ready for review and apply. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + date: z.string().describe('Target date in YYYY-MM-DD format'), + dayStartHour: z.coerce.number().min(0).max(23).default(8).describe('Day starts at this hour'), + dayEndHour: z.coerce.number().min(1).max(24).default(22).describe('Day ends at this hour'), + activities: z + .array( + z.object({ + label: z.string().min(1).describe('Activity name'), + durationMinutes: z.coerce.number().min(1).max(480).describe('Duration in minutes'), + priority: z.enum(['high', 'medium', 'low']).default('medium'), + category: z.string().optional().describe('Activity category'), + }) + ) + .min(1) + .max(20) + .describe('Activities to schedule'), + prepTimeMinutes: z.coerce + .number() + .min(0) + .max(30) + .default(5) + .describe('Buffer between activities'), + }), + async execute(args, req) { + const opts = { token: tokenOf(req), requestId: req.id }; + + await chronomindAgentActionCreate( + { + id: `aa_${crypto.randomUUID()}`, + actorId: 'mcp-server', + actorType: 'mcp', + toolName: 'chronomind.planner.planDay', + actionType: 'planner.plan_day', + payload: { date: args.date, activityCount: args.activities.length }, + }, + opts + ).catch(() => {}); + + return chronomindPlannerPlanDay(args, opts); + }, +}); + +// ── chronomind.planner.planNl ──────────────────────────────────────────────── + +registerTool({ + name: 'chronomind.planner.planNl', + description: + 'Parse natural language text describing a day plan into structured activities, then generate a proposed day plan. Supports semicolon/newline/then separators, durations (30m, 1h, half hour), priority keywords, and time constraints (after 2pm, before 10am). Returns parsed activities, any unparsed segments, and the proposed timeline. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + text: z + .string() + .min(1) + .max(2000) + .describe( + 'Natural language description of activities, e.g. "Meeting 30m; Deep work 2h; Gym 1h after 2pm"' + ), + date: z.string().optional().describe('Target date in YYYY-MM-DD format (defaults to today)'), + dayStartHour: z.coerce.number().min(0).max(23).default(8).describe('Day starts at this hour'), + dayEndHour: z.coerce.number().min(1).max(24).default(22).describe('Day ends at this hour'), + prepTimeMinutes: z.coerce + .number() + .min(0) + .max(30) + .default(5) + .describe('Buffer between activities'), + }), + async execute(args, req) { + const opts = { token: tokenOf(req), requestId: req.id }; + + await chronomindAgentActionCreate( + { + id: `aa_${crypto.randomUUID()}`, + actorId: 'mcp-server', + actorType: 'mcp', + toolName: 'chronomind.planner.planNl', + actionType: 'planner.plan_nl', + payload: { textLength: args.text.length }, + }, + opts + ).catch(() => {}); + + return chronomindPlannerPlanNl(args, opts); + }, +});