К основному содержанию
скилл CX Codex

extension-email-calendar-events — скилл для Claude Code: события

Скилл добавляет в приложение календарные события и рассылку iCalendar-приглашений по почте. Пригодится, если нужны встречи с гостями без ручной сборки ICS.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add caffeinelabs/skills --skill extension-email-calendar-events -a claude-code
Открыть на GitHub

Что делает extension-email-calendar-events

Скилл закрывает работу с событиями и встречами: создание, обновление, отмена, удаление, чтение и список. Внутри — готовый модуль mo:caffeineai-email-calendar-events/calendarEvents.mo, менять его нельзя. Он хранит события в состоянии, а на каждое письмо собирает и прикладывает файл iCalendar.

Отправка идёт через sendCalendarEvent. Одна копия приглашения на каждого гостя, через почтовый шлюз с настройками, которые платформа подставляет при деплое: INTEGRATIONS_GATEWAY_URL и INTEGRATIONS_GATEWAY_API_KEY. Без них функция вернёт #err. Организатор — это сам отправитель: в CalendarEvent.organizer лежит только отображаемое имя, а адрес собирается как <fromUsername>@<app domain>. Полный адрес передавать нельзя, домен приложение не читает и не угадывает.

Учтите: приём ответов от гостей (RSVP) пока не поддержан, так что подтверждения придётся собирать отдельно. Ещё момент с последовательностью: update, addAttendees, removeAttendees и cancel сохраняют изменение со следующим номером sequence и возвращают запись — отправляйте именно её, потому что календари применяют обновление или отмену только при sequence выше своего.

Какие задачи решает

  • создать встречу с гостями и разослать приглашения
  • обновить время или место события и уведомить участников
  • отменить событие и разослать отмену
  • добавить или убрать гостя из списка участников
  • получить список событий в прямом или обратном порядке

Примеры запросов агенту

  • добавь календарное событие на завтра и разошли приглашения гостям
  • обнови время встречи и отправь новую версию приглашения
  • отмени событие и разошли отмену всем участникам

Как установить extension-email-calendar-events

1Через Skills CLI (Vercel)

npx skills add caffeinelabs/skills --skill extension-email-calendar-events -a claude-code

2Вручную: положить файлы

~/.claude/skills/extension-email-calendar-events/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/extension-email-calendar-events/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Из того же набора25

extension-email-marketing Добавляет в приложение на Caffeine AI маркетинговые письма по темам: подписчики, токены отписки и страница /unsubscribe на своём фронтенде. CX Codex +1 28 100установок extension-camera Добавляет в агент правила работы с веб-камерой через React-хук useCamera. Пригодится для съёмки фото и видеопревью в браузере. CX Codex +1 28 100установок extension-qr-code Добавляет в React-приложение сканирование QR-кодов через камеру устройства. Пригодится, когда нужно считать код прямо в браузере. CX Codex +1 28 200установок extension-stripe Добавляет в проект на Caffeine AI оплату картами через Stripe: создание checkout-сессии и проверку её статуса. Пригодится, когда нужен приём платежей внутри канистры. CX Codex +1 28 200установок extension-posting-to-x Скилл для сборок Caffeine, которые публикуют твиты в X. Задаёт единственный поддерживаемый путь через пакет x-client и OAuth 2.0 PKCE. CX Codex +1 28 100установок extension-openai Задаёт правила интеграции OpenAI в канистры Motoko через пакет openai-client. Защищает API-ключ от утечки при реплицированных вызовах. CX Codex +1 28 100установок extension-email-raw Скилл добавляет в проект отправку одного письма сразу нескольким получателям через to, cc и bcc. Пригодится, когда нужно разослать уведомление участникам встречи. CX Codex +1 28 100установок extension-invite-links Скилл добавляет в проект систему приглашений и сбор ответов RSVP. Гости отправляют подтверждения по кодам без авторизации, а администратор видит сводку через вход в систему. CX Codex +1 28 100установок

Похожие

calendar-automation claude-office-skills Автоматизирует работу с Google Calendar и Outlook: выделяет слоты под фокусную работу, собирает утренние сводки в Slack и готовит данные к встречам. CX Codex +1 6 100установок pr-calendar elvisun Скилл превращает фид PR-календаря Medialyst в план для конкретного бренда: отбирает моменты, считает сроки питчинга назад от даты события и отсеивает неуместные поводы. CX Codex +1 696установок email-smtp-send tiangong-ai Отправляет письма через SMTP с локальными вложениями и сохраняет копию в папку «Отправленные» через IMAP. Пригодится агенту для надёжной почтовой рассылки и отчётов. CX Codex +1 6 800установок gws-calendar официальный googleworkspace Скилл помогает управлять событиями и календарями Google Calendar прямо через утилиту gws. CX Codex +1 61 200установок pipefy-members-email-webhooks pipefy Скилл для работы с Pipefy через агента: приглашает участников в пайп, отправляет письма из инбокса карточки и управляет вебхуками. CX Codex +1 669установок catalyst-calendar официальный anthropics Собирает календарь будущих событий для компаний и отраслей: отчётность, конференции, запуски, решения регуляторов и макроданные. CX Codex +1 1 100установок

Рядом в теме «Бэкенд и API»

Всё по теме «Бэкенд и API» →

Вопросы о extension-email-calendar-events

Как установить extension-email-calendar-events в Claude?

Выполните npx skills add caffeinelabs/skills --skill extension-email-calendar-events -a claude-code. Скилл появится в ~/.claude/skills/extension-email-calendar-events/SKILL.md.

Что делает extension-email-calendar-events?

Даёт CRUD для календарных событий и рассылку iCalendar-приглашений по почте. На каждое письмо прикладывается собранный файл iCalendar.

Приходят ли ответы гостей обратно в приложение?

Нет, приём RSVP пока не поддержан. Приглашения уходят, но подтверждения от участников приложение не получает.

Исходный текст

SKILL.md · Apache-2.0
---
name: extension-email-calendar-events
description: Support for organising events/meetings and sending invitations by email.
version: 0.4.1
compatibility:
  mops:
    caffeineai-email-calendar-events: "~0.4.0"
    caffeineai-email: "~0.4.0"
    caffeineai-authorization: "~1.0.1"
caffeineai-subscription: [plus, pro]
---

# Email — Calendar Events
Calendar events email extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).

## Overview

This skill adds support for organising events/meetings and sending iCalendar invitations by email. It provides CRUD operations for calendar events and email-based invitation delivery.

# Backend

## This component is for organising events/meetings and sending invitations by email

- Internally it builds and attaches an iCalendar file to each attendees's email
- This component does not yet support receiving RSVPs from attendees

### To add/update/cancel/delete/get/list calendar events

- Use the prefabricated module `mo:caffeineai-email-calendar-events/calendarEvents.mo` which cannot be modified.

```mo:caffeineai-email-calendar-events/calendarEvents.mo
module {
  public type CalendarEvent = {
    uid : Text;
    sequence : Nat32;
    method : CalendarEventMethod;
    summary : Text;
    description : Text;
    location : Text;
    startTime : Nat64;
    endTime : Nat64;
    organizer : Organizer;
    attendees : [Attendee];
  };

  public type CalendarEventMethod = {
    #request;
    #publish;
    #cancel;
  };

  // The organizer is the sender: the invitation comes from
  // `<fromUsername>@<app domain>` and that mailbox, under this display name,
  // is the ICS ORGANIZER and the first guest. Only attendees carry addresses.
  public type Organizer = {
    name : ?Text;
  };

  public type Mailbox = {
    email : Text;
    name : ?Text;
  };

  public type Attendee = {
    who : Mailbox;
    role : CalendarEventRole;
  };

  public type CalendarEventRole = {
    #chair;
    #required;
    #optional;
    #notParticipating;
  };

  public type State = {
    var events : List.List<CalendarEvent>;
    var uidMap : Map.Map<Text, Nat>;
  };

  public func add(
    self : State,
    uid : Text,
    summary : Text,
    description : Text,
    location : Text,
    startTime : Nat64,
    endTime : Nat64,
    organizer : Organizer,
    attendees : [Attendee]
  ) : ?CalendarEvent;

  // update, addAttendees, removeAttendees and cancel store the change with
  // the next sequence number and return the stored record. Send that record:
  // calendars apply an update or a cancellation only when its sequence is
  // higher than the one they hold.
  public func update(
    self : State,
    uid : Text,
    summary : ?Text,
    description : ?Text,
    location : ?Text,
    startTime : ?Nat64,
    endTime : ?Nat64,
    organizer : ?Organizer,
    attendees : ?[Attendee]
  ) : ?CalendarEvent;

  public func addAttendees(
    self : State,
    uid : Text,
    attendees : [Attendee]
  ) : ?CalendarEvent;

  public func removeAttendees(
    self : State,
    uid : Text,
    attendees : [Text]
  ) : ?CalendarEvent;

  public func cancel(self : State, uid : Text) : ?CalendarEvent;

  public func delete(self : State, uid : Text);

  public func get(self : State, uid : Text) : ?CalendarEvent;

  // Iterate over all calendar events older to newer
  public func iter(self : State) : Iter.Iter<CalendarEvent>;

  // Iterate over all calendar events newer to older
  public func reverse(self : State) : Iter.Iter<CalendarEvent>;

  // One invitation copy per attendee from `<fromUsername>@<app domain>`, the
  // organizer's mailbox, through the mail gateway.
  public func sendCalendarEvent(fromUsername : Text, event : CalendarEvent) : async { #ok; #err : Text };
}
```

### For sending calendar event invitations to attendees by email

- Use the `sendCalendarEvent` function of the same module. It sends one copy per attendee through the mail gateway with the settings the platform injects on every deploy (`INTEGRATIONS_GATEWAY_URL` / `INTEGRATIONS_GATEWAY_API_KEY`); without them it returns `#err`. `caffeineai-email` is a dependency of this package, and the app does not call it for calendar mail.
- `fromUsername` is the local part of the sender only, such as `events` or `no-reply`. The invitation comes from `<fromUsername>@<app domain>`, and the app domain is the project's own, resolved by the mail gateway. Never pass a full address and never read or guess the domain in the app.
- The organizer is that same mailbox. `CalendarEvent.organizer` carries only a display name, `{ name = ?"Community Team" }`, shown as the ICS `ORGANIZER` and the first guest, so calendars see the invitation arrive from its organizer. Attendees are the only mailboxes with addresses; the organizer receives a copy only when also listed as an attendee. Do not model the organizer as a user with an email address.
- It returns `#ok` once the invitation was accepted for every attendee, otherwise `#err(error)` with the error text. A failed send never throws.

### Example usage for an app which can add/update/cancel/delete/get/list calendar events and send invitations to them by email

```motoko filepath=src/backend/main.mo
import Runtime "mo:core/Runtime";
import Principal "mo:core/Principal";
import Map "mo:core/Map";
import Random "mo:core/Random";
import Set "mo:core/Set";
import Iter "mo:core/Iter";
import Option "mo:core/Option";
import Text "mo:core/Text";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import CalendarEvents "mo:caffeineai-email-calendar-events/calendarEvents";
import Uuid "mo:caffeineai-email-calendar-events/uuid";

actor {
  public type UserProfile = {
    name : Text;
    email : Text;
  };

  // Include authorization component
  let accessControlState : AccessControl.AccessControlState;
  include MixinAuthorization(accessControlState, null);

  // Store a map of caller principal to UserProfile
  var userProfiles : Map.Map<Principal, UserProfile>;

  // Store a set of emails for uniqueness check
  var emails : Set.Set<Text>;

  // Store the calendar events
  let calendarEvents : CalendarEvents.State;

  public shared ({ caller }) func registerUser(name : Text, email : Text) : async () {
    // Check if the user already exists
    if (userProfiles.containsKey(caller)) {
      Runtime.trap("User already registered");
    };

    // Check if the email is already used
    if (emails.contains(email)) {
      Runtime.trap("Email already taken");
    };

    // Add a user record
    userProfiles.add(
      caller,
      {
        name;
        email;
      }
    );
    emails.add(email);
  };

  public shared ({ caller }) func addCalendarEvent(summary : Text, description : Text, location : Text, startTimeMs : Nat64, endTimeMs : Nat64) : async () {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can add calendar events");
    };

    let organiser = userProfiles.get(caller)
      ?? Runtime.trap("Admin profile not found");

    let seed = await Random.blob();
    let uid = Uuid.generateV4(seed);

    if (
      calendarEvents.add(
        uid,
        summary,
        description,
        location,
        startTimeMs,
        endTimeMs,
        { name = ?organiser.name },
        userProfiles.values().map(
          func({ name; email }) {
            {
              who = { name = ?name; email };
              role = #required;
            };
          }
        ).toArray()
      ).isNull()
    ) {
      Runtime.trap("Failed to add calendar event");
    };
  };

  public shared ({ caller }) func updateEventDetails(uid : Text, summary : ?Text, description : ?Text, location : ?Text, startTimeMs : ?Nat64, endTimeMs : ?Nat64) : async () {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can update calendar events");
    };

    if (
      calendarEvents.update(
        uid,
        summary,
        description,
        location,
        startTimeMs,
        endTimeMs,
        null,
        null
      ).isNull()
    ) {
      Runtime.trap("Failed to update calendar event");
    };
  };

  public shared ({ caller }) func addEventAttendees(uid : Text, attendees : [CalendarEvents.Attendee]) : async () {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can add calendar event attendees");
    };

    if (calendarEvents.addAttendees(uid, attendees).isNull()) {
      Runtime.trap("Failed to add attendees to calendar event");
    };
  };

  public shared ({ caller }) func removeEventAttendees(uid : Text, attendees : [Text]) : async () {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can remove calendar event attendees");
    };

    if (calendarEvents.removeAttendees(uid, attendees).isNull()) {
      Runtime.trap("Failed to remove attendees from calendar event");
    };
  };

  public shared ({ caller }) func cancelEvent(uid : Text) : async () {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can cancel calendar events");
    };

    // The returned record carries method #cancel and the bumped sequence,
    // which is what tells the attendees' calendars to drop the event.
    let cancelled = calendarEvents.cancel(uid)
      ?? Runtime.trap("Failed to cancel calendar event");

    switch (await CalendarEvents.sendCalendarEvent("events", cancelled)) {
      case (#ok) {};
      case (#err(error)) {
        Runtime.trap("Failed to send the cancellation: " # error);
      };
    };
  };

  public shared ({ caller }) func listEvents() : async [CalendarEvents.CalendarEvent] {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can list calendar events");
    };

    calendarEvents.iter().toArray();
  };

  public shared ({ caller }) func deleteEvent(uid : Text) : async () {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can delete calendar events");
    };

    calendarEvents.delete(uid);
  };

  public shared ({ caller }) func sendEventInvitation(uid : Text) : async () {
    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
      Runtime.trap("Unauthorized: Only admins can send calendar event invitations");
    };

    let event = calendarEvents.get(uid)
      ?? Runtime.trap("Calendar event not found");

    switch (await CalendarEvents.sendCalendarEvent("events", event)) {
      case (#ok) {};
      case (#err(error)) {
        Runtime.trap("Failed to send the invitation: " # error);
      };
    };
  };
};
```

The migration chain head:

```motoko filepath=src/backend/migrations/00000000_000000.mo
import Map "mo:core/Map";
import Set "mo:core/Set";
import AccessControl "mo:caffeineai-authorization/access-control";
import CalendarEvents "mo:caffeineai-email-calendar-events/calendarEvents";

module {
  type UserProfile = {
    name : Text;
    email : Text;
  };

  type NewActor = {
    accessControlState : AccessControl.AccessControlState;
    var userProfiles : Map.Map<Principal, UserProfile>;
    var emails : Set.Set<Text>;
    calendarEvents : CalendarEvents.State;
  };

  public func migration(_old : {}) : NewActor {
    {
      accessControlState = AccessControl.initState();
      var userProfiles = Map.empty<Principal, UserProfile>();
      var emails = Set.empty<Text>();
      calendarEvents = CalendarEvents.new();
    };
  };
};
```

## Upgrading from the previous version

An app built with `caffeineai-email-calendar-events` 0.3.x re-pins with `mops add [email protected]` and `mops add [email protected]`. Nothing in the app code changes: invitations now always go through the mail gateway, and `sendCalendarEvent` returns `#err` instead of falling back to the transport canister when the gateway settings are missing.

An app built with `caffeineai-email-calendar-events` 0.2.x re-pins with `mops add [email protected]` and `mops add [email protected]`. `CalendarEvent.organizer` is now an `Organizer` (`{ name : ?Text }`) instead of a `Mailbox`: the organizer address is the sender's, `<fromUsername>@<app domain>`, built by the mail gateway, so the app no longer supplies one. `add` and `update` take the new type, and an app that stored events adds a migration step that maps each stored `organizer` to `{ name = old.organizer.name }`.

An app built with `caffeineai-email-calendar-events` 0.1.x re-pins with `mops add [email protected]` and `mops add [email protected]`. `sendCalendarEvent` keeps its signature but moves from `EmailClient` to this module (`CalendarEvents.sendCalendarEvent`), and the event types (`CalendarEvent`, `Attendee`, `Mailbox`, `CalendarEventMethod`, `CalendarEventRole`) are this module's; an app that imported them from `EmailClient` switches the import and drops `EmailClient` when nothing else uses it. Invitations are now composed and sent by the mail gateway. `update`, `addAttendees`, `removeAttendees` and `cancel` now return the stored record (with the bumped sequence) instead of the previous one, so an app that sends the returned record after a change sends the right sequence.

© caffeinelabs · Apache-2.0 · caffeinelabs/skills

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой