Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 13 additions & 7 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,17 @@ export class TimeTreeAPIClient {
private async syncEvents(
calendarId: string,
since: number = 0,
accumulated: Event[] = []
accumulated: Event[] = [],
startAt?: number,
endAt?: number,
): Promise<Event[]> {
const url = `${TIMETREE_CONFIG.BASE_URL}${TIMETREE_CONFIG.ENDPOINTS.EVENTS_SYNC(
let url = `${TIMETREE_CONFIG.BASE_URL}${TIMETREE_CONFIG.ENDPOINTS.EVENTS_SYNC(
calendarId
)}?since=${since}`;
if (startAt !== undefined) url += `&start_at=${startAt}`;
if (endAt !== undefined) url += `&end_at=${endAt}`;

logger.debug('Syncing events', { calendarId, since, accumulated: accumulated.length });
logger.debug('Syncing events', { calendarId, since, startAt, endAt, accumulated: accumulated.length });

try {
const response = await this.rateLimiter.executeWithRetry(async () => {
Expand All @@ -156,7 +160,7 @@ export class TimeTreeAPIClient {
nextSince: validated.since,
fetchedSoFar: accumulated.length,
});
return this.syncEvents(calendarId, validated.since, accumulated);
return this.syncEvents(calendarId, validated.since, accumulated, startAt, endAt);
}

logger.debug('Event sync complete', {
Expand All @@ -179,13 +183,15 @@ export class TimeTreeAPIClient {
/** Get all events from a calendar (handles pagination automatically). */
async getEventsByCalendar(
calendarId: string,
since: number = 0
since: number = 0,
startAt?: number,
endAt?: number,
): Promise<Event[]> {
await this.ensureAuthenticated();

logger.info('Fetching events for calendar', { calendarId, since });
logger.info('Fetching events for calendar', { calendarId, since, startAt, endAt });

const events = await this.syncEvents(calendarId, since);
const events = await this.syncEvents(calendarId, since, [], startAt, endAt);

logger.info('Events fetched successfully', {
calendarId,
Expand Down
37 changes: 30 additions & 7 deletions src/tools/event-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export const GetEventsInputSchema = z.object({
.describe(
'Optional Unix timestamp in milliseconds. Only return events starting after this time.'
),
end_before: z
.number()
.optional()
.describe(
'Optional Unix timestamp in milliseconds. Only return events starting before this time.'
),
limit: z
.number()
.optional()
Expand All @@ -38,7 +44,8 @@ export function createGetEventsTool(apiClient: TimeTreeAPIClient) {
return {
name: 'get_events',
description:
'Get all events from a specific TimeTree calendar. Automatically handles pagination to fetch all events. ' +
'Get events from a specific TimeTree calendar. Automatically handles pagination. ' +
'Use start_after and end_before to filter by date range (both are optional). ' +
'Returns event details including title, start/end times, location, notes, label color, and more. ' +
'Label colors (label_id 1-10): 1=Emerald green, 2=Modern cyan, 3=Deep sky blue, 4=Pastel brown, ' +
'5=Midnight black, 6=Apple red, 7=French rose, 8=Coral pink, 9=Bright orange, 10=Soft violet.',
Expand All @@ -53,7 +60,13 @@ export function createGetEventsTool(apiClient: TimeTreeAPIClient) {
type: 'number',
description:
'Optional Unix timestamp in milliseconds. Only return events starting after this time. ' +
'If user provides a date like "2026-02-01", convert it to Unix timestamp (e.g., 1769904000000).',
'If user provides a date like "2026-02-01", convert it to Unix timestamp (e.g., 1738368000000).',
},
end_before: {
type: 'number',
description:
'Optional Unix timestamp in milliseconds. Only return events starting before this time. ' +
'If user provides a date like "2026-12-31", convert it to Unix timestamp (e.g., 1767139200000).',
},
limit: {
type: 'number',
Expand All @@ -65,16 +78,26 @@ export function createGetEventsTool(apiClient: TimeTreeAPIClient) {
handler: async (args: unknown) => {
try {
const input = GetEventsInputSchema.parse(args);
const { calendar_id, start_after, limit } = input;
const { calendar_id, start_after, end_before, limit } = input;

logger.info('Tool: get_events called', { calendar_id, start_after, limit });
logger.info('Tool: get_events called', { calendar_id, start_after, end_before, limit });

const events = await apiClient.getEventsByCalendar(calendar_id, 0);
// Pass date range to the API — the server may use start_at/end_at to narrow
// the time window it returns. Client-side filters below act as a guaranteed fallback.
const events = await apiClient.getEventsByCalendar(
calendar_id,
0,
start_after,
end_before,
);

// Filter by start_after if provided
// Client-side date filters (always applied regardless of server-side support)
let filteredEvents = events;
if (start_after) {
filteredEvents = events.filter((event) => event.start_at > start_after);
filteredEvents = filteredEvents.filter((event) => event.start_at > start_after);
}
if (end_before) {
filteredEvents = filteredEvents.filter((event) => event.start_at < end_before);
}

// Limit results if provided
Expand Down
46 changes: 24 additions & 22 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,47 @@ import {
createGetCalendarLabelsTool,
createGetCalendarMembersTool,
createGetCalendarVirtualMembersTool,
createUpdateCalendarLabelsTool,
// createUpdateCalendarLabelsTool, // 쓰기 기능 비활성화 (조회 전용)
} from './calendar-metadata-tools.js';
import {
createAddEventCommentTool,
createDeleteEventCommentTool,
// createAddEventCommentTool, // 쓰기 기능 비활성화 (조회 전용)
// createDeleteEventCommentTool, // 쓰기 기능 비활성화 (조회 전용)
createListEventCommentsTool,
createUpdateEventCommentTool,
// createUpdateEventCommentTool, // 쓰기 기능 비활성화 (조회 전용)
} from './comment-tools.js';
import {
createCreateEventTool,
createUpdateEventTool,
createDeleteEventTool,
} from './event-crud-tools.js';
// 쓰기 기능 비활성화 (조회 전용): 이벤트 생성/수정/삭제 import 전체 주석 처리
// import {
// createCreateEventTool,
// createUpdateEventTool,
// createDeleteEventTool,
// } from './event-crud-tools.js';
import { createGetEventsTool, createGetUpdatedEventsTool } from './event-tools.js';
import {
createCreateMemoTool,
createDeleteMemoTool,
// createCreateMemoTool, // 쓰기 기능 비활성화 (조회 전용)
// createDeleteMemoTool, // 쓰기 기능 비활성화 (조회 전용)
createListMemosTool,
createUpdateMemoTool,
// createUpdateMemoTool, // 쓰기 기능 비활성화 (조회 전용)
} from './memo-tools.js';

export function registerTools(apiClient: TimeTreeAPIClient) {
return [
createListCalendarsTool(apiClient),
createGetEventsTool(apiClient),
createGetUpdatedEventsTool(apiClient),
createCreateEventTool(apiClient),
createUpdateEventTool(apiClient),
createDeleteEventTool(apiClient),
// 쓰기 기능 비활성화 (조회 전용)
// createCreateEventTool(apiClient),
// createUpdateEventTool(apiClient),
// createDeleteEventTool(apiClient),
createListMemosTool(apiClient),
createCreateMemoTool(apiClient),
createUpdateMemoTool(apiClient),
createDeleteMemoTool(apiClient),
createAddEventCommentTool(apiClient),
// createCreateMemoTool(apiClient),
// createUpdateMemoTool(apiClient),
// createDeleteMemoTool(apiClient),
// createAddEventCommentTool(apiClient),
createListEventCommentsTool(apiClient),
createUpdateEventCommentTool(apiClient),
createDeleteEventCommentTool(apiClient),
// createUpdateEventCommentTool(apiClient),
// createDeleteEventCommentTool(apiClient),
createGetCalendarLabelsTool(apiClient),
createUpdateCalendarLabelsTool(apiClient),
// createUpdateCalendarLabelsTool(apiClient),
createGetCalendarMembersTool(apiClient),
createGetCalendarVirtualMembersTool(apiClient),
];
Expand Down
2 changes: 1 addition & 1 deletion src/types/timetree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export type EventAttachmentInput = z.infer<typeof EventAttachmentInputSchema>;
export const CalendarUserSchema = z.object({
id: z.number(),
user_id: z.number(),
name: z.string(),
name: z.string().optional().nullable(), // upstream may return null (e.g. deactivated users)
role: z.number().optional(), // 1 = owner, 0 = member
deactivated_at: z.number().nullable().optional(),
birth_day: z.number().optional().nullable(),
Expand Down
24 changes: 24 additions & 0 deletions tests/api-client.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,27 @@ test('event comment methods use activity endpoints and filter comment activities
assert.ok(calls.some((call) => call.url === 'https://timetreeapp.com/api/v1/calendar/123/event/evt/activities'));
assert.ok(calls.some((call) => call.url === 'https://timetreeapp.com/api/v1/calendar/123/event/evt/activity/comment'));
});

test('getCalendars tolerates calendar_users with null name (deactivated users)', async () => {
const client = makeClient({
get: async () => ({
calendars: [
{
id: 1,
name: 'Active calendar',
calendar_users: [
{ id: 1, user_id: 1, name: 'Alice', role: 1 },
// deactivated member: upstream returns null name
{ id: 2, user_id: 2, name: null, deactivated_at: 1700000000000 },
],
},
],
}),
});

const calendars = await client.getCalendars();

assert.equal(calendars.length, 1);
assert.equal(calendars[0].name, 'Active calendar');
assert.equal(calendars[0].calendar_users[1].name, null);
});
14 changes: 3 additions & 11 deletions tests/tools.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,12 @@ function parseToolText(result) {
return JSON.parse(result.content[0].text);
}

test('registerTools exposes baseline plus Wave 1 tools', () => {
test('registerTools exposes read-only tools (write tools disabled)', () => {
const names = registerTools({}).map((tool) => tool.name).sort();

// Read-only build: write tools (create/update/delete/add) are commented out
// in src/tools/index.ts. Only listing/getting tools are registered.
assert.deepEqual(names, [
'add_event_comment',
'create_event',
'create_memo',
'delete_event',
'delete_event_comment',
'delete_memo',
'get_calendar_labels',
'get_calendar_members',
'get_calendar_virtual_members',
Expand All @@ -49,10 +45,6 @@ test('registerTools exposes baseline plus Wave 1 tools', () => {
'list_calendars',
'list_event_comments',
'list_memos',
'update_calendar_labels',
'update_event',
'update_event_comment',
'update_memo',
].sort());
});

Expand Down