#!/usr/bin/env npx tsx /** * Run any Todoist tool directly without going through MCP. * * Usage: * npx tsx scripts/run-tool.ts '' * npx tsx scripts/run-tool.ts --file * npx tsx scripts/run-tool.ts --list * * Examples: * npx tsx scripts/run-tool.ts add-tasks '{"tasks":[{"content":"Test task","order":1}]}' * npx tsx scripts/run-tool.ts find-tasks '{"searchText":"meeting"}' * npx tsx scripts/run-tool.ts get-overview '{}' * * Requires TODOIST_API_KEY in .env file (and optionally TODOIST_BASE_URL). */ import { readFileSync } from 'node:fs' import { TodoistApi } from '@doist/todoist-sdk' import { config } from 'dotenv' import { registeredTools } from '../src/tool-registry.js' import { createTodoistClient, runWithUsageTrackingContext } from '../src/usage-tracking.js' config() // Define a minimal type for tool execution that works with any tool type ExecutableTool = { name: string description: string execute: ( // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- tools have varying parameter schemas args: any, client: TodoistApi, ) => Promise<{ textContent?: string; structuredContent?: unknown; contentItems?: unknown[] }> } const tools: Record = Object.fromEntries( registeredTools.map((tool) => [tool.name, tool as ExecutableTool]), ) function printUsage() { console.log(` Usage: npx tsx scripts/run-tool.ts '' npx tsx scripts/run-tool.ts --file npx tsx scripts/run-tool.ts --list Available tools: ${Object.keys(tools) .sort() .map((name) => ` - ${name}`) .join('\n')} `) } async function main() { const args = process.argv.slice(2) if (args.length === 0 || args[0] === '--help' || args[0] === '-h') { printUsage() process.exit(0) } if (args[0] === '--list') { console.log('Available tools:') for (const name of Object.keys(tools).sort()) { const tool = tools[name] console.log(`\n${name}:`) console.log(` ${tool.description}`) } process.exit(0) } const toolName = args[0] const tool = tools[toolName] if (!tool) { console.error(`Unknown tool: ${toolName}`) console.error(`Available tools: ${Object.keys(tools).sort().join(', ')}`) process.exit(1) } let jsonArgs: string if (args[1] === '--file') { if (!args[2]) { console.error('--file requires a path argument') process.exit(1) } jsonArgs = readFileSync(args[2], 'utf-8') } else { jsonArgs = args[1] || '{}' } let parsedArgs: unknown try { parsedArgs = JSON.parse(jsonArgs) } catch (e) { console.error('Invalid JSON args:', e) process.exit(1) } const apiKey = process.env.TODOIST_API_KEY if (!apiKey) { console.error('TODOIST_API_KEY not found in environment or .env file') process.exit(1) } const baseUrl = process.env.TODOIST_BASE_URL const client = createTodoistClient(apiKey, { baseUrl, // Local direct runs are a dev helper, not real MCP traffic. tracking: { enabled: false }, }) console.log(`Running ${toolName} with args:`) console.log(JSON.stringify(parsedArgs, null, 2)) console.log('---') try { const result = await runWithUsageTrackingContext(tool.name, () => tool.execute(parsedArgs, client), ) if (result.textContent) { console.log('\nText output:') console.log(result.textContent) } if (result.structuredContent) { console.log('\nStructured output:') console.log(JSON.stringify(result.structuredContent, null, 2)) } if (result.contentItems?.length) { console.log(`\nContent items: ${result.contentItems.length}`) for (const item of result.contentItems) { const entry = item as Record if (entry.type === 'image') { const data = entry.data as string console.log( ` [image] ${entry.mimeType} (${Math.round((data.length * 0.75) / 1024)}KB base64)`, ) } else if (entry.type === 'text') { const text = entry.text as string console.log(` [text] ${text.length > 200 ? `${text.slice(0, 200)}...` : text}`) } else if (entry.type === 'resource') { const resource = entry.resource as Record const blob = resource.blob as string console.log( ` [resource] ${resource.mimeType} (${Math.round((blob.length * 0.75) / 1024)}KB base64)`, ) } } } } catch (error) { console.error('Tool execution failed:', error) process.exit(1) } } main()