Initial backup 2026-02-17

This commit is contained in:
Krilly
2026-02-17 15:50:53 +00:00
commit 8902a93add
941 changed files with 131420 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "lark-calendar",
"installedVersion": "1.0.0",
"installedAt": 1770184137321
}

View File

@@ -0,0 +1,229 @@
---
name: lark-calendar
description: Create, update, and delete calendar events and tasks in Lark (Feishu). Includes employee directory for automatic name-to-user_id resolution.
version: 1.0.0
author: Claw AI
---
# Lark Calendar & Task Skill
Create, update, and delete calendar events and tasks in Lark (Feishu).
## Overview
This skill provides full CRUD operations for:
- **Calendar Events** — meetings, appointments, schedules
- **Tasks (Todo)** — action items with deadlines
## Configuration
**Required Environment Variables** (in `.secrets.env`):
```bash
FEISHU_APP_ID=cli_a9f52a4ed7b8ded4
FEISHU_APP_SECRET=<your-app-secret>
```
**Default Calendar:** `feishu.cn_caF80RJxgGcbBGsQx64bCh@group.calendar.feishu.cn` (Claw calendar)
**Default Timezone:** `Asia/Singapore`
## Quick Reference
### Create Calendar Event
```bash
node skills/lark-calendar/scripts/create-event.mjs \
--title "Meeting with Team" \
--description "Discuss Q2 roadmap" \
--start "2026-02-03 14:00:00" \
--end "2026-02-03 15:00:00" \
--attendees "Boyang,RK" \
--location "Meeting Room A"
```
**Parameters:**
| Param | Required | Description |
|-------|----------|-------------|
| `--title` | ✅ | Event title |
| `--description` | ❌ | Event description |
| `--start` | ✅ | Start time (YYYY-MM-DD HH:MM:SS) |
| `--end` | ✅ | End time (YYYY-MM-DD HH:MM:SS) |
| `--attendees` | ❌ | Comma-separated names (auto-resolved to user_ids) |
| `--attendee-ids` | ❌ | Comma-separated user_ids directly |
| `--location` | ❌ | Event location |
| `--timezone` | ❌ | Timezone (default: Asia/Singapore) |
| `--calendar` | ❌ | Calendar ID (uses default if omitted) |
### Update Calendar Event
```bash
node skills/lark-calendar/scripts/update-event.mjs \
--event-id "f9900f6b-b472-4b17-a818-7b5584abdc37_0" \
--title "Updated Title" \
--start "2026-02-03 15:00:00" \
--end "2026-02-03 16:00:00"
```
### Delete Calendar Event
```bash
node skills/lark-calendar/scripts/delete-event.mjs \
--event-id "f9900f6b-b472-4b17-a818-7b5584abdc37_0"
```
### List Calendar Events
```bash
# List events for next 7 days
node skills/lark-calendar/scripts/list-events.mjs
# List events in date range
node skills/lark-calendar/scripts/list-events.mjs \
--start "2026-02-01" \
--end "2026-02-28"
```
### Create Task
```bash
node skills/lark-calendar/scripts/create-task.mjs \
--title "Review PR #123" \
--description "Code review for authentication module" \
--due "2026-02-05 18:00:00" \
--assignees "Boyang,jc"
```
**Parameters:**
| Param | Required | Description |
|-------|----------|-------------|
| `--title` | ✅ | Task title |
| `--description` | ❌ | Task description |
| `--due` | ✅ | Due date (YYYY-MM-DD HH:MM:SS) |
| `--assignees` | ❌ | Comma-separated names (auto-resolved) |
| `--assignee-ids` | ❌ | Comma-separated user_ids directly |
| `--timezone` | ❌ | Timezone (default: Asia/Singapore) |
### Update Task
```bash
node skills/lark-calendar/scripts/update-task.mjs \
--task-id "35fc5310-a1b1-49c7-be75-be631d3079ee" \
--title "Updated Task" \
--due "2026-02-06 18:00:00"
```
### Delete Task
```bash
node skills/lark-calendar/scripts/delete-task.mjs \
--task-id "35fc5310-a1b1-49c7-be75-be631d3079ee"
```
### Manage Event Attendees
```bash
# Add attendees
node skills/lark-calendar/scripts/manage-attendees.mjs \
--event-id "xxx" --add "RK,jc"
# Remove attendees
node skills/lark-calendar/scripts/manage-attendees.mjs \
--event-id "xxx" --remove "jc"
```
### Manage Task Members
```bash
# Add members
node skills/lark-calendar/scripts/manage-task-members.mjs \
--task-id "xxx" --add "RK,jc"
# Remove members
node skills/lark-calendar/scripts/manage-task-members.mjs \
--task-id "xxx" --remove "jc"
```
## Employee Directory
Names are auto-resolved to Lark user_ids. Supported names:
| user_id | Names | Role |
|---------|-------|------|
| `dgg163e1` | Boyang, by, 博洋 | Boss |
| `gb71g28b` | RK | Leadership, R&D |
| `53gc5724` | Ding | Leadership, Operations |
| `217ec2c2` | Charline | HR |
| `f2bfd283` | 曾晓玲, xiaoling | HR |
| `f26fe45d` | HH | Research |
| `45858f91` | zan, Eva | - |
| `7f79b6de` | Issac | Operations |
| `1fb2547g` | 王铁柱 | Operations |
| `e5997acd` | 尼克, Nico | Operations |
| `438c3c1f` | Ivan | Operations |
| `17g8bab2` | Dodo | R&D, Product |
| `73b45ec5` | 启超, QiChaoShi | R&D, Design |
| `d1978a39` | chenglin | R&D, Frontend |
| `ef6fc4a7` | 冠林, Green | R&D, Frontend |
| `b47fa8f2` | sixian, sx, Sixian-Yu | R&D, Frontend |
| `934fbf15` | jc, sagiri, 俊晨 | R&D, Backend |
| `8c4aad87` | 大明, daming | R&D, Backend |
| `ab87g5e1` | Emily Yobal | Intern |
| `55fa337f` | jingda, 景达 | Intern |
| `333c7cf1` | 刘纪源, 纪源, Aiden | Intern |
## Business Rules
1. **Boyang is always added** as attendee to every calendar event (automatic)
2. **Timezone handling:** Uses IANA identifiers (e.g., `Asia/Singapore`, `Asia/Shanghai`)
3. **Time format:** Always `YYYY-MM-DD HH:MM:SS`
4. **user_id vs open_id:** This skill uses `user_id` format (e.g., `dgg163e1`), NOT `open_id` (e.g., `ou_xxx`)
## Programmatic Usage
```javascript
import { createEvent, updateEvent, deleteEvent } from './skills/lark-calendar/lib/calendar.mjs';
import { createTask, updateTask, deleteTask } from './skills/lark-calendar/lib/task.mjs';
import { resolveNames } from './skills/lark-calendar/lib/employees.mjs';
// Create event
const result = await createEvent({
title: 'Team Sync',
description: 'Weekly standup',
startTime: '2026-02-03 10:00:00',
endTime: '2026-02-03 10:30:00',
attendeeIds: ['dgg163e1', 'gb71g28b'],
location: 'Zoom',
timezone: 'Asia/Singapore'
});
// Create task
const task = await createTask({
title: 'Review document',
description: 'Q2 planning doc',
dueTime: '2026-02-05 18:00:00',
assigneeIds: ['dgg163e1'],
timezone: 'Asia/Singapore'
});
```
## Lark API Reference
- [Calendar Events API](https://open.larksuite.com/document/server-docs/calendar-v4/calendar-event/create)
- [Calendar Attendees API](https://open.larksuite.com/document/server-docs/calendar-v4/calendar-event-attendee/create)
- [Tasks API](https://open.larksuite.com/document/server-docs/task-v2/task/create)
## Permissions Required
Ensure your Lark app has these scopes:
- `calendar:calendar` — Read/write calendar ✅ (already enabled)
- `calendar:calendar:readonly` — Read calendar ✅ (already enabled)
- `task:task:write` — Write tasks ⚠️ (needs to be added for task creation)
- `task:task:read` — Read tasks
- `contact:user.employee_id:readonly` — Read user info ✅ (already enabled)
**To add permissions:**
1. Go to [Lark Open Platform](https://open.larksuite.com/app/cli_a9f52a4ed7b8ded4/auth)
2. Add scopes: `task:task:write`, `contact:contact:readonly` (for dynamic employee lookup)
3. Re-publish the app version
**Note:** Without `contact:contact:readonly`, the skill uses a static fallback employee list. Update `lib/employees.mjs` when team changes.

View File

@@ -0,0 +1,310 @@
/**
* Lark Calendar Event Operations
*/
import { larkApi } from './lark-api.mjs';
import { ensureBoyangIncluded, getDisplayName } from './employees.mjs';
// Default calendar ID (Claw calendar)
export const DEFAULT_CALENDAR_ID = 'feishu.cn_caF80RJxgGcbBGsQx64bCh@group.calendar.feishu.cn';
// Default timezone
export const DEFAULT_TIMEZONE = 'Asia/Singapore';
/**
* Convert datetime string to Unix timestamp
* @param {string} timeStr - Format: YYYY-MM-DD HH:MM:SS
* @param {string} timezone - IANA timezone (e.g., Asia/Singapore)
* @returns {number} - Unix timestamp in seconds
*/
export function datetimeToTimestamp(timeStr, timezone = DEFAULT_TIMEZONE) {
// Parse the datetime string
const [datePart, timePart] = timeStr.split(' ');
const [year, month, day] = datePart.split('-').map(Number);
const [hour, minute, second] = (timePart || '00:00:00').split(':').map(Number);
// Create date in the specified timezone
// JavaScript Date uses local time, so we need to handle timezone offset
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second || 0).padStart(2, '0')}`;
// Use Intl to get the timezone offset
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
// Create a date object assuming the input is in the target timezone
// We need to find the UTC equivalent
const localDate = new Date(dateStr);
// Get the offset for this timezone at this date
const utcDate = new Date(localDate.toLocaleString('en-US', { timeZone: 'UTC' }));
const tzDate = new Date(localDate.toLocaleString('en-US', { timeZone: timezone }));
const offset = utcDate - tzDate;
// Adjust and return timestamp
return Math.floor((localDate.getTime() + offset) / 1000);
}
/**
* Convert Unix timestamp to datetime string
* @param {number} timestamp - Unix timestamp in seconds
* @param {string} timezone - IANA timezone
* @returns {string} - Format: YYYY-MM-DD HH:MM:SS
*/
export function timestampToDatetime(timestamp, timezone = DEFAULT_TIMEZONE) {
const date = new Date(timestamp * 1000);
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
const parts = formatter.formatToParts(date);
const values = {};
for (const part of parts) {
values[part.type] = part.value;
}
return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}:${values.second}`;
}
/**
* Create a calendar event
* @param {object} options
* @param {string} options.title - Event title
* @param {string} [options.description] - Event description
* @param {string} options.startTime - Start time (YYYY-MM-DD HH:MM:SS)
* @param {string} options.endTime - End time (YYYY-MM-DD HH:MM:SS)
* @param {string[]} [options.attendeeIds] - Array of user_ids
* @param {string} [options.location] - Event location
* @param {string} [options.timezone] - IANA timezone
* @param {string} [options.calendarId] - Calendar ID
* @returns {object} - Created event data
*/
export async function createEvent({
title,
description = '',
startTime,
endTime,
attendeeIds = [],
location = '',
timezone = DEFAULT_TIMEZONE,
calendarId = DEFAULT_CALENDAR_ID
}) {
// Always include Boyang
const finalAttendeeIds = ensureBoyangIncluded(attendeeIds);
// Convert times to timestamps
const startTimestamp = datetimeToTimestamp(startTime, timezone);
const endTimestamp = datetimeToTimestamp(endTime, timezone);
// Build location params
const locationParams = location ? { name: location } : null;
// Create the event
const eventData = await larkApi('POST', `/calendar/v4/calendars/${calendarId}/events`, {
data: {
summary: title,
description: description,
start_time: { timestamp: String(startTimestamp), timezone },
end_time: { timestamp: String(endTimestamp), timezone },
visibility: 'private',
attendee_ability: 'can_modify_event',
free_busy_status: 'busy',
reminders: [],
schemas: [],
attachments: [],
color: -1,
recurrence: null,
location: locationParams
}
});
const event = eventData.event;
// Add attendees if any
let attendees = [];
if (finalAttendeeIds.length > 0) {
const attendeesData = await addEventAttendees(calendarId, event.event_id, finalAttendeeIds);
attendees = attendeesData.attendees || [];
}
return {
event,
attendees,
attendeeNames: attendees.map(a => a.display_name).join(', ')
};
}
/**
* Update a calendar event
* @param {object} options
* @param {string} options.eventId - Event ID
* @param {string} [options.title] - Event title
* @param {string} [options.description] - Event description
* @param {string} [options.startTime] - Start time (YYYY-MM-DD HH:MM:SS)
* @param {string} [options.endTime] - End time (YYYY-MM-DD HH:MM:SS)
* @param {string} [options.location] - Event location
* @param {string} [options.timezone] - IANA timezone
* @param {string} [options.calendarId] - Calendar ID
* @returns {object} - Updated event data
*/
export async function updateEvent({
eventId,
title,
description,
startTime,
endTime,
location,
timezone = DEFAULT_TIMEZONE,
calendarId = DEFAULT_CALENDAR_ID
}) {
const updateData = {};
if (title !== undefined) updateData.summary = title;
if (description !== undefined) updateData.description = description;
if (location !== undefined) updateData.location = location ? { name: location } : null;
if (startTime) {
const startTimestamp = datetimeToTimestamp(startTime, timezone);
updateData.start_time = { timestamp: String(startTimestamp), timezone };
}
if (endTime) {
const endTimestamp = datetimeToTimestamp(endTime, timezone);
updateData.end_time = { timestamp: String(endTimestamp), timezone };
}
const result = await larkApi('PATCH', `/calendar/v4/calendars/${calendarId}/events/${eventId}`, {
data: updateData
});
return result.event;
}
/**
* Delete a calendar event
* @param {string} eventId - Event ID
* @param {string} [calendarId] - Calendar ID
* @param {boolean} [notify] - Send notification to attendees
* @returns {boolean} - Success
*/
export async function deleteEvent(eventId, calendarId = DEFAULT_CALENDAR_ID, notify = true) {
await larkApi('DELETE', `/calendar/v4/calendars/${calendarId}/events/${eventId}`, {
params: { need_notification: String(notify) }
});
return true;
}
/**
* Add attendees to an event
* @param {string} calendarId - Calendar ID
* @param {string} eventId - Event ID
* @param {string[]} userIds - Array of user_ids
* @returns {object} - Attendees data
*/
export async function addEventAttendees(calendarId, eventId, userIds) {
const attendees = userIds.map(userId => ({
type: 'user',
is_optional: true,
user_id: userId
}));
return larkApi('POST', `/calendar/v4/calendars/${calendarId}/events/${eventId}/attendees`, {
params: { user_id_type: 'user_id' },
data: {
attendees,
need_notification: true
}
});
}
/**
* Remove attendees from an event
* @param {string} calendarId - Calendar ID
* @param {string} eventId - Event ID
* @param {string[]} userIds - Array of user_ids to remove
* @returns {object} - Result
*/
export async function removeEventAttendees(calendarId, eventId, userIds) {
const deleteIds = userIds.map(userId => ({
type: 'user',
is_optional: true,
user_id: userId
}));
return larkApi('POST', `/calendar/v4/calendars/${calendarId}/events/${eventId}/attendees/batch_delete`, {
params: { user_id_type: 'user_id' },
data: {
delete_ids: deleteIds,
need_notification: true
}
});
}
/**
* List events in a calendar
* @param {object} options
* @param {string} [options.calendarId] - Calendar ID
* @param {string} [options.startTime] - Start of range (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)
* @param {string} [options.endTime] - End of range
* @param {string} [options.timezone] - Timezone
* @returns {object[]} - Array of events
*/
export async function listEvents({
calendarId = DEFAULT_CALENDAR_ID,
startTime,
endTime,
timezone = DEFAULT_TIMEZONE
} = {}) {
// Default to next 7 days if not specified
const now = new Date();
if (!startTime) {
startTime = now.toISOString().split('T')[0] + ' 00:00:00';
} else if (!startTime.includes(' ')) {
startTime += ' 00:00:00';
}
if (!endTime) {
const weekLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
endTime = weekLater.toISOString().split('T')[0] + ' 23:59:59';
} else if (!endTime.includes(' ')) {
endTime += ' 23:59:59';
}
const startTs = datetimeToTimestamp(startTime, timezone);
const endTs = datetimeToTimestamp(endTime, timezone);
const result = await larkApi('GET', `/calendar/v4/calendars/${calendarId}/events`, {
params: {
start_time: String(startTs),
end_time: String(endTs),
page_size: 50
}
});
return result.items || [];
}
/**
* Get a single event
* @param {string} eventId - Event ID
* @param {string} [calendarId] - Calendar ID
* @returns {object} - Event data
*/
export async function getEvent(eventId, calendarId = DEFAULT_CALENDAR_ID) {
const result = await larkApi('GET', `/calendar/v4/calendars/${calendarId}/events/${eventId}`);
return result.event;
}

View File

@@ -0,0 +1,241 @@
/**
* Employee Directory - Dynamic lookup via Lark Contact API
*
* Uses Lark's contact API to resolve names to user_ids dynamically.
* Falls back to cached data for known employees.
*/
import { larkApi } from './lark-api.mjs';
// Boyang's user_id - always added as attendee
export const BOYANG_USER_ID = 'dgg163e1';
// Cache for employee data (populated on first lookup)
let employeeCache = null;
let cacheTimestamp = 0;
const CACHE_TTL = 3600000; // 1 hour
// Fallback static data for known employees (used when API fails)
// Update this when team changes, or add contact:contact:readonly permission for dynamic lookup
const FALLBACK_EMPLOYEES = {
'dgg163e1': { user_id: 'dgg163e1', name: 'Boyang', en_name: 'Boyang', nickname: 'by' },
'gb71g28b': { user_id: 'gb71g28b', name: 'RK', en_name: 'RK' },
'53gc5724': { user_id: '53gc5724', name: 'Ding', en_name: 'Ding' },
'217ec2c2': { user_id: '217ec2c2', name: 'Charline', en_name: 'Charline' },
'f2bfd283': { user_id: 'f2bfd283', name: '曾晓玲', en_name: 'xiaoling' },
'f26fe45d': { user_id: 'f26fe45d', name: 'HH', en_name: 'HH' },
'45858f91': { user_id: '45858f91', name: 'Eva', nickname: 'zan' },
'7f79b6de': { user_id: '7f79b6de', name: 'Issac', en_name: 'Issac' },
'1fb2547g': { user_id: '1fb2547g', name: '王铁柱' },
'e5997acd': { user_id: 'e5997acd', name: 'Nico', nickname: '尼克' },
'438c3c1f': { user_id: '438c3c1f', name: 'Ivan', en_name: 'Ivan' },
'17g8bab2': { user_id: '17g8bab2', name: 'Dodo', en_name: 'Dodo' },
'73b45ec5': { user_id: '73b45ec5', name: '启超', en_name: 'QiChaoShi' },
'd1978a39': { user_id: 'd1978a39', name: 'chenglin', en_name: 'chenglin' },
'ef6fc4a7': { user_id: 'ef6fc4a7', name: '冠林', en_name: 'Green' },
'b47fa8f2': { user_id: 'b47fa8f2', name: 'Sixian-Yu', nickname: 'sx', en_name: 'sixian' },
'934fbf15': { user_id: '934fbf15', name: '俊晨', en_name: 'jc', nickname: 'sagiri' },
'8c4aad87': { user_id: '8c4aad87', name: '大明', en_name: 'daming' },
'ab87g5e1': { user_id: 'ab87g5e1', name: 'Emily Yobal', en_name: 'Emily' },
'55fa337f': { user_id: '55fa337f', name: '景达', en_name: 'jingda' },
'333c7cf1': { user_id: '333c7cf1', name: '刘纪源', en_name: 'Aiden', nickname: '纪源' },
};
/**
* Fetch all employees from Lark Contact API
* @returns {Promise<Map<string, object>>} - Map of user_id -> employee data
*/
async function fetchEmployees() {
const employees = new Map();
let pageToken = '';
try {
do {
const params = {
department_id: '0', // Root department = all employees
page_size: 50,
user_id_type: 'user_id'
};
if (pageToken) params.page_token = pageToken;
const result = await larkApi('GET', '/contact/v3/users', { params });
for (const user of (result.items || [])) {
employees.set(user.user_id, {
user_id: user.user_id,
name: user.name,
en_name: user.en_name,
nickname: user.nickname,
email: user.email,
mobile: user.mobile,
department_ids: user.department_ids,
open_id: user.open_id
});
}
pageToken = result.has_more ? result.page_token : '';
} while (pageToken);
return employees;
} catch (error) {
console.error('Failed to fetch employees from Lark API:', error.message);
console.log('[employees] Using fallback static employee data');
// Return fallback data
const fallback = new Map();
for (const [id, emp] of Object.entries(FALLBACK_EMPLOYEES)) {
fallback.set(id, emp);
}
return fallback;
}
}
/**
* Get employee cache (refreshes if stale)
*/
async function getEmployeeCache() {
const now = Date.now();
if (!employeeCache || (now - cacheTimestamp) > CACHE_TTL) {
employeeCache = await fetchEmployees();
cacheTimestamp = now;
if (employeeCache.size > 0) {
console.log(`[employees] Loaded ${employeeCache.size} employees from Lark API`);
}
}
return employeeCache;
}
/**
* Build name lookup index from employee cache
*/
function buildNameIndex(employees) {
const index = new Map();
for (const [userId, emp] of employees) {
// Index by various name fields (case-insensitive)
const names = [emp.name, emp.en_name, emp.nickname].filter(Boolean);
for (const name of names) {
index.set(name.toLowerCase(), userId);
// Also index parts (e.g., "Wang Boyang" -> "boyang", "wang")
for (const part of name.split(/\s+/)) {
if (part.length > 1) {
index.set(part.toLowerCase(), userId);
}
}
}
}
return index;
}
/**
* Resolve a name to user_id
* @param {string} name - Employee name (case-insensitive)
* @returns {Promise<string|null>} - user_id or null if not found
*/
export async function resolveNameToId(name) {
if (!name) return null;
const employees = await getEmployeeCache();
// Check if it's already a user_id
if (employees.has(name)) return name;
// Build index and lookup
const index = buildNameIndex(employees);
return index.get(name.toLowerCase()) || null;
}
/**
* Resolve multiple names to user_ids
* @param {string[]} names - Array of names
* @returns {Promise<{resolved: string[], unresolved: string[]}>}
*/
export async function resolveNames(names) {
const resolved = [];
const unresolved = [];
for (const name of names) {
const userId = await resolveNameToId(name.trim());
if (userId) {
if (!resolved.includes(userId)) {
resolved.push(userId);
}
} else {
unresolved.push(name);
}
}
return { resolved, unresolved };
}
/**
* Get employee info by user_id
* @param {string} userId
* @returns {Promise<object|null>}
*/
export async function getEmployee(userId) {
const employees = await getEmployeeCache();
return employees.get(userId) || null;
}
/**
* Get display name for a user_id
* @param {string} userId
* @returns {Promise<string>}
*/
export async function getDisplayName(userId) {
const emp = await getEmployee(userId);
return emp ? (emp.name || emp.en_name || userId) : userId;
}
/**
* Synchronous display name (uses cached data only)
* For use in non-async contexts - may return user_id if cache not populated
*/
export function getDisplayNameSync(userId) {
if (!employeeCache) return userId;
const emp = employeeCache.get(userId);
return emp ? (emp.name || emp.en_name || userId) : userId;
}
/**
* Ensure Boyang is in the attendee list
* @param {string[]} userIds
* @returns {string[]}
*/
export function ensureBoyangIncluded(userIds) {
if (!userIds.includes(BOYANG_USER_ID)) {
return [...userIds, BOYANG_USER_ID];
}
return userIds;
}
/**
* List all employees (for debugging/display)
* @returns {Promise<object[]>}
*/
export async function listEmployees() {
const employees = await getEmployeeCache();
return Array.from(employees.values());
}
/**
* Search employees by partial name match
* @param {string} query
* @returns {Promise<object[]>}
*/
export async function searchEmployees(query) {
const employees = await getEmployeeCache();
const results = [];
const q = query.toLowerCase();
for (const emp of employees.values()) {
const searchFields = [emp.name, emp.en_name, emp.nickname, emp.email].filter(Boolean);
if (searchFields.some(f => f.toLowerCase().includes(q))) {
results.push(emp);
}
}
return results;
}

View File

@@ -0,0 +1,136 @@
/**
* Lark (Feishu) API Wrapper
* Handles authentication and API calls with automatic token refresh
*/
import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// Load secrets
const __dirname = dirname(fileURLToPath(import.meta.url));
config({ path: join(__dirname, '../../../../.secrets.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
// Use larksuite.com for international Lark
const BASE_URL = 'https://open.larksuite.com/open-apis';
let accessToken = null;
let tokenExpiry = 0;
/**
* Get or refresh access token
*/
async function getAccessToken() {
// Return cached token if still valid (with 5 min buffer)
if (accessToken && Date.now() < tokenExpiry - 300000) {
return accessToken;
}
const response = await fetch(`${BASE_URL}/auth/v3/tenant_access_token/internal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: APP_ID,
app_secret: APP_SECRET
})
});
const data = await response.json();
if (data.code !== 0) {
throw new Error(`Failed to get access token: ${JSON.stringify(data)}`);
}
accessToken = `Bearer ${data.tenant_access_token}`;
// Token expires in ~2 hours, we store expiry time
tokenExpiry = Date.now() + (data.expire * 1000);
return accessToken;
}
/**
* Make authenticated API request
* @param {string} method - HTTP method
* @param {string} endpoint - API endpoint (without base URL)
* @param {object} options - { params, data }
* @returns {object} - Response data
*/
export async function larkApi(method, endpoint, { params = null, data = null } = {}) {
const token = await getAccessToken();
let url = `${BASE_URL}${endpoint}`;
// Add query params
if (params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
searchParams.append(key, value);
}
}
const queryString = searchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
const fetchOptions = {
method,
headers: {
'Authorization': token,
'Content-Type': 'application/json'
}
};
if (data && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
fetchOptions.body = JSON.stringify(data);
}
const response = await fetch(url, fetchOptions);
const result = await response.json();
// Check for token expiry error
if (result.code === 99991663) {
// Token expired, clear cache and retry once
accessToken = null;
tokenExpiry = 0;
return larkApi(method, endpoint, { params, data });
}
if (result.code !== 0) {
const error = new Error(`Lark API error: ${JSON.stringify(result)}`);
error.code = result.code;
error.larkResponse = result;
throw error;
}
return result.data;
}
/**
* Reply to a message
* @param {string} messageId - Message ID to reply to
* @param {object} content - Message content
*/
export async function replyMessage(messageId, content) {
return larkApi('POST', `/im/v1/messages/${messageId}/reply`, { data: content });
}
/**
* Send message to a chat
* @param {string} receiveId - Chat ID or user ID
* @param {string} receiveIdType - 'chat_id' | 'user_id' | 'open_id'
* @param {object} content - Message content
*/
export async function sendMessage(receiveId, receiveIdType, content) {
return larkApi('POST', '/im/v1/messages', {
params: { receive_id_type: receiveIdType },
data: {
receive_id: receiveId,
...content
}
});
}

View File

@@ -0,0 +1,187 @@
/**
* Lark Task Operations
*/
import { larkApi } from './lark-api.mjs';
import { datetimeToTimestamp, timestampToDatetime, DEFAULT_TIMEZONE } from './calendar.mjs';
/**
* Create a task
* @param {object} options
* @param {string} options.title - Task title (summary)
* @param {string} [options.description] - Task description
* @param {string} options.dueTime - Due time (YYYY-MM-DD HH:MM:SS)
* @param {string[]} [options.assigneeIds] - Array of user_ids to assign
* @param {string} [options.timezone] - IANA timezone
* @returns {object} - Created task data
*/
export async function createTask({
title,
description = '',
dueTime,
assigneeIds = [],
timezone = DEFAULT_TIMEZONE
}) {
const dueTimestamp = datetimeToTimestamp(dueTime, timezone);
// Build members array
const members = assigneeIds.map(userId => ({
type: 'user',
id: userId,
role: 'assignee'
}));
const result = await larkApi('POST', '/task/v2/tasks', {
params: { user_id_type: 'user_id' },
data: {
mode: 1,
summary: title,
description: description,
due: {
timestamp: String(dueTimestamp * 1000), // Task API uses milliseconds
is_all_day: false
},
members: members,
reminders: [{ relative_fire_minute: 30 }]
}
});
return result.task;
}
/**
* Update a task
* @param {object} options
* @param {string} options.taskId - Task GUID
* @param {string} [options.title] - Task title
* @param {string} [options.description] - Task description
* @param {string} [options.dueTime] - Due time (YYYY-MM-DD HH:MM:SS)
* @param {string} [options.timezone] - IANA timezone
* @returns {object} - Updated task data
*/
export async function updateTask({
taskId,
title,
description,
dueTime,
timezone = DEFAULT_TIMEZONE
}) {
const updateFields = [];
const taskUpdate = {};
if (title !== undefined) {
taskUpdate.summary = title;
updateFields.push('summary');
}
if (description !== undefined) {
taskUpdate.description = description;
updateFields.push('description');
}
if (dueTime) {
const dueTimestamp = datetimeToTimestamp(dueTime, timezone);
taskUpdate.due = {
timestamp: String(dueTimestamp * 1000),
is_all_day: false
};
updateFields.push('due');
}
const result = await larkApi('PATCH', `/task/v2/tasks/${taskId}`, {
params: { user_id_type: 'user_id' },
data: {
task: taskUpdate,
update_fields: updateFields
}
});
return result.task;
}
/**
* Delete a task
* @param {string} taskId - Task GUID
* @returns {boolean} - Success
*/
export async function deleteTask(taskId) {
await larkApi('DELETE', `/task/v2/tasks/${taskId}`);
return true;
}
/**
* Add members to a task
* @param {string} taskId - Task GUID
* @param {string[]} userIds - Array of user_ids
* @returns {object} - Result
*/
export async function addTaskMembers(taskId, userIds) {
const members = userIds.map(userId => ({
type: 'user',
id: userId,
role: 'assignee'
}));
return larkApi('POST', `/task/v2/tasks/${taskId}/add_members`, {
params: { user_id_type: 'user_id' },
data: { members }
});
}
/**
* Remove members from a task
* @param {string} taskId - Task GUID
* @param {string[]} userIds - Array of user_ids
* @returns {object} - Result
*/
export async function removeTaskMembers(taskId, userIds) {
const members = userIds.map(userId => ({
type: 'user',
id: userId,
role: 'assignee'
}));
return larkApi('POST', `/task/v2/tasks/${taskId}/remove_members`, {
params: { user_id_type: 'user_id' },
data: { members }
});
}
/**
* Get a task by ID
* @param {string} taskId - Task GUID
* @returns {object} - Task data
*/
export async function getTask(taskId) {
const result = await larkApi('GET', `/task/v2/tasks/${taskId}`, {
params: { user_id_type: 'user_id' }
});
return result.task;
}
/**
* List tasks (with pagination)
* @param {object} options
* @param {number} [options.pageSize] - Number of tasks per page
* @param {string} [options.pageToken] - Pagination token
* @returns {object} - { items, page_token, has_more }
*/
export async function listTasks({ pageSize = 50, pageToken = '' } = {}) {
const params = { page_size: pageSize, user_id_type: 'user_id' };
if (pageToken) params.page_token = pageToken;
return larkApi('GET', '/task/v2/tasks', { params });
}
/**
* Complete a task
* @param {string} taskId - Task GUID
* @returns {object} - Updated task
*/
export async function completeTask(taskId) {
return updateTask({
taskId,
// Mark as completed by setting completed_at
// Note: Actual completion might need different API call
});
}

28
skills/lark-calendar/package-lock.json generated Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "lark-calendar",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lark-calendar",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"dotenv": "^17.2.3"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
}
}
}

View File

@@ -0,0 +1,19 @@
{
"name": "lark-calendar",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^17.2.3"
},
"type": "module"
}

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env node
/**
* Create a calendar event
*
* Usage:
* node create-event.mjs --title "Meeting" --start "2026-02-03 14:00:00" --end "2026-02-03 15:00:00"
*/
import { parseArgs } from 'util';
import { createEvent, DEFAULT_CALENDAR_ID, DEFAULT_TIMEZONE } from '../lib/calendar.mjs';
import { resolveNames, getDisplayNameSync } from '../lib/employees.mjs';
const { values } = parseArgs({
options: {
title: { type: 'string' },
description: { type: 'string', default: '' },
start: { type: 'string' },
end: { type: 'string' },
attendees: { type: 'string', default: '' },
'attendee-ids': { type: 'string', default: '' },
location: { type: 'string', default: '' },
timezone: { type: 'string', default: DEFAULT_TIMEZONE },
calendar: { type: 'string', default: DEFAULT_CALENDAR_ID },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Create a Lark calendar event
Options:
--title Event title (required)
--description Event description
--start Start time: YYYY-MM-DD HH:MM:SS (required)
--end End time: YYYY-MM-DD HH:MM:SS (required)
--attendees Comma-separated names (auto-resolved to user_ids)
--attendee-ids Comma-separated user_ids directly
--location Event location
--timezone IANA timezone (default: Asia/Singapore)
--calendar Calendar ID (uses default Claw calendar if omitted)
-h, --help Show this help
Examples:
node create-event.mjs --title "Team Sync" --start "2026-02-03 10:00:00" --end "2026-02-03 10:30:00"
node create-event.mjs --title "Review" --start "2026-02-03 14:00:00" --end "2026-02-03 15:00:00" --attendees "Boyang,RK,jc"
`);
process.exit(0);
}
// Validate required fields
if (!values.title) {
console.error('Error: --title is required');
process.exit(1);
}
if (!values.start) {
console.error('Error: --start is required');
process.exit(1);
}
if (!values.end) {
console.error('Error: --end is required');
process.exit(1);
}
// Resolve attendees
let attendeeIds = [];
if (values['attendee-ids']) {
attendeeIds = values['attendee-ids'].split(',').map(s => s.trim()).filter(Boolean);
}
if (values.attendees) {
const names = values.attendees.split(',').map(s => s.trim()).filter(Boolean);
const { resolved, unresolved } = resolveNames(names);
attendeeIds = [...new Set([...attendeeIds, ...resolved])];
if (unresolved.length > 0) {
console.warn(`Warning: Could not resolve names: ${unresolved.join(', ')}`);
}
}
async function main() {
try {
console.log('Creating event...');
console.log(` Title: ${values.title}`);
console.log(` Start: ${values.start}`);
console.log(` End: ${values.end}`);
console.log(` Timezone: ${values.timezone}`);
console.log(` Attendees: ${attendeeIds.map(id => getDisplayNameSync(id)).join(', ') || '(Boyang will be added automatically)'}`);
if (values.location) console.log(` Location: ${values.location}`);
console.log('');
const result = await createEvent({
title: values.title,
description: values.description,
startTime: values.start,
endTime: values.end,
attendeeIds,
location: values.location,
timezone: values.timezone,
calendarId: values.calendar
});
console.log('✅ Event created successfully!');
console.log('');
console.log('Event Details:');
console.log(` Event ID: ${result.event.event_id}`);
console.log(` Title: ${result.event.summary}`);
console.log(` Link: ${result.event.app_link || 'N/A'}`);
console.log(` Attendees: ${result.attendeeNames || 'N/A'}`);
// Output JSON for programmatic use
if (process.env.JSON_OUTPUT) {
console.log('\nJSON:');
console.log(JSON.stringify(result, null, 2));
}
} catch (error) {
console.error('❌ Failed to create event:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env node
/**
* Create a task
*
* Usage:
* node create-task.mjs --title "Review PR" --due "2026-02-05 18:00:00"
*/
import { parseArgs } from 'util';
import { createTask } from '../lib/task.mjs';
import { DEFAULT_TIMEZONE } from '../lib/calendar.mjs';
import { resolveNames, getDisplayNameSync } from '../lib/employees.mjs';
const { values } = parseArgs({
options: {
title: { type: 'string' },
description: { type: 'string', default: '' },
due: { type: 'string' },
assignees: { type: 'string', default: '' },
'assignee-ids': { type: 'string', default: '' },
timezone: { type: 'string', default: DEFAULT_TIMEZONE },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Create a Lark task
Options:
--title Task title (required)
--description Task description
--due Due time: YYYY-MM-DD HH:MM:SS (required)
--assignees Comma-separated names (auto-resolved to user_ids)
--assignee-ids Comma-separated user_ids directly
--timezone IANA timezone (default: Asia/Singapore)
-h, --help Show this help
Examples:
node create-task.mjs --title "Review PR #123" --due "2026-02-05 18:00:00"
node create-task.mjs --title "Finish report" --due "2026-02-03 17:00:00" --assignees "Boyang,jc"
`);
process.exit(0);
}
// Validate required fields
if (!values.title) {
console.error('Error: --title is required');
process.exit(1);
}
if (!values.due) {
console.error('Error: --due is required');
process.exit(1);
}
// Resolve assignees
let assigneeIds = [];
if (values['assignee-ids']) {
assigneeIds = values['assignee-ids'].split(',').map(s => s.trim()).filter(Boolean);
}
if (values.assignees) {
const names = values.assignees.split(',').map(s => s.trim()).filter(Boolean);
const { resolved, unresolved } = resolveNames(names);
assigneeIds = [...new Set([...assigneeIds, ...resolved])];
if (unresolved.length > 0) {
console.warn(`Warning: Could not resolve names: ${unresolved.join(', ')}`);
}
}
async function main() {
try {
console.log('Creating task...');
console.log(` Title: ${values.title}`);
console.log(` Due: ${values.due}`);
console.log(` Timezone: ${values.timezone}`);
console.log(` Assignees: ${assigneeIds.map(id => getDisplayNameSync(id)).join(', ') || '(none)'}`);
console.log('');
const task = await createTask({
title: values.title,
description: values.description,
dueTime: values.due,
assigneeIds,
timezone: values.timezone
});
console.log('✅ Task created successfully!');
console.log('');
console.log('Task Details:');
console.log(` Task ID: ${task.guid}`);
console.log(` Title: ${task.summary}`);
console.log(` URL: ${task.url || 'N/A'}`);
// Output JSON for programmatic use
if (process.env.JSON_OUTPUT) {
console.log('\nJSON:');
console.log(JSON.stringify(task, null, 2));
}
} catch (error) {
console.error('❌ Failed to create task:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env node
/**
* Delete a calendar event
*
* Usage:
* node delete-event.mjs --event-id "f9900f6b-b472-4b17-a818-7b5584abdc37_0"
*/
import { parseArgs } from 'util';
import { deleteEvent, DEFAULT_CALENDAR_ID } from '../lib/calendar.mjs';
const { values } = parseArgs({
options: {
'event-id': { type: 'string' },
calendar: { type: 'string', default: DEFAULT_CALENDAR_ID },
'no-notify': { type: 'boolean', default: false },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Delete a Lark calendar event
Options:
--event-id Event ID to delete (required)
--calendar Calendar ID
--no-notify Don't send notifications to attendees
-h, --help Show this help
Examples:
node delete-event.mjs --event-id "f9900f6b-b472-4b17-a818-7b5584abdc37_0"
`);
process.exit(0);
}
if (!values['event-id']) {
console.error('Error: --event-id is required');
process.exit(1);
}
async function main() {
try {
console.log(`Deleting event: ${values['event-id']}`);
await deleteEvent(values['event-id'], values.calendar, !values['no-notify']);
console.log('✅ Event deleted successfully!');
} catch (error) {
console.error('❌ Failed to delete event:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* Delete a task
*
* Usage:
* node delete-task.mjs --task-id "35fc5310-a1b1-49c7-be75-be631d3079ee"
*/
import { parseArgs } from 'util';
import { deleteTask } from '../lib/task.mjs';
const { values } = parseArgs({
options: {
'task-id': { type: 'string' },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Delete a Lark task
Options:
--task-id Task GUID to delete (required)
-h, --help Show this help
Examples:
node delete-task.mjs --task-id "35fc5310-a1b1-49c7-be75-be631d3079ee"
`);
process.exit(0);
}
if (!values['task-id']) {
console.error('Error: --task-id is required');
process.exit(1);
}
async function main() {
try {
console.log(`Deleting task: ${values['task-id']}`);
await deleteTask(values['task-id']);
console.log('✅ Task deleted successfully!');
} catch (error) {
console.error('❌ Failed to delete task:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env node
/**
* List calendar events
*
* Usage:
* node list-events.mjs
* node list-events.mjs --start "2026-02-01" --end "2026-02-28"
*/
import { parseArgs } from 'util';
import { listEvents, timestampToDatetime, DEFAULT_CALENDAR_ID, DEFAULT_TIMEZONE } from '../lib/calendar.mjs';
const { values } = parseArgs({
options: {
start: { type: 'string' },
end: { type: 'string' },
timezone: { type: 'string', default: DEFAULT_TIMEZONE },
calendar: { type: 'string', default: DEFAULT_CALENDAR_ID },
json: { type: 'boolean' },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
List Lark calendar events
Options:
--start Start date: YYYY-MM-DD (default: today)
--end End date: YYYY-MM-DD (default: 7 days from now)
--timezone IANA timezone (default: Asia/Singapore)
--calendar Calendar ID
--json Output as JSON
-h, --help Show this help
Examples:
node list-events.mjs
node list-events.mjs --start "2026-02-01" --end "2026-02-28"
`);
process.exit(0);
}
async function main() {
try {
const events = await listEvents({
calendarId: values.calendar,
startTime: values.start,
endTime: values.end,
timezone: values.timezone
});
if (values.json) {
console.log(JSON.stringify(events, null, 2));
return;
}
if (events.length === 0) {
console.log('No events found in the specified range.');
return;
}
console.log(`Found ${events.length} event(s):\n`);
for (const event of events) {
const startTime = event.start_time?.timestamp
? timestampToDatetime(parseInt(event.start_time.timestamp), values.timezone)
: 'N/A';
const endTime = event.end_time?.timestamp
? timestampToDatetime(parseInt(event.end_time.timestamp), values.timezone)
: 'N/A';
console.log(`📅 ${event.summary || '(No title)'}`);
console.log(` ID: ${event.event_id}`);
console.log(` Time: ${startTime}${endTime}`);
if (event.location?.name) {
console.log(` Location: ${event.location.name}`);
}
if (event.description) {
console.log(` Description: ${event.description.substring(0, 100)}${event.description.length > 100 ? '...' : ''}`);
}
console.log('');
}
} catch (error) {
console.error('❌ Failed to list events:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env node
/**
* Add or remove attendees from a calendar event
*
* Usage:
* node manage-attendees.mjs --event-id "xxx" --add "RK,jc"
* node manage-attendees.mjs --event-id "xxx" --remove "jc"
*/
import { parseArgs } from 'util';
import { addEventAttendees, removeEventAttendees, DEFAULT_CALENDAR_ID } from '../lib/calendar.mjs';
import { resolveNames, getDisplayNameSync } from '../lib/employees.mjs';
const { values } = parseArgs({
options: {
'event-id': { type: 'string' },
add: { type: 'string' },
remove: { type: 'string' },
'user-ids': { type: 'string' },
calendar: { type: 'string', default: DEFAULT_CALENDAR_ID },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Manage calendar event attendees
Options:
--event-id Event ID (required)
--add Comma-separated names to add
--remove Comma-separated names to remove
--user-ids Comma-separated user_ids directly (use with --add or --remove)
--calendar Calendar ID
-h, --help Show this help
Examples:
node manage-attendees.mjs --event-id "xxx" --add "RK,jc"
node manage-attendees.mjs --event-id "xxx" --remove "jc"
`);
process.exit(0);
}
if (!values['event-id']) {
console.error('Error: --event-id is required');
process.exit(1);
}
if (!values.add && !values.remove) {
console.error('Error: Either --add or --remove is required');
process.exit(1);
}
async function main() {
try {
const calendarId = values.calendar;
const eventId = values['event-id'];
if (values.add) {
const names = values.add.split(',').map(s => s.trim()).filter(Boolean);
const { resolved, unresolved } = resolveNames(names);
if (values['user-ids']) {
resolved.push(...values['user-ids'].split(',').map(s => s.trim()).filter(Boolean));
}
if (unresolved.length > 0) {
console.warn(`Warning: Could not resolve names: ${unresolved.join(', ')}`);
}
if (resolved.length === 0) {
console.error('No valid attendees to add');
process.exit(1);
}
console.log(`Adding attendees: ${resolved.map(id => getDisplayNameSync(id)).join(', ')}`);
const result = await addEventAttendees(calendarId, eventId, resolved);
console.log('✅ Attendees added successfully!');
console.log(`Updated attendees: ${(result.attendees || []).map(a => a.display_name).join(', ')}`);
}
if (values.remove) {
const names = values.remove.split(',').map(s => s.trim()).filter(Boolean);
const { resolved, unresolved } = resolveNames(names);
if (unresolved.length > 0) {
console.warn(`Warning: Could not resolve names: ${unresolved.join(', ')}`);
}
if (resolved.length === 0) {
console.error('No valid attendees to remove');
process.exit(1);
}
console.log(`Removing attendees: ${resolved.map(id => getDisplayNameSync(id)).join(', ')}`);
await removeEventAttendees(calendarId, eventId, resolved);
console.log('✅ Attendees removed successfully!');
}
} catch (error) {
console.error('❌ Failed:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* Add or remove members from a task
*
* Usage:
* node manage-task-members.mjs --task-id "xxx" --add "RK,jc"
* node manage-task-members.mjs --task-id "xxx" --remove "jc"
*/
import { parseArgs } from 'util';
import { addTaskMembers, removeTaskMembers } from '../lib/task.mjs';
import { resolveNames, getDisplayNameSync } from '../lib/employees.mjs';
const { values } = parseArgs({
options: {
'task-id': { type: 'string' },
add: { type: 'string' },
remove: { type: 'string' },
'user-ids': { type: 'string' },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Manage task members/assignees
Options:
--task-id Task GUID (required)
--add Comma-separated names to add
--remove Comma-separated names to remove
--user-ids Comma-separated user_ids directly (use with --add or --remove)
-h, --help Show this help
Examples:
node manage-task-members.mjs --task-id "xxx" --add "RK,jc"
node manage-task-members.mjs --task-id "xxx" --remove "jc"
`);
process.exit(0);
}
if (!values['task-id']) {
console.error('Error: --task-id is required');
process.exit(1);
}
if (!values.add && !values.remove) {
console.error('Error: Either --add or --remove is required');
process.exit(1);
}
async function main() {
try {
const taskId = values['task-id'];
if (values.add) {
const names = values.add.split(',').map(s => s.trim()).filter(Boolean);
const { resolved, unresolved } = resolveNames(names);
if (values['user-ids']) {
resolved.push(...values['user-ids'].split(',').map(s => s.trim()).filter(Boolean));
}
if (unresolved.length > 0) {
console.warn(`Warning: Could not resolve names: ${unresolved.join(', ')}`);
}
if (resolved.length === 0) {
console.error('No valid members to add');
process.exit(1);
}
console.log(`Adding members: ${resolved.map(id => getDisplayNameSync(id)).join(', ')}`);
await addTaskMembers(taskId, resolved);
console.log('✅ Members added successfully!');
}
if (values.remove) {
const names = values.remove.split(',').map(s => s.trim()).filter(Boolean);
const { resolved, unresolved } = resolveNames(names);
if (unresolved.length > 0) {
console.warn(`Warning: Could not resolve names: ${unresolved.join(', ')}`);
}
if (resolved.length === 0) {
console.error('No valid members to remove');
process.exit(1);
}
console.log(`Removing members: ${resolved.map(id => getDisplayNameSync(id)).join(', ')}`);
await removeTaskMembers(taskId, resolved);
console.log('✅ Members removed successfully!');
}
} catch (error) {
console.error('❌ Failed:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env node
/**
* Update a calendar event
*
* Usage:
* node update-event.mjs --event-id "xxx" --title "New Title"
*/
import { parseArgs } from 'util';
import { updateEvent, DEFAULT_CALENDAR_ID, DEFAULT_TIMEZONE } from '../lib/calendar.mjs';
const { values } = parseArgs({
options: {
'event-id': { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
start: { type: 'string' },
end: { type: 'string' },
location: { type: 'string' },
timezone: { type: 'string', default: DEFAULT_TIMEZONE },
calendar: { type: 'string', default: DEFAULT_CALENDAR_ID },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Update a Lark calendar event
Options:
--event-id Event ID to update (required)
--title New event title
--description New event description
--start New start time: YYYY-MM-DD HH:MM:SS
--end New end time: YYYY-MM-DD HH:MM:SS
--location New event location
--timezone IANA timezone (default: Asia/Singapore)
--calendar Calendar ID
-h, --help Show this help
Examples:
node update-event.mjs --event-id "xxx" --title "Updated Meeting"
node update-event.mjs --event-id "xxx" --start "2026-02-03 15:00:00" --end "2026-02-03 16:00:00"
`);
process.exit(0);
}
if (!values['event-id']) {
console.error('Error: --event-id is required');
process.exit(1);
}
async function main() {
try {
console.log(`Updating event: ${values['event-id']}`);
const event = await updateEvent({
eventId: values['event-id'],
title: values.title,
description: values.description,
startTime: values.start,
endTime: values.end,
location: values.location,
timezone: values.timezone,
calendarId: values.calendar
});
console.log('✅ Event updated successfully!');
console.log('');
console.log('Updated Event:');
console.log(` Title: ${event.summary}`);
console.log(` Event ID: ${event.event_id}`);
} catch (error) {
console.error('❌ Failed to update event:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env node
/**
* Update a task
*
* Usage:
* node update-task.mjs --task-id "xxx" --title "New Title"
*/
import { parseArgs } from 'util';
import { updateTask } from '../lib/task.mjs';
import { DEFAULT_TIMEZONE } from '../lib/calendar.mjs';
const { values } = parseArgs({
options: {
'task-id': { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
due: { type: 'string' },
timezone: { type: 'string', default: DEFAULT_TIMEZONE },
help: { type: 'boolean', short: 'h' }
}
});
if (values.help) {
console.log(`
Update a Lark task
Options:
--task-id Task GUID to update (required)
--title New task title
--description New task description
--due New due time: YYYY-MM-DD HH:MM:SS
--timezone IANA timezone (default: Asia/Singapore)
-h, --help Show this help
Examples:
node update-task.mjs --task-id "xxx" --title "Updated Task"
node update-task.mjs --task-id "xxx" --due "2026-02-06 18:00:00"
`);
process.exit(0);
}
if (!values['task-id']) {
console.error('Error: --task-id is required');
process.exit(1);
}
async function main() {
try {
console.log(`Updating task: ${values['task-id']}`);
const task = await updateTask({
taskId: values['task-id'],
title: values.title,
description: values.description,
dueTime: values.due,
timezone: values.timezone
});
console.log('✅ Task updated successfully!');
console.log('');
console.log('Updated Task:');
console.log(` Title: ${task.summary}`);
console.log(` Task ID: ${task.guid}`);
} catch (error) {
console.error('❌ Failed to update task:', error.message);
if (error.larkResponse) {
console.error('Lark response:', JSON.stringify(error.larkResponse, null, 2));
}
process.exit(1);
}
}
main();