
Feishu Calendar
- 1 installs
- 20 repo stars
- Updated April 11, 2026
- autogame-17/feishu-skills
feishu-calendar is a Claude skill that manages Feishu (Lark) calendars: listing, searching, syncing events, creating entries from task requests, and setting up shared project calendars.
About
This skill manages Feishu (Lark) calendars through Node scripts. It lists and searches calendars, syncs events to local state, creates calendar entries (including from task requests), and sets up shared project calendars with members and roles. A task-marking protocol triggers on phrases like 'Mark this task' or 'Remind me to' and creates an event with the requester as attendee and a default 1-hour duration. It requires feishu-common with valid app credentials.
- Lists, searches, and syncs Feishu (Lark) calendars and creates events from task requests via Node scripts
- Sets up shared project calendars with members and roles, and marks tasks with a 1-hour default duration
Feishu Calendar by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,982 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
feishu-calendar capabilities & compatibility
- Capabilities
- feishu batch sender · feishu attendance
- Use cases
- project management
- Runs
- Runs locally
What feishu-calendar says it does
Manage Feishu (Lark) calendars including listing, searching, syncing events, creating calendar entries from task requests, and setting up shared project calendars.
Run `create.js` with `--summary "Task: <Title>"`, `--attendees` set to the requester's ID, and a 1-hour default duration.
npx skills add https://github.com/autogame-17/feishu-skills --skill feishu-calendarAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 20 |
| Last updated | April 11, 2026 |
| Repository | autogame-17/feishu-skills ↗ |
What it does
List, search, sync, and create Feishu calendar events, including task reminders and shared project calendars.
When should I use this skill?
When a user wants to manage Feishu calendars, create events, mark a task, or set up a shared project calendar.
What you get
- Created Feishu calendar events and shared project calendars
By the numbers
- 6 calendar scripts (list, search, check, sync, create, setup shared)
- 1-hour default task duration
Files
feishu-calendar
Manage Feishu (Lark) calendars -- list, search, sync events, create entries, and set up shared calendars.
Prerequisites
feishu-commoninstalled with validFEISHU_APP_IDandFEISHU_APP_SECRET.
Commands
List Calendars
Check available calendars and their IDs.
node skills/feishu-calendar/list_test.jsSearch Calendar
Find a calendar by name/summary.
node skills/feishu-calendar/search_cal.jsCheck Master's Calendar
Specific check for the Master's calendar status.
node skills/feishu-calendar/check_master.jsSync Events
Sync calendar events to local state/memory:
node skills/feishu-calendar/sync_routine.jsCreate Event
node skills/feishu-calendar/create.js --summary "Task: <Title>" --desc "<Context>" --start "<ISO>" --end "<ISO+1h>" --attendees "<User_ID>"Setup Shared Calendar
Create a shared project calendar and add members:
node skills/feishu-calendar/setup_shared.js --name "Project Name" --desc "Description" --members "ou_1,ou_2" --role "writer"Task Marking Protocol
Trigger: User says "Mark this task" or "Remind me to...".
1. Extract date/time from the request (e.g., "Feb 4th" becomes YYYY-02-04). 2. Run create.js with --summary "Task: <Title>", --attendees set to the requester's ID, and a 1-hour default duration.
{
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
"slug": "feishu-calendar",
"version": "1.0.0",
"publishedAt": 1770118148570
}{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "feishu-calendar",
"installedVersion": "1.0.0",
"installedAt": 1770561506797
}
node_modules
.env
package-lock.json
const CalendarManager = require('./lib/CalendarManager');
const { getTimestampCST } = require('./time-helper');
const manager = new CalendarManager();
(async () => {
try {
const calendar = await manager.getCalendar('Master');
if (!calendar) {
console.log("No accessible calendars found.");
return;
}
console.log(`Using Calendar: ${calendar.summary} (ID: ${calendar.calendar_id})`);
// List events for next 3 days
const events = await manager.listEvents(calendar.calendar_id, Date.now(), Date.now() + 86400 * 3000, 50);
if (events && events.length > 0) {
console.log(`Found ${events.length} events.`);
events.forEach(e => console.log(`- [${getTimestampCST(e.start_time.timestamp * 1000)}] ${e.summary}`));
} else {
console.log("No events found.");
}
} catch (e) {
console.error("Error in check_master:", e.message);
}
})();
const { Client } = require('@larksuiteoapi/node-sdk');
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
if (!APP_ID || !APP_SECRET) {
console.error('Error: FEISHU_APP_ID or FEISHU_APP_SECRET not found in .env');
process.exit(1);
}
const client = new Client({ appId: APP_ID, appSecret: APP_SECRET });
async function checkCalendar(hours = 24) {
console.log(`Checking primary calendar for events in next ${hours} hours...`);
try {
// 1. List calendars to find primary
const listRes = await client.calendar.calendar.list();
if (listRes.code !== 0) {
throw new Error(`Failed to list calendars: ${listRes.msg}`);
}
const calendars = listRes.data.calendar_list;
if (!calendars || calendars.length === 0) {
console.log('No calendars found.');
return;
}
// Assume first calendar is primary or look for "primary" flag
let primaryCal = calendars[0];
let calendarId = primaryCal.calendar_id;
const calendarArgIndex = process.argv.indexOf('--calendar');
if (calendarArgIndex !== -1 && calendarArgIndex + 1 < process.argv.length) {
const requestedId = process.argv[calendarArgIndex + 1];
const found = calendars.find(c => c.calendar_id === requestedId);
if (found) {
primaryCal = found;
calendarId = requestedId;
} else {
console.warn(`Calendar ID ${requestedId} not found in list. Using default: ${primaryCal.summary}`);
}
}
console.log(`Using Calendar: ${primaryCal.summary} (ID: ${calendarId})`);
// 2. List events
const now = Math.floor(Date.now() / 1000);
const endTime = now + (hours * 3600);
let eventRes = await client.calendar.calendarEvent.list({
path: { calendar_id: calendarId },
params: {
start_time: String(now),
end_time: String(endTime),
page_size: 50
}
});
// Fallback for permission errors on specific IDs
if (eventRes.code !== 0 && calendarId !== 'primary') {
console.log(`Failed to access calendar ${calendarId} (${eventRes.code}: ${eventRes.msg}). Falling back to 'primary'...`);
eventRes = await client.calendar.calendarEvent.list({
path: { calendar_id: 'primary' },
params: {
start_time: String(now),
end_time: String(endTime),
page_size: 50
}
});
if (eventRes.code === 0) {
console.log(`Using 'primary' calendar instead.`);
}
}
if (eventRes.code !== 0) {
throw new Error(`Failed to list events: ${eventRes.msg}`);
}
const events = eventRes.data.items || [];
if (events.length === 0) {
console.log(`No events found in the next ${hours} hours.`);
} else {
console.log(`Found ${events.length} upcoming events:`);
events.forEach(e => {
const start = new Date(parseInt(e.start_time.timestamp) * 1000).toLocaleString();
console.log(`- [${start}] ${e.summary || '(No Title)'}`);
});
}
} catch (error) {
console.error('Error checking calendar:', error.message);
process.exit(1);
}
}
// Parse args
const args = process.argv.slice(2);
let hours = 24;
if (args.includes('--next')) {
const idx = args.indexOf('--next');
if (idx + 1 < args.length) {
hours = parseInt(args[idx + 1], 10) || 24;
}
}
checkCalendar(hours);
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
(async () => {
console.log('🧹 Deep Cleaning Calendar...');
// 1. Find Bot Calendar
let botCalendarId;
const calList = await client.calendar.calendar.list();
if (calList.code === 0 && calList.data.calendar_list) {
const botCal = calList.data.calendar_list.find(c => c.summary === 'OpenClaw Assistant');
if (botCal) botCalendarId = botCal.calendar_id;
}
if (!botCalendarId) {
// Try fallback
try {
const primary = await client.calendar.calendar.get({ calendar_id: 'primary' });
if (primary.code === 0) botCalendarId = 'primary';
} catch(e) {}
}
if (!botCalendarId) {
console.log("No calendar to clean.");
return;
}
// 2. List ALL Future Events
const now = Math.floor(Date.now() / 1000);
const endTime = now + 30 * 24 * 3600; // 30 days
// SDK approach with raw fallback if needed
let res = await client.request({
method: 'GET',
url: `/open-apis/calendar/v4/calendars/${encodeURIComponent(botCalendarId)}/events`,
params: {
start_time: String(now),
end_time: String(endTime),
page_size: 100
}
});
if (res.code !== 0 && botCalendarId !== 'primary') {
console.log(`Access failed for ${botCalendarId}. Retrying primary...`);
botCalendarId = 'primary';
res = await client.request({
method: 'GET',
url: `/open-apis/calendar/v4/calendars/primary/events`,
params: {
start_time: String(now),
end_time: String(endTime),
page_size: 100
}
});
}
if (res.code === 0 && res.data.items) {
for (const evt of res.data.items) {
// Delete everything EXCEPT the "System Maintenance" one?
// Or just delete the "undefined" ones.
if (!evt.summary || evt.summary === 'undefined' || evt.summary.trim() === '') {
// If it is already deleted/cancelled (status="cancelled"), skip
if (evt.status === 'cancelled') continue;
console.log(`🗑️ Deleting Empty Event: ${evt.event_id}`);
try {
await client.request({
method: 'DELETE',
url: `/open-apis/calendar/v4/calendars/${encodeURIComponent(botCalendarId)}/events/${evt.event_id}`
});
} catch (delErr) {
console.log(`Failed to delete ${evt.event_id} (might be already gone): ${delErr.message}`);
}
}
}
console.log("Cleanup Done.");
}
})();
const { program } = require('commander');
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
if (!APP_ID || !APP_SECRET) {
console.error('Error: FEISHU_APP_ID or FEISHU_APP_SECRET not set.');
process.exit(1);
}
const client = new Lark.Client({
appId: APP_ID,
appSecret: APP_SECRET,
});
program
.requiredOption('--summary <text>', 'Event title')
.requiredOption('--start <time>', 'Start time (YYYY-MM-DD HH:mm)')
.requiredOption('--end <time>', 'End time (YYYY-MM-DD HH:mm)')
.option('--desc <text>', 'Description')
.option('--calendar <id>', 'Target Calendar ID')
.option('--attendees <ids>', 'Comma-separated user OpenIDs to invite')
.parse(process.argv);
const options = program.opts();
async function createEvent() {
try {
const startTs = Math.floor(new Date(options.start).getTime() / 1000);
const endTs = Math.floor(new Date(options.end).getTime() / 1000);
if (isNaN(startTs) || isNaN(endTs)) {
console.error('Invalid date format.');
process.exit(1);
}
// Auto-discover calendar if not provided
let targetCalendarId = options.calendar;
if (!targetCalendarId) {
try {
const calList = await client.calendar.calendar.list();
if (calList.code === 0 && calList.data.calendar_list && calList.data.calendar_list.length > 0) {
// Prefer 'OpenClaw Assistant', then 'primary', then first in list
const botCal = calList.data.calendar_list.find(c => c.summary === 'OpenClaw Assistant') || calList.data.calendar_list[0];
targetCalendarId = botCal.calendar_id;
console.log(`Auto-selected calendar: ${botCal.summary} (${targetCalendarId})`);
}
} catch (e) {
console.warn("Failed to auto-discover calendar:", e.message);
}
}
if (!targetCalendarId) {
console.error("No calendar found. Please create one first.");
process.exit(1);
}
const attendees = [];
if (options.attendees) {
options.attendees.split(',').forEach(id => {
attendees.push({
type: 'user',
user_id: id.trim()
});
});
}
console.log(`Creating event on calendar: ${targetCalendarId}`);
// Direct Request with Fallback
async function tryCreate(calId) {
return await client.request({
method: 'POST',
url: `/open-apis/calendar/v4/calendars/${encodeURIComponent(calId)}/events?user_id_type=open_id`,
data: {
summary: options.summary,
description: options.desc || '',
need_notification: true, // Explicitly request notification
start_time: { timestamp: String(startTs), timezone: 'Asia/Shanghai' },
end_time: { timestamp: String(endTs), timezone: 'Asia/Shanghai' },
attendees: attendees.length > 0 ? attendees : undefined,
vchat: { vc_type: 'no_meeting' }
}
});
}
let response = await tryCreate(targetCalendarId);
// Fallback to primary if specific ID fails (permission issue)
if (response.code !== 0 && targetCalendarId !== 'primary') {
console.log(`Creation failed on ${targetCalendarId} (${response.code}). Retrying on 'primary'...`);
response = await tryCreate('primary');
}
if (response.code !== 0) {
console.error(`Feishu API Error: ${response.msg} (Code: ${response.code})`);
process.exit(1);
}
const evt = response.data.event;
console.log(`✅ Event Created: ${evt.summary}`);
console.log(` Time: ${new Date(parseInt(evt.start_time.timestamp)*1000).toLocaleString()}`);
console.log(` Link: ${evt.app_link}`);
} catch (e) {
console.error('Error:', e.message);
if (e.response) console.error('Data:', JSON.stringify(e.response.data));
}
}
createEvent();
const CalendarManager = require('./lib/CalendarManager');
/**
* Feishu Calendar Skill
*
* Main entry point for programmatic usage and CLI dispatch.
*/
// Export the CalendarManager class for use by other skills
exports.CalendarManager = CalendarManager;
// Export a default instance for convenience
exports.defaultManager = new CalendarManager();
// CLI Dispatcher
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
if (!command || command === '--help' || command === '-h') {
console.log(`
Feishu Calendar Skill
Usage:
node skills/feishu-calendar [command] [options]
Commands:
list List calendars
check Check upcoming events (default: 24h)
sync Sync events to memory/HEARTBEAT.md
search Search for a calendar by name
help Show this help message
Examples:
node skills/feishu-calendar check --hours 48
node skills/feishu-calendar sync
`);
process.exit(0);
}
// Dispatch to existing scripts based on command
const scriptMap = {
'list': './list_test.js',
'check': './check.js',
'sync': './sync.js', // or sync_routine.js
'search': './search_cal.js'
};
if (scriptMap[command]) {
// Forward arguments to the script
// We can require the script if it exports a main function,
// but these scripts seem designed to run on import or top-level await.
// Given they are legacy scripts, using child_process.spawn might be safer to preserve their env,
// OR we can try to require them if we know they are safe.
// 'check.js' runs on load. 'sync.js' runs on load.
// So requiring them is the way to go.
// Adjust process.argv so the script sees the right args
// Remove the command name from args
process.argv.splice(2, 1);
try {
require(scriptMap[command]);
} catch (e) {
console.error(`Failed to execute command '${command}':`, e);
process.exit(1);
}
} else {
console.error(`Unknown command: ${command}`);
console.error(`Run with --help for usage.`);
process.exit(1);
}
}
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../../.env') });
class CalendarManager {
constructor() {
this.client = new Lark.Client({
appId: process.env.FEISHU_APP_ID,
appSecret: process.env.FEISHU_APP_SECRET
});
}
async getCalendar(searchKeyword = 'Master') {
try {
const listRes = await this.client.calendar.calendar.list();
if (listRes.code !== 0) throw new Error(`List calendars failed: ${listRes.msg}`);
const calendars = listRes.data.calendar_list || [];
let target = calendars.find(c => c.summary.includes(searchKeyword) || c.summary.includes("OpenClaw"));
if (!target && calendars.length > 0) target = calendars[0]; // Fallback
return target;
} catch (e) {
console.error(`[CalendarManager] Error getting calendar: ${e.message}`);
return null;
}
}
async listEvents(calendarId, startTime = Date.now(), endTime = Date.now() + 86400 * 3000, pageSize = 50) {
try {
const params = {
start_time: String(Math.floor(startTime / 1000)),
end_time: String(Math.floor(endTime / 1000)),
page_size: pageSize
};
let res = await this.client.calendar.calendarEvent.list({
path: { calendar_id: calendarId },
params: params
});
if (res.code !== 0) {
if (calendarId !== 'primary') {
console.log(`[CalendarManager] Access failed for ID ${calendarId}, falling back to 'primary'...`);
res = await this.client.calendar.calendarEvent.list({
path: { calendar_id: 'primary' },
params: params
});
}
}
if (res.code !== 0) throw new Error(`Fetch events failed: ${res.msg}`);
return res.data.items || [];
} catch (e) {
console.error(`[CalendarManager] Error listing events: ${e.message}`);
return [];
}
}
async addEvent(calendarId, eventData) {
try {
// eventData structure: { summary, description, start_time, end_time, recurrence, ... }
// Ensure timestamps are strings
if (typeof eventData.start_time.timestamp === 'number') eventData.start_time.timestamp = String(eventData.start_time.timestamp);
if (typeof eventData.end_time.timestamp === 'number') eventData.end_time.timestamp = String(eventData.end_time.timestamp);
const res = await this.client.calendar.calendarEvent.create({
path: { calendar_id: calendarId },
data: eventData
});
if (res.code !== 0) {
if (calendarId !== 'primary') {
console.log(`[CalendarManager] Create failed for ID ${calendarId}, falling back to 'primary'...`);
return await this.client.calendar.calendarEvent.create({
path: { calendar_id: 'primary' },
data: eventData
});
}
throw new Error(`Create event failed: ${res.msg}`);
}
return res.data;
} catch (e) {
console.error(`[CalendarManager] Error adding event: ${e.message}`);
return null;
}
}
}
module.exports = CalendarManager;
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
(async () => {
console.log("Listing Calendars...");
const res = await client.calendar.calendar.list();
if (res.code === 0) {
res.data.calendar_list.forEach(c => {
console.log(`- [${c.summary}] ID: ${c.calendar_id} (Role: ${c.role})`);
});
} else {
console.error("Error:", res);
}
})();
{
"name": "feishu-calendar",
"version": "1.0.0",
"description": "Manage Feishu Calendars, sync events, and check schedules.",
"main": "index.js",
"scripts": {
"test": "node list_test.js"
},
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.0.0",
"dotenv": "^16.0.0"
}
}
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const MASTER_ID = process.env.OPENCLAW_MASTER_ID;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
(async () => {
// Attempt 2: Search for calendar
console.log("Searching for Master's calendar...");
try {
const res = await client.calendar.calendar.search({
data: {
query: process.env.FEISHU_CALENDAR_QUERY || process.argv[2] || "ExampleUser"
}
});
if (res.code === 0 && res.data.items) {
console.log("Found Calendars:", res.data.items.map(c => `${c.summary} (${c.calendar_id})`));
} else {
console.log("Search failed or empty:", res);
}
} catch(e) { console.error("Search Error:", e.message); }
})();
const { program } = require('commander');
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
if (!APP_ID || !APP_SECRET) {
console.error('Error: FEISHU_APP_ID or FEISHU_APP_SECRET not set.');
process.exit(1);
}
const client = new Lark.Client({
appId: APP_ID,
appSecret: APP_SECRET,
});
async function clearTestEvents() {
try {
console.log('🧹 Cleaning up test events...');
// 1. Find Bot Calendar
let botCalendarId;
const calList = await client.calendar.calendar.list();
if (calList.code === 0 && calList.data.calendar_list) {
const botCal = calList.data.calendar_list.find(c => c.summary === 'OpenClaw Assistant');
if (botCal) botCalendarId = botCal.calendar_id;
}
if (!botCalendarId) {
// Try fallback
try {
const primary = await client.calendar.calendar.get({ calendar_id: 'primary' });
if (primary.code === 0) botCalendarId = 'primary';
} catch(e) {}
}
if (!botCalendarId) {
console.error('❌ Bot calendar not found.');
return;
}
// 2. List Events
let res = await client.request({
method: 'GET',
url: `/open-apis/calendar/v4/calendars/${encodeURIComponent(botCalendarId)}/events`,
params: { page_size: 50 }
});
// Fallback for list
if (res.code !== 0 && botCalendarId !== 'primary') {
console.log(`Access failed for ${botCalendarId}. Retrying primary...`);
botCalendarId = 'primary';
res = await client.request({
method: 'GET',
url: `/open-apis/calendar/v4/calendars/primary/events`,
params: { page_size: 50 }
});
}
if (res.code === 0 && res.data.items) {
for (const evt of res.data.items) {
if (evt.summary.includes('Test') || evt.summary.includes('Invite')) {
console.log(`🗑️ Deleting: ${evt.summary} (${evt.event_id})`);
await client.request({
method: 'DELETE',
url: `/open-apis/calendar/v4/calendars/${encodeURIComponent(botCalendarId)}/events/${evt.event_id}`
});
}
}
}
} catch (e) {
console.error('Cleanup Error:', e.message);
}
}
async function addMaintenanceSchedule() {
try {
console.log('📅 Adding Daily Maintenance Schedule...');
// 1. Get calendar list to find appropriate target
let botCalendarId;
const calList = await client.calendar.calendar.list();
if (calList.code === 0 && calList.data.calendar_list) {
const botCal = calList.data.calendar_list.find(c => c.summary === 'OpenClaw Assistant');
if (botCal) botCalendarId = botCal.calendar_id;
}
// If not found, default to primary immediately to avoid unnecessary checks
if (!botCalendarId) botCalendarId = 'primary';
// Recurrence Rule: Daily at 04:00 AM
// RRULE:FREQ=DAILY;BYHOUR=4;BYMINUTE=0;BYSECOND=0
// Create Recurring Event
// Note: For recurring, start_time is the first instance
const now = new Date();
const tomorrow4am = new Date(now);
tomorrow4am.setDate(now.getDate() + 1);
tomorrow4am.setHours(4, 0, 0, 0);
const startTs = Math.floor(tomorrow4am.getTime() / 1000);
const endTs = startTs + 300; // 5 minutes duration
// Helper to attempt creation
async function create(calId) {
return await client.request({
method: 'POST',
url: `/open-apis/calendar/v4/calendars/${encodeURIComponent(calId)}/events`,
data: {
summary: '🛡️ System Maintenance (Auto-Restart)',
description: 'Routine system health check and gateway restart if idle.',
start_time: { timestamp: String(startTs), timezone: 'Asia/Shanghai' },
end_time: { timestamp: String(endTs), timezone: 'Asia/Shanghai' },
recurrence: 'FREQ=DAILY;INTERVAL=1', // Daily
color: -1,
permissions: 'public'
}
});
}
let res = await create(botCalendarId);
// Fallback retry if specific ID failed (e.g. permission error) and it wasn't already primary
if (res.code !== 0 && botCalendarId !== 'primary') {
console.log(`Creation failed on ${botCalendarId} (${res.code}). Retrying on 'primary'...`);
res = await create('primary');
}
if (res.code === 0) {
console.log(`✅ Maintenance Schedule Added: ${res.data.event.app_link}`);
} else {
console.error(`❌ Failed to add schedule: ${res.msg}`);
}
} catch (e) {
console.error('Schedule Error:', e.message);
}
}
(async () => {
await clearTestEvents();
await addMaintenanceSchedule();
})();
const { program } = require('commander');
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
if (!APP_ID || !APP_SECRET) {
console.error('Error: FEISHU_APP_ID or FEISHU_APP_SECRET not set.');
process.exit(1);
}
const client = new Lark.Client({
appId: APP_ID,
appSecret: APP_SECRET,
});
program
.requiredOption('--name <text>', 'Calendar Name')
.requiredOption('--desc <text>', 'Calendar Description')
.requiredOption('--members <ids>', 'Comma-separated OpenIDs to add as members')
.option('--role <role>', 'Role for members (writer, reader, owner)', 'writer')
.parse(process.argv);
const options = program.opts();
async function main() {
try {
console.log(`Creating shared calendar: ${options.name}...`);
// 1. Create Calendar
// Use default method for creation, which is usually fine for new calendars.
// Fallback isn't really applicable for creating a NEW calendar, but we can ensure consistent error handling.
let createRes = await client.request({
method: 'POST',
url: '/open-apis/calendar/v4/calendars',
data: {
summary: options.name,
description: options.desc,
permissions: 'private', // Default privacy
color: -1,
summary_alias: options.name
}
});
if (createRes.code !== 0) {
console.error(`Failed to create calendar: ${createRes.msg}`);
process.exit(1);
}
const calendarId = createRes.data.calendar.calendar_id;
console.log(`✅ Calendar Created: ${calendarId}`);
// 2. Add Members (ACL)
const members = options.members.split(',').map(s => s.trim()).filter(s => s);
for (const userId of members) {
console.log(`Adding member ${userId} as ${options.role}...`);
// Try specific calendar ACL update
let aclRes = await client.request({
method: 'POST',
url: `/open-apis/calendar/v4/calendars/${encodeURIComponent(calendarId)}/acls?user_id_type=open_id`,
data: {
role: options.role,
scope: {
type: 'user',
user_id: userId
}
}
});
// If failed (e.g. 403 or permission denied), check if fallback is needed, though creating a new calendar usually works fine.
// But if this script is reused for existing calendars where permissions are tight, we might fail.
// However, this script creates NEW calendars, so permission should be fine (Bot is owner).
if (aclRes.code !== 0) {
console.error(`❌ Failed to add ${userId}: ${aclRes.msg}`);
} else {
console.log(`✅ Added ${userId}`);
}
}
// 3. Subscribe to the calendar (Bot needs to subscribe to manage it effectively? Bot is owner, auto-subscribed)
// Actually Bot is owner, so it has access.
console.log(`\n🎉 Shared Calendar Setup Complete! ID: ${calendarId}`);
} catch (e) {
console.error('Error:', e.message);
if (e.response) console.error('Data:', JSON.stringify(e.response.data));
}
}
main();
const CalendarManager = require('./lib/CalendarManager');
const manager = new CalendarManager();
async function addRoutineEvents() {
console.log('📅 Syncing Routine Tasks to Calendar (via CalendarManager)...');
// 1. Find Bot Calendar
const botCal = await manager.getCalendar('OpenClaw Assistant');
if (!botCal) return console.error("Calendar not found");
// 2. Define Routine Events
const routines = [
{
summary: '🛡️ System Maintenance (Auto-Restart)',
description: 'Automated health check and process restart to prevent memory leaks.',
hour: 4, // 04:00 UTC
minute: 0,
duration: 300, // 5 mins
rrule: 'FREQ=DAILY',
color: -1
},
{
summary: '🌅 Morning Briefing',
description: 'Generate and send a morning briefing card (yesterday highlights + today agenda) to Master.',
hour: 1, // 01:30 UTC = 09:30 CST
minute: 30,
duration: 300, // 5 mins
rrule: 'FREQ=DAILY',
color: -1
},
{
summary: '📝 Xiaoxia\'s Diary',
description: 'Reflect on the day, write diary, and update Feishu Doc.',
hour: 20, // 20:00 UTC = 04:00 CST (next day) -- wait, 20:00 UTC is late.
minute: 0,
duration: 900, // 15 mins
rrule: 'FREQ=DAILY',
color: -1
},
{
summary: '🦐 ClawdChat Check',
description: 'Check community feed and interact with other agents.',
hour: 0,
minute: 0,
duration: 300, // 5 mins
rrule: 'FREQ=DAILY;INTERVAL=1;BYHOUR=0,4,8,12,16,20', // Every 4 hours
color: -1
},
{
summary: '🔄 Calendar Sync',
description: 'Check for new tasks and sync heartbeat state.',
hour: 0,
minute: 15,
duration: 60, // 1 min
rrule: 'FREQ=HOURLY;INTERVAL=1;BYMINUTE=15,45',
color: -1
}
];
// 3. Create Events
const now = new Date();
for (const task of routines) {
console.log(`Scheduling: ${task.summary}`);
// Calculate next occurrence for start_time
const start = new Date(now);
start.setUTCHours(task.hour, task.minute, 0, 0);
if (start < now) start.setDate(start.getDate() + 1);
const startTs = Math.floor(start.getTime() / 1000);
const endTs = startTs + task.duration;
const eventData = {
summary: task.summary,
description: task.description,
start_time: { timestamp: String(startTs), timezone: 'UTC' },
end_time: { timestamp: String(endTs), timezone: 'UTC' },
recurrence: task.rrule,
permissions: 'public'
};
const res = await manager.addEvent(botCal.calendar_id, eventData);
if (res) console.log(`✅ Added: ${task.summary}`);
else console.error(`❌ Failed ${task.summary}`);
}
}
addRoutineEvents();
const CalendarManager = require('./lib/CalendarManager');
const fs = require('fs');
const path = require('path');
const { getTimestampCST } = require('../time-helper');
const manager = new CalendarManager();
(async () => {
console.log("🔄 Syncing Calendar Events (via CalendarManager)...");
// 1. Get Bot Calendar (or Primary)
// We prefer 'OpenClaw Assistant' but fallback is fine.
const botCal = await manager.getCalendar('OpenClaw Assistant');
if (!botCal) return console.error("Calendar not found.");
// 2. Fetch Events (Future 7 days)
const now = Math.floor(Date.now() / 1000);
const endTime = now + 7 * 24 * 3600;
// Use CalendarManager's robust listing with fallback
const events = await manager.listEvents(botCal.calendar_id, now, endTime, 50);
if (events && events.length > 0) {
console.log(`✅ Found ${events.length} active events.`);
// Format for reporting
let report = "📅 **OpenClaw Schedule (Next 7 Days):**\n\n";
events.forEach(e => {
const start = getTimestampCST(parseInt(e.start_time.timestamp) * 1000);
report += `- **${start}**: ${e.summary || '(No Title)'} (ID: ${e.event_id.slice(-4)})\n`;
});
console.log(report);
// Save state
fs.writeFileSync(path.resolve(__dirname, '../../memory/calendar_events.json'), JSON.stringify(events, null, 2));
// Sync to HEARTBEAT.md
const heartbeatPath = path.resolve(__dirname, '../../HEARTBEAT.md');
if (fs.existsSync(heartbeatPath)) {
let heartbeatContent = fs.readFileSync(heartbeatPath, 'utf8');
// Generate calendar section content
let calendarSection = "## 📅 Calendar (Next 24h)\n\n";
// Filter events happening in next 24h
const next24h = events.filter(e => {
const t = parseInt(e.start_time.timestamp);
return t < (Date.now()/1000 + 86400);
});
if (next24h.length === 0) {
// If no events, just keep it minimal or don't modify section?
// Let's explicitly say 'No events' to be helpful.
calendarSection = "## 📅 Calendar (Next 24h)\n\n- No upcoming events in the next 24 hours.\n";
} else {
next24h.forEach(e => {
const start = getTimestampCST(parseInt(e.start_time.timestamp) * 1000).split(' ')[1]; // Extract HH:MM
calendarSection += `- [ ] ${start} - ${e.summary}\n`;
});
}
// Regex to find existing Calendar section or append
// We look for "## 📅 Calendar" or just "## Calendar"
const calendarRegex = /## (?:📅 )?Calendar.*?(?=\n## |$)/s;
if (calendarRegex.test(heartbeatContent)) {
// Replace existing section
heartbeatContent = heartbeatContent.replace(calendarRegex, calendarSection.trim());
} else {
// Append before "## Morning" or at end if not found
const insertPos = heartbeatContent.indexOf('## Morning');
if (insertPos !== -1) {
heartbeatContent = heartbeatContent.slice(0, insertPos) + calendarSection + "\n" + heartbeatContent.slice(insertPos);
} else {
heartbeatContent += "\n" + calendarSection;
}
}
fs.writeFileSync(heartbeatPath, heartbeatContent, 'utf8');
console.log("✅ Synced to HEARTBEAT.md");
}
} else {
console.log("No active events found.");
// Clear HEARTBEAT.md calendar section if empty? Or keep it blank?
// Let's keep it minimal if empty.
}
})();
function getTimestampCST(timestamp) {
const d = new Date(timestamp);
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(d);
}
module.exports = {
getTimestampCST
};
Related skills
FAQ
What can feishu-calendar do?
List and search calendars, sync events, create entries (including from task requests), and set up shared project calendars with members and roles.
How does task marking work?
When a user says 'Mark this task' or 'Remind me to', it extracts the date/time and runs create.js with a 'Task:' summary and a 1-hour default duration.