import { google, type Auth } from 'googleapis'; import { readFileSync, existsSync } from 'fs'; import { resolve } from 'path'; import { homedir } from 'os'; import type { GtasksConfig } from '../../config/schema.js'; import type { Tool, ToolResult } from '../types.js'; /** Expand ~ to home directory. */ function expandPath(p: string): string { if (p.startsWith('~/') || p === '~') { return resolve(homedir(), p.slice(2)); } return resolve(p); } /** Create an OAuth2 client from Google Tasks config (credentials + token files). */ function createOAuth2Client(config: NonNullable): Auth.OAuth2Client { const credentialsPath = config.credentials_file; if (!credentialsPath) { throw new Error('No credentials_file configured. Set automation.gtasks.credentials_file in config.'); } const expandedCredsPath = expandPath(credentialsPath); if (!existsSync(expandedCredsPath)) { throw new Error(`Credentials file not found: ${expandedCredsPath}`); } const credentials = JSON.parse(readFileSync(expandedCredsPath, 'utf-8')); const { client_id, client_secret, redirect_uris } = credentials.installed ?? credentials.web ?? {}; if (!client_id || !client_secret) { throw new Error('Invalid credentials file — missing client_id or client_secret'); } const oauth2Client = new google.auth.OAuth2( client_id, client_secret, redirect_uris?.[0] ?? 'http://localhost', ); const tokenPath = expandPath(config.token_file ?? '~/.config/flynn/gtasks-token.json'); if (!existsSync(tokenPath)) { throw new Error(`Token file not found: ${tokenPath}. Run "flynn gtasks-auth" to authenticate.`); } const token = JSON.parse(readFileSync(tokenPath, 'utf-8')); oauth2Client.setCredentials(token); return oauth2Client; } interface TaskListSummary { id: string; title: string; updated: string; } interface TaskSummary { id: string; title: string; status: string; due: string; notes: string; updated: string; parent: string; } /** Format task lists for tool output. */ function formatTaskLists(lists: TaskListSummary[]): string { if (lists.length === 0) { return 'No task lists found.'; } return lists .map(l => `[${l.id}] ${l.title}\n Updated: ${l.updated}`) .join('\n\n'); } /** Format tasks for tool output. */ function formatTasks(tasks: TaskSummary[]): string { if (tasks.length === 0) { return 'No tasks found.'; } return tasks .map(t => { const checkbox = t.status === 'completed' ? '[x]' : '[ ]'; const parts = [`${checkbox} ${t.title}`]; if (t.due) parts.push(` Due: ${t.due}`); if (t.notes) parts.push(` Notes: ${t.notes}`); parts.push(` ID: ${t.id}`); return parts.join('\n'); }) .join('\n\n'); } /** * Creates Google Tasks read-only tools bound to the given GtasksConfig. * Tools create their own OAuth2 client per invocation. */ export function createGtasksTools(config: NonNullable): Tool[] { const tasksLists: Tool = { name: 'tasks.lists', description: 'List all Google Tasks task lists. Returns id, title, and last updated time for each list.', inputSchema: { type: 'object', properties: { maxResults: { type: 'number', description: 'Maximum number of task lists to return (default: 20)', }, }, }, execute: async (rawArgs: unknown): Promise => { const args = rawArgs as { maxResults?: number }; const maxResults = args.maxResults ?? 20; try { const auth = createOAuth2Client(config); const tasks = google.tasks({ version: 'v1', auth }); const response = await tasks.tasklists.list({ maxResults, }); const items = response.data.items ?? []; const lists: TaskListSummary[] = items.map(l => ({ id: l.id ?? '', title: l.title ?? '(untitled)', updated: l.updated ?? '', })); return { success: true, output: formatTaskLists(lists), }; } catch (error) { return { success: false, output: '', error: error instanceof Error ? error.message : String(error), }; } }, }; const tasksList: Tool = { name: 'tasks.list', description: 'List tasks from a Google Tasks task list. Returns title, status (completed/needsAction), due date, and notes. Use tasks.lists first to get task list IDs.', inputSchema: { type: 'object', properties: { taskListId: { type: 'string', description: 'Task list ID (from tasks.lists). Defaults to the primary "@default" list.', }, maxResults: { type: 'number', description: 'Maximum number of tasks to return (default: 100)', }, showCompleted: { type: 'boolean', description: 'Whether to include completed tasks (default: true)', }, showHidden: { type: 'boolean', description: 'Whether to include hidden/deleted tasks (default: false)', }, }, }, execute: async (rawArgs: unknown): Promise => { const args = rawArgs as { taskListId?: string; maxResults?: number; showCompleted?: boolean; showHidden?: boolean }; const taskListId = args.taskListId ?? '@default'; const maxResults = args.maxResults ?? 100; try { const auth = createOAuth2Client(config); const tasks = google.tasks({ version: 'v1', auth }); const response = await tasks.tasks.list({ tasklist: taskListId, maxResults, showCompleted: args.showCompleted ?? true, showHidden: args.showHidden ?? false, }); const items = response.data.items ?? []; const taskList: TaskSummary[] = items.map(t => ({ id: t.id ?? '', title: t.title ?? '(untitled)', status: t.status ?? 'needsAction', due: t.due ?? '', notes: t.notes ?? '', updated: t.updated ?? '', parent: t.parent ?? '', })); return { success: true, output: formatTasks(taskList), }; } catch (error) { return { success: false, output: '', error: error instanceof Error ? error.message : String(error), }; } }, }; return [tasksLists, tasksList]; }