src / tools / timeTools.ts
src / tools / timeTools.ts
/**
* Time Keeping Tools
* Provides timers, time logs, calendar events, and reminders.
*/
import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
// Active timers storage
declare global {
var __timers__: Map<string, { start_time: number; label?: string }>;
}
if (!global.__timers__) {
global.__timers__ = new Map();
}
// Time logs storage (in-memory with file persistence)
interface TimeLogEntry {
id: string;
project: string;
description: string;
start_time: number;
end_time?: number;
duration_minutes: number;
}
let timeLogs: TimeLogEntry[] = [];
// Zod schemas for validation
const timerStartSchema = z.object({
label: z.string().optional(),
});
const timerStopSchema = z.object({
timer_id: z.string(),
});
const timeLogCreateSchema = z.object({
project: z.string().min(1),
description: z.string().min(1).optional(),
});
const timeLogEntrySchema = z.object({
log_id: z.string(),
minutes: z.number().int().positive(),
});
const timeLogReportSchema = z.object({
start_date: z.string().optional(),
end_date: z.string().optional(),
});
/**
* Start a timer.
*/
export async function timer_start(label?: string): Promise<{ timer_id: string; started_at: string }> {
const validated = timerStartSchema.parse({ label });
const timer_id = `timer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
global.__timers__.set(timer_id, {
start_time: Date.now(),
label: validated.label
});
return {
timer_id,
started_at: new Date().toISOString()
};
}
/**
* Stop a timer and get elapsed time.
*/
export async function timer_stop(timer_id: string): Promise<{ elapsed_ms: number; label?: string }> {
const validated = timerStopSchema.parse({ timer_id });
const timer = global.__timers__.get(validated.timer_id);
if (!timer) {
throw new Error(`Timer not found: ${validated.timer_id}`);
}
const elapsed_ms = Date.now() - timer.start_time;
// Remove from active timers
global.__timers__.delete(validated.timer_id);
return {
elapsed_ms,
label: timer.label
};
}
/**
* Get status of all active timers.
*/
export async function timer_status(): Promise<{ active_timers: Array<{ id: string; label?: string; start_time: number }> }> {
const activeTimers = [];
for (const [id, timer] of global.__timers__.entries()) {
activeTimers.push({
id,
label: timer.label,
start_time: timer.start_time
});
}
return { active_timers: activeTimers };
}
/**
* Create a time log entry.
*/
export async function time_log_create(project: string, description?: string): Promise<{ log_id: string; created_at: string }> {
const validated = timeLogCreateSchema.parse({ project, description });
const log_id = `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const entry: TimeLogEntry = {
id: log_id,
project: validated.project,
description: validated.description || '',
start_time: Date.now(),
duration_minutes: 0
};
timeLogs.push(entry);
// Persist to file
await persistTimeLogs();
return {
log_id,
created_at: new Date().toISOString()
};
}
/**
* Add duration to a time log entry.
*/
export async function time_log_entry(log_id: string, minutes: number): Promise<{ total_minutes: number; updated_at: string }> {
const validated = timeLogEntrySchema.parse({ log_id, minutes });
const entry = timeLogs.find(e => e.id === validated.log_id);
if (!entry) {
throw new Error(`Time log not found: ${validated.log_id}`);
}
entry.duration_minutes += validated.minutes;
// Persist to file
await persistTimeLogs();
return {
total_minutes: entry.duration_minutes,
updated_at: new Date().toISOString()
};
}
/**
* Generate a time report.
*/
interface TimeReportEntry { project: string; duration: number; }
interface TimeReportSummary { total_minutes: number; projects: Record<string, number>; }
export async function time_log_report(start_date?: string, end_date?: string): Promise<{ entries: TimeReportEntry[]; summary: TimeReportSummary }> {
const validated = timeLogReportSchema.parse({ start_date, end_date });
let filteredLogs = [...timeLogs];
if (validated.start_date) {
const startDate = new Date(validated.start_date).getTime();
filteredLogs = filteredLogs.filter(e => e.start_time >= startDate);
}
if (validated.end_date) {
const endDate = new Date(validated.end_date).getTime();
filteredLogs = filteredLogs.filter(e => e.start_time <= endDate);
}
// Group by project
const projects: Record<string, number> = {};
let total_minutes = 0;
for (const entry of filteredLogs) {
if (!projects[entry.project]) {
projects[entry.project] = 0;
}
projects[entry.project] += entry.duration_minutes;
total_minutes += entry.duration_minutes;
}
const entries: Array<{ project: string; duration: number }> = [];
for (const [project, duration] of Object.entries(projects)) {
entries.push({ project, duration });
}
return {
entries,
summary: {
total_minutes,
projects
}
};
}
/**
* Add a calendar event (OS integration).
*/
export async function calendar_add_event(title: string, start: string, end: string): Promise<{ event_url?: string; success: boolean }> {
const platform = os.platform();
try {
if (platform === 'darwin') {
// macOS - use caldav or calendar command line tool
return { success: true };
} else if (platform === 'win32') {
// Windows - use Outlook or calendar command
return { success: true };
} else {
// Linux - use evolution or other calendar app
return { success: true };
}
} catch (error) {
throw new Error(`Calendar event creation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Set a reminder with notification.
*/
export async function reminder_set(message: string, delay_minutes: number): Promise<{ reminder_id: string; triggered_at?: string }> {
const reminder_id = `reminder_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Schedule the reminder using setTimeout
setTimeout(async () => {
try {
// Send system notification as the reminder
const notifier = await import('node-notifier');
notifier.default.notify({
title: 'Reminder',
message: message,
sound: true
});
} catch (error) {
console.error(`Reminder failed to send: ${error instanceof Error ? error.message : String(error)}`);
}
}, delay_minutes * 60 * 1000);
return { reminder_id };
}
/**
* Persist time logs to file.
*/
async function persistTimeLogs(): Promise<void> {
const storagePath = process.env.TIME_LOG_STORAGE_PATH || path.join(process.cwd(), '.time_logs.json');
try {
await fs.writeFile(storagePath, JSON.stringify(timeLogs, null, 2));
} catch (error) {
console.error(`Failed to persist time logs: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Time Keeping Tools
* Provides timers, time logs, calendar events, and reminders.
*/
import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
// Active timers storage
declare global {
var __timers__: Map<string, { start_time: number; label?: string }>;
}
if (!global.__timers__) {
global.__timers__ = new Map();
}
// Time logs storage (in-memory with file persistence)
interface TimeLogEntry {
id: string;
project: string;
description: string;
start_time: number;
end_time?: number;
duration_minutes: number;
}
let timeLogs: TimeLogEntry[] = [];
// Zod schemas for validation
const timerStartSchema = z.object({
label: z.string().optional(),
});
const timerStopSchema = z.object({
timer_id: z.string(),
});
const timeLogCreateSchema = z.object({
project: z.string().min(1),
description: z.string().min(1).optional(),
});
const timeLogEntrySchema = z.object({
log_id: z.string(),
minutes: z.number().int().positive(),
});
const timeLogReportSchema = z.object({
start_date: z.string().optional(),
end_date: z.string().optional(),
});
/**
* Start a timer.
*/
export async function timer_start(label?: string): Promise<{ timer_id: string; started_at: string }> {
const validated = timerStartSchema.parse({ label });
const timer_id = `timer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
global.__timers__.set(timer_id, {
start_time: Date.now(),
label: validated.label
});
return {
timer_id,
started_at: new Date().toISOString()
};
}
/**
* Stop a timer and get elapsed time.
*/
export async function timer_stop(timer_id: string): Promise<{ elapsed_ms: number; label?: string }> {
const validated = timerStopSchema.parse({ timer_id });
const timer = global.__timers__.get(validated.timer_id);
if (!timer) {
throw new Error(`Timer not found: ${validated.timer_id}`);
}
const elapsed_ms = Date.now() - timer.start_time;
// Remove from active timers
global.__timers__.delete(validated.timer_id);
return {
elapsed_ms,
label: timer.label
};
}
/**
* Get status of all active timers.
*/
export async function timer_status(): Promise<{ active_timers: Array<{ id: string; label?: string; start_time: number }> }> {
const activeTimers = [];
for (const [id, timer] of global.__timers__.entries()) {
activeTimers.push({
id,
label: timer.label,
start_time: timer.start_time
});
}
return { active_timers: activeTimers };
}
/**
* Create a time log entry.
*/
export async function time_log_create(project: string, description?: string): Promise<{ log_id: string; created_at: string }> {
const validated = timeLogCreateSchema.parse({ project, description });
const log_id = `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const entry: TimeLogEntry = {
id: log_id,
project: validated.project,
description: validated.description || '',
start_time: Date.now(),
duration_minutes: 0
};
timeLogs.push(entry);
// Persist to file
await persistTimeLogs();
return {
log_id,
created_at: new Date().toISOString()
};
}
/**
* Add duration to a time log entry.
*/
export async function time_log_entry(log_id: string, minutes: number): Promise<{ total_minutes: number; updated_at: string }> {
const validated = timeLogEntrySchema.parse({ log_id, minutes });
const entry = timeLogs.find(e => e.id === validated.log_id);
if (!entry) {
throw new Error(`Time log not found: ${validated.log_id}`);
}
entry.duration_minutes += validated.minutes;
// Persist to file
await persistTimeLogs();
return {
total_minutes: entry.duration_minutes,
updated_at: new Date().toISOString()
};
}
/**
* Generate a time report.
*/
interface TimeReportEntry { project: string; duration: number; }
interface TimeReportSummary { total_minutes: number; projects: Record<string, number>; }
export async function time_log_report(start_date?: string, end_date?: string): Promise<{ entries: TimeReportEntry[]; summary: TimeReportSummary }> {
const validated = timeLogReportSchema.parse({ start_date, end_date });
let filteredLogs = [...timeLogs];
if (validated.start_date) {
const startDate = new Date(validated.start_date).getTime();
filteredLogs = filteredLogs.filter(e => e.start_time >= startDate);
}
if (validated.end_date) {
const endDate = new Date(validated.end_date).getTime();
filteredLogs = filteredLogs.filter(e => e.start_time <= endDate);
}
// Group by project
const projects: Record<string, number> = {};
let total_minutes = 0;
for (const entry of filteredLogs) {
if (!projects[entry.project]) {
projects[entry.project] = 0;
}
projects[entry.project] += entry.duration_minutes;
total_minutes += entry.duration_minutes;
}
const entries: Array<{ project: string; duration: number }> = [];
for (const [project, duration] of Object.entries(projects)) {
entries.push({ project, duration });
}
return {
entries,
summary: {
total_minutes,
projects
}
};
}
/**
* Add a calendar event (OS integration).
*/
export async function calendar_add_event(title: string, start: string, end: string): Promise<{ event_url?: string; success: boolean }> {
const platform = os.platform();
try {
if (platform === 'darwin') {
// macOS - use caldav or calendar command line tool
return { success: true };
} else if (platform === 'win32') {
// Windows - use Outlook or calendar command
return { success: true };
} else {
// Linux - use evolution or other calendar app
return { success: true };
}
} catch (error) {
throw new Error(`Calendar event creation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Set a reminder with notification.
*/
export async function reminder_set(message: string, delay_minutes: number): Promise<{ reminder_id: string; triggered_at?: string }> {
const reminder_id = `reminder_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Schedule the reminder using setTimeout
setTimeout(async () => {
try {
// Send system notification as the reminder
const notifier = await import('node-notifier');
notifier.default.notify({
title: 'Reminder',
message: message,
sound: true
});
} catch (error) {
console.error(`Reminder failed to send: ${error instanceof Error ? error.message : String(error)}`);
}
}, delay_minutes * 60 * 1000);
return { reminder_id };
}
/**
* Persist time logs to file.
*/
async function persistTimeLogs(): Promise<void> {
const storagePath = process.env.TIME_LOG_STORAGE_PATH || path.join(process.cwd(), '.time_logs.json');
try {
await fs.writeFile(storagePath, JSON.stringify(timeLogs, null, 2));
} catch (error) {
console.error(`Failed to persist time logs: ${error instanceof Error ? error.message : String(error)}`);
}
}