.claude/skills/grammy-patterns/SKILL.md
# Grammy Patterns Patterns and conventions for building Telegram bots with the Grammy framework (TypeScript). ## Core Concepts ### Bot Initialization ```typescript import { Bot, Context, session } from "grammy"; import { conversations, createConversation } from "@grammyjs/conversations"; // Extend context with session and conversations type SessionData = { step?: string }; type MyContext = Context & ConversationFlavor & SessionFlavor<SessionData>; const bot = new Bot<MyContext>(process.env
npx skillsauth add YaroslavKomarov/ShedulerBot .claude/skills/grammy-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns and conventions for building Telegram bots with the Grammy framework (TypeScript).
import { Bot, Context, session } from "grammy";
import { conversations, createConversation } from "@grammyjs/conversations";
// Extend context with session and conversations
type SessionData = { step?: string };
type MyContext = Context & ConversationFlavor & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>(process.env.TELEGRAM_BOT_TOKEN!);
bot.use(session({ initial: (): SessionData => ({}) }));
bot.use(conversations());
Conversations are the primary pattern for multi-step dialogs (onboarding, task addition).
import { Conversation, ConversationFlavor } from "@grammyjs/conversations";
type MyConversation = Conversation<MyContext>;
async function onboardingConversation(conversation: MyConversation, ctx: MyContext) {
await ctx.reply("В каком часовом поясе ты живёшь?");
const timezoneMsg = await conversation.wait();
const timezone = timezoneMsg.message?.text ?? "";
await ctx.reply("Расскажи как выглядит твой обычный день...");
const scheduleMsg = await conversation.wait();
// Call LLM to parse schedule
const parsed = await conversation.external(() =>
parseDaySchedule(scheduleMsg.message?.text ?? "")
);
// Confirm with user
await ctx.reply(`Вот что получилось:\n${formatSchedule(parsed)}\n\nВсё верно?`);
const confirm = await conversation.wait();
if (confirm.message?.text?.toLowerCase().includes("да")) {
await conversation.external(() => saveUserProfile(ctx.from!.id, parsed));
await ctx.reply("Готово! Завтра в 9:00 получишь первый план на день.");
}
}
// Register conversation
bot.use(createConversation(onboardingConversation, "onboarding"));
// Enter conversation from command
bot.command("start", async (ctx) => {
const user = await getUserByTelegramId(ctx.from!.id);
if (!user) {
await ctx.conversation.enter("onboarding");
} else {
await ctx.reply("Добро пожаловать обратно!");
}
});
All async side effects (DB calls, LLM calls) inside a conversation MUST use conversation.external(). This ensures replay safety.
// CORRECT
const result = await conversation.external(async () => {
return await db.query("SELECT ...");
});
// WRONG — will break on replay
const result = await db.query("SELECT ...");
bot.on("message:text", async (ctx) => {
const text = ctx.message.text;
const userId = ctx.from!.id;
// Detect intent via LLM
const intent = await detectIntent(userId, text);
switch (intent.type) {
case "add_task":
await ctx.conversation.enter("addTask");
break;
case "mark_done":
await handleMarkDone(ctx, intent.data);
break;
case "show_plan":
await handleShowPlan(ctx);
break;
default:
await ctx.reply("Не понял. Попробуй иначе.");
}
});
Grammy uses HTML or Markdown parse mode:
// HTML (recommended — predictable escaping)
await ctx.reply(
`<b>📋 План на сегодня</b>\n\n` +
`<b>💼 Работа</b> 11:00 – 19:00\n` +
` ⚡ Тесты к авторизации (~2ч)\n` +
` • Код-ревью Антона (~30м)`,
{ parse_mode: "HTML" }
);
// Escape user content before embedding in HTML
import { escapeHtml } from "./utils";
await ctx.reply(`Задача: <b>${escapeHtml(task.title)}</b>`, { parse_mode: "HTML" });
bot.catch((err) => {
const ctx = err.ctx;
console.error(`Error for update ${ctx.update.update_id}:`, err.error);
// Don't crash — log and continue
});
For Railway/Fly.io (long-running): use webhooks in production, polling in development.
if (process.env.NODE_ENV === "production") {
// Express + webhook
const app = express();
app.use(express.json());
app.use(`/webhook/${process.env.TELEGRAM_BOT_TOKEN}`, webhookCallback(bot, "express"));
app.listen(PORT);
} else {
// Long polling for dev
bot.start();
}
Always handle /cancel or unexpected commands inside conversations:
async function addTaskConversation(conversation: MyConversation, ctx: MyContext) {
await ctx.reply("Что нужно сделать?");
const msg = await conversation.waitFor("message:text", {
otherwise: (ctx) => ctx.reply("Пожалуйста, отправь текст."),
});
if (msg.message.text === "/cancel") {
await ctx.reply("Отменено.");
return;
}
// ...
}
Use conversation.external() to store and retrieve state rather than local variables when side effects are involved:
const taskDraft = await conversation.external(async () => {
const draft = await llm.parseTask(userMessage);
await db.saveDraft(userId, draft); // persist in case of restart
return draft;
});
For yes/no confirmations, use inline keyboards:
import { InlineKeyboard } from "grammy";
const keyboard = new InlineKeyboard()
.text("✅ Да, верно", "confirm_yes")
.text("✏️ Поправить", "confirm_edit");
await ctx.reply("Всё верно?", { reply_markup: keyboard });
// In conversation, wait for callback
const callbackCtx = await conversation.waitForCallbackQuery(["confirm_yes", "confirm_edit"]);
await callbackCtx.answerCallbackQuery();
{
"grammy": "^1.x",
"@grammyjs/conversations": "^2.x",
"@grammyjs/types": "^3.x"
}
conversation.external()bot.catch() — never let unhandled errors crash the processbot.start() or webhook setupdevelopment
Verify completed implementation against the plan. Checks that all tasks were fully implemented, nothing was forgotten, code compiles, tests pass, and quality standards are met. Use after "/ai-factory.implement" completes, or when user says "verify", "check work", "did we miss anything".
data-ai
Create a step-by-step implementation plan for a feature or task. Breaks down work into actionable tasks tracked via the task system. Use when user says "plan", "create tasks", "break down", or "make a plan for".
tools
# Supabase TypeScript Patterns Patterns for using Supabase with TypeScript in this project. Uses **service role key** (server-side only). Tables are prefixed `sch_`. ## Client Setup ```typescript // src/db/client.ts import { createClient } from "@supabase/supabase-js"; import type { Database } from "./types"; // generated types export const supabase = createClient<Database>( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, // server-side only, bypasses RLS { auth:
development
Generate professional Agent Skills for Claude Code and other AI agents. Creates complete skill packages with SKILL.md, references, scripts, and templates. Use when creating new skills, generating custom slash commands, or building reusable AI capabilities. Validates against Agent Skills specification.