From 3d740d92288e0bcfc59fd53e2c309178d94a580a Mon Sep 17 00:00:00 2001 From: Simon KP Date: Tue, 27 Jan 2026 09:06:22 +1100 Subject: [PATCH] feat(checkins): add trigger tool for cron-based check-ins The isolated cron agent sessions don't fire message_received hooks, so the previous approach of intercepting [system] checkins:trigger messages didn't work. This adds a checkins_trigger tool that the cron agent calls directly to trigger check-ins. Also adds checkins_help tool for user guidance. --- extensions/checkins/index.ts | 6 ++ extensions/checkins/src/scheduler.ts | 13 ++- extensions/checkins/src/tools/help.ts | 131 +++++++++++++++++++++++ extensions/checkins/src/tools/trigger.ts | 73 +++++++++++++ 4 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 extensions/checkins/src/tools/help.ts create mode 100644 extensions/checkins/src/tools/trigger.ts diff --git a/extensions/checkins/index.ts b/extensions/checkins/index.ts index c8a8fd4a8..ead93f0e6 100644 --- a/extensions/checkins/index.ts +++ b/extensions/checkins/index.ts @@ -20,6 +20,8 @@ import { createMemberRemoveTool } from "./src/tools/member-remove.js"; import { createMemberListTool } from "./src/tools/member-list.js"; import { createMemberTimezoneTool } from "./src/tools/member-timezone.js"; import { createVacationTool } from "./src/tools/vacation.js"; +import { createTriggerTool } from "./src/tools/trigger.js"; +import { createHelpTool } from "./src/tools/help.js"; // DM handler import import { handleDmResponse, triggerCheckIn } from "./src/dm-handler.js"; @@ -178,6 +180,8 @@ const checkinsPlugin = { createMemberListTool(storage, getDiscordConfig), createMemberTimezoneTool(storage, getDiscordConfig), createVacationTool(storage, getDiscordConfig), + createTriggerTool(storage), + createHelpTool(), ]; }, { @@ -190,6 +194,8 @@ const checkinsPlugin = { "checkins_member_list", "checkins_member_timezone", "checkins_vacation", + "checkins_trigger", + "checkins_help", ], }, ); diff --git a/extensions/checkins/src/scheduler.ts b/extensions/checkins/src/scheduler.ts index 5b5a6c11e..726f77b5b 100644 --- a/extensions/checkins/src/scheduler.ts +++ b/extensions/checkins/src/scheduler.ts @@ -32,6 +32,17 @@ export function getCheckInJobId(memberId: string): string { * @returns CronJobCreate configuration */ export function buildCheckInCronJob(member: Member, team: Team): CronJobCreate { + // Build a clear instruction for the agent to call the trigger tool + const triggerMessage = [ + "SCHEDULED CHECK-IN TRIGGER", + "", + "You must call the checkins_trigger tool with these exact parameters:", + ` memberId: "${member.id}"`, + ` teamId: "${team.id}"`, + "", + "Do not respond with any other message. Just call the tool.", + ].join("\n"); + return { name: `Check-in: ${member.displayName ?? member.discordUserId}`, description: `Daily check-in for team ${team.name}`, @@ -45,7 +56,7 @@ export function buildCheckInCronJob(member: Member, team: Team): CronJobCreate { wakeMode: "now", payload: { kind: "agentTurn", - message: `[system] checkins:trigger:${member.id}:${team.id}`, + message: triggerMessage, deliver: false, }, }; diff --git a/extensions/checkins/src/tools/help.ts b/extensions/checkins/src/tools/help.ts new file mode 100644 index 000000000..89a5ccbd5 --- /dev/null +++ b/extensions/checkins/src/tools/help.ts @@ -0,0 +1,131 @@ +/** + * Help tool for the check-ins extension. + * Provides usage information and available commands. + */ + +import { Type } from "@sinclair/typebox"; + +/** + * Create the help tool for the checkins extension. + */ +export function createHelpTool() { + return { + name: "checkins_help", + description: + "Get help and usage information for the check-ins extension. " + + "Shows available commands, how to set up teams, and how check-ins work.", + parameters: Type.Object({ + topic: Type.Optional( + Type.String({ + description: + "Specific topic: 'teams', 'members', 'schedule', 'vacation', or leave empty for overview", + }), + ), + }), + async execute(_id: string, params: Record) { + const topic = params.topic ? String(params.topic).toLowerCase().trim() : ""; + + if (topic === "teams") { + return { + content: [ + { + type: "text", + text: `**Team Management** + +• **Create a team**: "Create a team called [name] in channel #[channel]" +• **Delete a team**: "Delete the [name] team" +• **List teams**: "Show all teams" or "List teams" + +Teams are linked to a Discord server (configured in clawdbot.json) and post check-ins to a specific channel.`, + }, + ], + }; + } + + if (topic === "members") { + return { + content: [ + { + type: "text", + text: `**Member Management** + +• **Add a member**: "Add @user to [team] with timezone [tz]" +• **Remove a member**: "Remove @user from [team]" +• **List members**: "Show members of [team]" +• **Change timezone**: "Set @user's timezone to [tz]" + +Supported timezone formats: "America/New_York", "EST", "Eastern", "Australia/Melbourne", "AEST", etc. + +Each member can only be on one team per server.`, + }, + ], + }; + } + + if (topic === "schedule") { + return { + content: [ + { + type: "text", + text: `**Check-in Schedule** + +• Default check-in time: 5:00 PM in the member's timezone +• Check-ins are skipped on weekends (Saturday/Sunday in member's timezone) +• Each member gets a DM with 3 questions: + 1. What did you work on today? + 2. What are you planning to work on next? + 3. Any blockers or things you need help with? + +• Completed check-ins are posted to the team's channel +• Manual check-in: DM the bot with "checkin" or "standup"`, + }, + ], + }; + } + + if (topic === "vacation") { + return { + content: [ + { + type: "text", + text: `**Vacation Mode** + +• **Set vacation**: "Set @user on vacation until [date]" +• **End vacation**: "End @user's vacation" + +While on vacation, scheduled check-ins are skipped. Members can still do manual check-ins if they want. + +Date formats: "January 15", "2024-01-15", "next Monday", "in 2 weeks"`, + }, + ], + }; + } + + // Default overview + return { + content: [ + { + type: "text", + text: `**Check-ins Extension Help** + +Daily standup check-ins via Discord DM. Members receive questions at their scheduled time and responses are posted to the team channel. + +**Topics** (ask "checkins help [topic]"): +• \`teams\` - Creating and managing teams +• \`members\` - Adding/removing team members +• \`schedule\` - How check-in scheduling works +• \`vacation\` - Setting vacation mode + +**Quick Start**: +1. Create a team: "Create a team called Engineering in #standups" +2. Add members: "Add @alice to Engineering with timezone America/New_York" +3. Members will receive DMs at 5 PM their time + +**Manual Check-in**: +DM the bot with "checkin" or "standup" to start a check-in anytime.`, + }, + ], + }; + }, + }; +} diff --git a/extensions/checkins/src/tools/trigger.ts b/extensions/checkins/src/tools/trigger.ts new file mode 100644 index 000000000..673348865 --- /dev/null +++ b/extensions/checkins/src/tools/trigger.ts @@ -0,0 +1,73 @@ +/** + * Internal trigger tool for scheduled check-ins. + * Called by the cron agent to initiate check-in flows. + */ + +import { Type } from "@sinclair/typebox"; + +import type { CheckinsStorage } from "../storage.js"; +import { triggerCheckIn } from "../dm-handler.js"; + +/** + * Create the internal trigger tool for cron-based check-ins. + * + * @param storage - CheckinsStorage instance + * @param getAccountId - Function to get Discord account ID + */ +export function createTriggerTool( + storage: CheckinsStorage, + getAccountId?: () => string | undefined, +) { + return { + name: "checkins_trigger", + description: + "Internal tool for scheduled check-ins. Triggers a check-in flow for a specific member. " + + "Only call this when instructed by the cron scheduler.", + parameters: Type.Object({ + memberId: Type.String({ description: "Member UUID from the scheduler" }), + teamId: Type.String({ description: "Team UUID from the scheduler" }), + }), + async execute(_id: string, params: Record) { + const memberId = String(params.memberId).trim(); + const teamId = String(params.teamId).trim(); + + if (!memberId || !teamId) { + return { + content: [{ type: "text", text: "Missing memberId or teamId" }], + isError: true, + }; + } + + const member = storage.getMember(memberId); + if (!member) { + return { + content: [{ type: "text", text: `Member ${memberId} not found (may have been removed)` }], + }; + } + + const team = storage.getTeam(teamId); + if (!team) { + return { + content: [{ type: "text", text: `Team ${teamId} not found (may have been deleted)` }], + }; + } + + try { + await triggerCheckIn(storage, memberId, teamId, getAccountId?.()); + return { + content: [ + { + type: "text", + text: `Check-in triggered for ${member.displayName ?? member.discordUserId}`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: "text", text: `Failed to trigger check-in: ${String(err)}` }], + isError: true, + }; + } + }, + }; +}