// Name: Google Tasks Manager // Description: A lightweight Google Tasks client with full CRUD operations // Author: malacon import '@johnlindquist/kit' import { google } from 'googleapis' import { OAuth2Client } from 'google-auth-library' /* * ── SETUP REQUIREMENTS ── * * 1. Enable Google Tasks API in Google Cloud Console: * https://console.cloud.google.com/apis/library/tasks.googleapis.com * * 2. Create OAuth 2.0 credentials (Desktop Application): * https://console.cloud.google.com/apis/credentials * Download the JSON file (client_secret_xxx.json) * * 3. First run will prompt for the path to your credentials file * * 4. Follow OAuth flow in browser to authorize the app */ // ── CONSTANTS ── const SCOPES = ['https://www.googleapis.com/auth/tasks'] const TOKEN_PATH = kitPath('google-tasks-token.json') const CREDENTIALS_PATH_KEY = 'GOOGLE_TASKS_CREDENTIALS_PATH' // ── AUTH HELPERS ── async function getAuthClient() { try { const credentialsPath = await env(CREDENTIALS_PATH_KEY, async () => { return await path({ hint: 'Select your Google OAuth credentials JSON file', startPath: home('Downloads'), }) }) const credentials = JSON.parse(await readFile(credentialsPath, 'utf8')) const { client_secret, client_id, redirect_uris } = credentials.installed const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uris[0]) // Try to load existing token if (await pathExists(TOKEN_PATH)) { const token = JSON.parse(await readFile(TOKEN_PATH, 'utf8')) oAuth2Client.setCredentials(token) // Refresh token if needed if (token.expiry_date && Date.now() >= token.expiry_date) { await oAuth2Client.refreshAccessToken() await writeFile(TOKEN_PATH, JSON.stringify(oAuth2Client.credentials)) } } else { // First time auth flow const authUrl = oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: SCOPES, }) await div(md(` # Google Tasks Authentication Please visit this URL to authorize the app: [${authUrl}](${authUrl}) Copy the authorization code and paste it below. `)) const code = await arg('Enter the authorization code:') const { tokens } = await oAuth2Client.getToken(code) oAuth2Client.setCredentials(tokens) await writeFile(TOKEN_PATH, JSON.stringify(tokens)) } return oAuth2Client } catch (error) { await div(md(`# Authentication Error\n\n${error.message}`)) throw error } } // ── GOOGLE TASKS API HELPERS ── async function getTasksService() { const auth = await getAuthClient() return google.tasks({ version: 'v1', auth }) } async function listTaskLists() { const service = await getTasksService() const response = await service.tasklists.list() return response.data.items || [] } async function listTasks(taskListId) { const service = await getTasksService() const response = await service.tasks.list({ tasklist: taskListId, showCompleted: true, showHidden: true, }) return response.data.items || [] } async function createTask(taskListId, taskData) { const service = await getTasksService() const response = await service.tasks.insert({ tasklist: taskListId, requestBody: taskData, }) return response.data } async function updateTask(taskListId, taskId, taskData) { const service = await getTasksService() const response = await service.tasks.update({ tasklist: taskListId, task: taskId, requestBody: taskData, }) return response.data } async function deleteTask(taskListId, taskId) { const service = await getTasksService() await service.tasks.delete({ tasklist: taskListId, task: taskId, }) } // ── UI HELPERS ── function formatTaskForDisplay(task) { const status = task.status === 'completed' ? '✅' : '⬜' const title = task.title || 'Untitled Task' const dueDate = task.due ? ` (Due: ${formatDate(new Date(task.due), 'MMM dd, yyyy')})` : '' return { name: `${status} ${title}${dueDate}`, value: task, description: task.notes || '', } } async function showTaskForm(existingTask = null) { const isEdit = !!existingTask const title = isEdit ? 'Edit Task' : 'Create New Task' const [taskTitle, notes, dueDate] = await fields([ { label: 'Task Title', value: existingTask?.title || '', placeholder: 'Enter task title...', }, { label: 'Notes (optional)', value: existingTask?.notes || '', placeholder: 'Add notes...', }, { label: 'Due Date (optional)', type: 'datetime-local', value: existingTask?.due ? new Date(existingTask.due).toISOString().slice(0, 16) : '', }, ]) if (!taskTitle.trim()) { await notify('Task title is required') return null } const taskData = { title: taskTitle.trim(), notes: notes.trim() || undefined, due: dueDate ? new Date(dueDate).toISOString() : undefined, } if (isEdit) { taskData.status = existingTask.status } return taskData } // ── MAIN MENU FLOW ── async function showMainMenu() { while (true) { const action = await arg('Google Tasks Manager', [ { name: '📋 Select Task List', value: 'select-list' }, { name: '➕ Create Task', value: 'create-task' }, { name: '🔄 Refresh Auth', value: 'refresh-auth' }, { name: '❌ Quit', value: 'quit' }, ]) switch (action) { case 'select-list': await showTaskListSelector() break case 'create-task': await showCreateTaskFlow() break case 'refresh-auth': await refreshAuth() break case 'quit': return } } } async function showTaskListSelector() { try { const taskLists = await listTaskLists() if (taskLists.length === 0) { await div(md('# No Task Lists Found\n\nPlease create a task list in Google Tasks first.')) return } const choices = taskLists.map(list => ({ name: list.title, value: list, description: `${list.title} - Task List`, })) const selectedList = await select('Select a Task List:', choices) await showTasksInList(selectedList) } catch (error) { await notify(`Error loading task lists: ${error.message}`) } } async function showTasksInList(taskList) { while (true) { try { const tasks = await listTasks(taskList.id) const choices = [ { name: '⬅️ Back to Main Menu', value: 'back' }, { name: '➕ Create New Task', value: 'create' }, ...tasks.map(formatTaskForDisplay), ] const selection = await select(`Tasks in "${taskList.title}":`, choices) if (selection === 'back') { return } else if (selection === 'create') { await createTaskInList(taskList.id) } else { await showTaskActions(taskList.id, selection) } } catch (error) { await notify(`Error loading tasks: ${error.message}`) return } } } async function showTaskActions(taskListId, task) { const action = await arg(`Task: ${task.title}`, [ { name: '✏️ Edit Task', value: 'edit' }, { name: task.status === 'completed' ? '⬜ Mark Pending' : '✅ Mark Complete', value: 'toggle' }, { name: '🗑️ Delete Task', value: 'delete' }, { name: '⬅️ Back to List', value: 'back' }, ]) switch (action) { case 'edit': await editTask(taskListId, task) break case 'toggle': await toggleTaskStatus(taskListId, task) break case 'delete': await deleteTaskWithConfirmation(taskListId, task) break case 'back': return } } async function createTaskInList(taskListId) { const taskData = await showTaskForm() if (taskData) { try { await createTask(taskListId, taskData) await notify('Task created successfully!') } catch (error) { await notify(`Error creating task: ${error.message}`) } } } async function editTask(taskListId, task) { const taskData = await showTaskForm(task) if (taskData) { try { await updateTask(taskListId, task.id, taskData) await notify('Task updated successfully!') } catch (error) { await notify(`Error updating task: ${error.message}`) } } } async function toggleTaskStatus(taskListId, task) { try { const newStatus = task.status === 'completed' ? 'needsAction' : 'completed' const updateData = { status: newStatus } if (newStatus === 'completed') { updateData.completed = new Date().toISOString() } await updateTask(taskListId, task.id, updateData) await notify(`Task marked as ${newStatus === 'completed' ? 'complete' : 'pending'}!`) } catch (error) { await notify(`Error updating task: ${error.message}`) } } async function deleteTaskWithConfirmation(taskListId, task) { const confirm = await arg(`Delete "${task.title}"?`, [ { name: '✅ Yes, Delete', value: true }, { name: '❌ Cancel', value: false }, ]) if (confirm) { try { await deleteTask(taskListId, task.id) await notify('Task deleted successfully!') } catch (error) { await notify(`Error deleting task: ${error.message}`) } } } async function showCreateTaskFlow() { try { const taskLists = await listTaskLists() if (taskLists.length === 0) { await div(md('# No Task Lists Found\n\nPlease create a task list in Google Tasks first.')) return } const selectedList = await select('Select Task List:', taskLists.map(list => ({ name: list.title, value: list, }))) await createTaskInList(selectedList.id) } catch (error) { await notify(`Error: ${error.message}`) } } async function refreshAuth() { try { if (await pathExists(TOKEN_PATH)) { await remove(TOKEN_PATH) } await getAuthClient() await notify('Authentication refreshed successfully!') } catch (error) { await notify(`Error refreshing auth: ${error.message}`) } } // ── MAIN EXECUTION ── try { await showMainMenu() } catch (error) { console.error('Fatal error:', error) await div(md(`# Error\n\n${error.message}\n\nCheck the console for more details.`)) }