Merge pull request #32 from techops-services/fix/checkins-cron-trigger

fix(checkins): use tool-based cron triggers + add help command
This commit is contained in:
Simon KP 2026-01-27 09:11:51 +11:00 committed by GitHub
commit 9e9aea6482
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 225 additions and 1 deletions

View File

@ -88,6 +88,9 @@
"ackReaction": "👀",
"inbound": {
"debounceMs": 2000
},
"groupChat": {
"mentionPatterns": ["@tobias"]
}
},
"skills": {

View File

@ -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",
],
},
);

View File

@ -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,
},
};

View File

@ -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<string, unknown>) {
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.`,
},
],
};
},
};
}

View File

@ -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<string, unknown>) {
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,
};
}
},
};
}