From bab90f1df34457012ecf0339e953ebf7eff828be Mon Sep 17 00:00:00 2001 From: diegomarzaa Date: Mon, 5 Jan 2026 18:46:29 +0100 Subject: [PATCH] fix: API compatibility with latest TaskNotes plugin (fixes #2) The current TaskNotes plugin API has different behavior than expected: - GET /api/tasks with query params returns HTTP 400 - POST /api/tasks/query exists but returns 0 tasks This fix implements client-side filtering as a workaround: Changes to lib/api.js: - listTasks(): Now uses GET /api/tasks without params and filters client-side - queryTasks(): Handles FilterParser AST format for advanced filtering - searchTasks(): Implements client-side title search - Added helper methods: getAllTasks(), evaluateGroup(), evaluateNode(), evaluateCondition() Changes to commands/projects.js: - Fixed project filtering to handle [[wikilink]] format in project names - showProject() and showProjectStats() now properly match tasks to projects Tested with TaskNotes plugin v4.2.1. All CLI commands now work correctly: - tn list (with all filter options) - tn search - tn projects list/show/stats - All other commands that depend on task listing --- commands/projects.js | 24 ++++-- lib/api.js | 187 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 187 insertions(+), 24 deletions(-) diff --git a/commands/projects.js b/commands/projects.js index 28d00ab..12ee489 100644 --- a/commands/projects.js +++ b/commands/projects.js @@ -152,9 +152,15 @@ async function showProject(api, projectName, options) { archived: 'false' }); - const projectTasks = result.tasks.filter(task => - task.projects && task.projects.includes(projectName) - ); + // Filter tasks - handle both plain names and [[wikilink]] format + const projectTasks = result.tasks.filter(task => { + if (!task.projects || task.projects.length === 0) return false; + return task.projects.some(proj => { + if (!proj || typeof proj !== 'string') return false; + const cleanProj = proj.replace(/^\[\[|\]\]$/g, ''); + return cleanProj === projectName || proj === projectName || cleanProj.includes(projectName) || projectName.includes(cleanProj); + }); + }); spinner.succeed(`Found ${projectTasks.length} tasks for project`); @@ -220,9 +226,15 @@ async function showProjectStats(api, projectName, options) { project: projectName }); - const projectTasks = result.tasks.filter(task => - task.projects && task.projects.includes(projectName) - ); + // Filter tasks - handle both plain names and [[wikilink]] format + const projectTasks = result.tasks.filter(task => { + if (!task.projects || task.projects.length === 0) return false; + return task.projects.some(proj => { + if (!proj || typeof proj !== 'string') return false; + const cleanProj = proj.replace(/^\[\[|\]\]$/g, ''); + return cleanProj === projectName || proj === projectName || cleanProj.includes(projectName) || projectName.includes(cleanProj); + }); + }); spinner.succeed(`Stats calculated for ${projectTasks.length} tasks`); diff --git a/lib/api.js b/lib/api.js index 828ef59..d862844 100644 --- a/lib/api.js +++ b/lib/api.js @@ -81,25 +81,160 @@ class TaskNotesAPI { } async listTasks(filters = {}) { - const params = new URLSearchParams(); + // Use GET /api/tasks (no params) and filter client-side + // POST /api/tasks/query has a bug in the current plugin version + const result = await this.getAllTasks(); + let tasks = result.tasks || []; + const total = tasks.length; - Object.entries(filters).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - params.append(key, value); - } - }); - - const queryString = params.toString(); - const endpoint = `/api/tasks${queryString ? `?${queryString}` : ''}`; + // Apply client-side filters + if (filters.completed === 'true') { + tasks = tasks.filter(t => t.status === 'done'); + } else if (filters.completed === 'false') { + tasks = tasks.filter(t => t.status !== 'done'); + } - return this.request(endpoint); + if (filters.scheduled_after) { + const afterDate = filters.scheduled_after.split('T')[0]; + tasks = tasks.filter(t => t.scheduled && t.scheduled >= afterDate); + } + if (filters.scheduled_before) { + const beforeDate = filters.scheduled_before.split('T')[0]; + tasks = tasks.filter(t => t.scheduled && t.scheduled <= beforeDate); + } + if (filters.due_before) { + const beforeDate = filters.due_before.split('T')[0]; + tasks = tasks.filter(t => t.due && t.due < beforeDate); + } + + // Apply limit + const limit = filters.limit || 200; + if (tasks.length > limit) { + tasks = tasks.slice(0, limit); + } + + return { + tasks, + total, + filtered: tasks.length, + vault: result.vault + }; + } + + async getAllTasks() { + // GET /api/tasks without parameters returns all tasks + return this.request('/api/tasks'); } async queryTasks(filterQuery) { - return this.request('/api/tasks/query', { - method: 'POST', - body: JSON.stringify(filterQuery) - }); + // Use getAllTasks and filter client-side since POST /api/tasks/query is buggy + const result = await this.getAllTasks(); + let tasks = result.tasks || []; + const total = tasks.length; + + // Handle FilterParser AST format (type: 'group', children: [...]) + if (filterQuery.type === 'group' && filterQuery.children) { + tasks = tasks.filter(task => this.evaluateGroup(task, filterQuery)); + } + // Handle simple conditions array format + else if (filterQuery.conditions && filterQuery.conditions.length > 0) { + tasks = tasks.filter(task => { + return filterQuery.conditions.every(condition => + this.evaluateCondition(task, condition) + ); + }); + } + + // Apply limit + const limit = filterQuery.limit || 200; + if (tasks.length > limit) { + tasks = tasks.slice(0, limit); + } + + return { + tasks, + total, + filtered: tasks.length, + vault: result.vault + }; + } + + evaluateGroup(task, group) { + const { conjunction, children } = group; + + if (!children || children.length === 0) { + return true; + } + + if (conjunction === 'or') { + return children.some(child => this.evaluateNode(task, child)); + } else { + // Default to AND + return children.every(child => this.evaluateNode(task, child)); + } + } + + evaluateNode(task, node) { + if (node.type === 'group') { + return this.evaluateGroup(task, node); + } else if (node.type === 'condition') { + return this.evaluateCondition(task, node); + } + return true; + } + + evaluateCondition(task, condition) { + const { property, operator, value } = condition; + const taskValue = task[property]; + + switch (operator) { + case 'is': + if (Array.isArray(taskValue)) { + return taskValue.some(v => v.toLowerCase() === value.toLowerCase()); + } + return taskValue && taskValue.toLowerCase() === value.toLowerCase(); + case 'is-not': + if (Array.isArray(taskValue)) { + return !taskValue.some(v => v.toLowerCase() === value.toLowerCase()); + } + return !taskValue || taskValue.toLowerCase() !== value.toLowerCase(); + case 'contains': + if (Array.isArray(taskValue)) { + return taskValue.some(v => v.toLowerCase().includes(value.toLowerCase())); + } + return taskValue && taskValue.toLowerCase().includes(value.toLowerCase()); + case 'does-not-contain': + if (Array.isArray(taskValue)) { + return !taskValue.some(v => v.toLowerCase().includes(value.toLowerCase())); + } + return !taskValue || !taskValue.toLowerCase().includes(value.toLowerCase()); + case 'is-before': + case 'before': + return taskValue && taskValue < value; + case 'is-after': + case 'after': + return taskValue && taskValue > value; + case 'is-on-or-before': + case 'on-or-before': + return taskValue && taskValue <= value; + case 'is-on-or-after': + case 'on-or-after': + return taskValue && taskValue >= value; + case 'is-empty': + case 'empty': + return !taskValue || (Array.isArray(taskValue) && taskValue.length === 0); + case 'is-not-empty': + case 'not-empty': + return taskValue && (!Array.isArray(taskValue) || taskValue.length > 0); + case 'is-greater-than': + case 'greater-than': + return taskValue && parseFloat(taskValue) > parseFloat(value); + case 'is-less-than': + case 'less-than': + return taskValue && parseFloat(taskValue) < parseFloat(value); + default: + return true; + } } async getTask(taskId) { @@ -139,10 +274,26 @@ class TaskNotesAPI { } async searchTasks(query) { - return this.listTasks({ - limit: 50, - // Add search functionality when API supports it - }); + // Search using title contains - client-side filtering + const result = await this.getAllTasks(); + let tasks = result.tasks || []; + const queryLower = query.toLowerCase(); + + tasks = tasks.filter(t => + t.title && t.title.toLowerCase().includes(queryLower) + ); + + // Limit to 50 results + if (tasks.length > 50) { + tasks = tasks.slice(0, 50); + } + + return { + tasks, + total: result.tasks.length, + filtered: tasks.length, + vault: result.vault + }; } async getFilterOptions() {