Browse Source

feat: v2.2.0 — action-based tool surface + full v3 API coverage

Consolidates ~30 one-action-per-tool registrations into 7 resource tools,
each discriminated by an `action` enum. Matches the ShadowMan C++ builtin
tool convention (manage_mcp, kb, skill, office, file, documents etc.) —
and uses JSON Schema `oneOf` arms with `{const: "..."}` on the action
field so the LLM sampler only sees the parameters relevant to the chosen
action (no cross-action field leakage).

Tools:
- zoho_auth — setup, status, resolve_portal
- zoho_project — list, get, users, statuses
- zoho_task — list, get, create, update, delete, subtasks, my, search
- zoho_comment — list, add, update, delete
- zoho_milestone — list, create, update, delete
- zoho_tasklist — list, get, create, update, delete
- zoho_timesheet — list, create, update, delete

New coverage vs v2.0.0:
- milestones CRUD
- tasklists CRUD + get
- list project users (resolve zpuid before assignments)
- list project task statuses (needed for v3 status = {id: …})
- subtasks listing + parent_task_id on create
- list comments
- update timesheet entry
- get project

v3 body shape additions on create/update task: status_id → status.{id},
parent_task_id → parental_info.parent_task_id, tasklist_id (update) →
tasklist.{id}, billing_type.
Fszontagh 20 hours ago
parent
commit
8685be20a3
3 changed files with 838 additions and 349 deletions
  1. 219 128
      index.js
  2. 209 28
      lib/tasks.js
  3. 410 193
      manifest.json

+ 219 - 128
index.js

@@ -1,135 +1,226 @@
-// Zoho Tasks Plugin v2.0.0
+// Zoho Tasks Plugin v2.2.0
 // EU data center: accounts.zoho.eu
+// Action-based tool surface, mirroring the ShadowMan C++ builtin style
+// (see manage_mcp, kb, skill, office etc. — one tool per resource, with
+// an `action` enum that picks a sub-operation).
+
 var auth = require('./lib/auth');
 var tasks = require('./lib/tasks');
 
-shadowman.log.info('Zoho Tasks plugin v2.0.0 loaded');
-
-// Auth tools
-shadowman.tools.register('setup_auth', function(args, context) {
-    shadowman.storage.set('auth_conversation_id', context.conversationId);
-    return auth.setupAuth(args.grant_token);
-});
-
-shadowman.tools.register('auth_status', function(args, context) {
-    return auth.getStatus();
-});
-
-shadowman.tools.register('resolve_portal', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    return auth.resolvePortal(token, args);
-});
-
-// Project tools
-shadowman.tools.register('list_projects', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    return tasks.listProjects(token, args.index || 1);
-});
-
-// Task tools
-shadowman.tools.register('list_tasks', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    return tasks.listTasks(token, args.project_id, args);
-});
-
-shadowman.tools.register('get_task', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.task_id) return { error: 'Missing task_id parameter' };
-    return tasks.getTask(token, args.project_id, args.task_id);
-});
-
-shadowman.tools.register('create_task', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.name) return { error: 'Missing name parameter' };
-    return tasks.createTask(token, args.project_id, args);
-});
-
-shadowman.tools.register('update_task', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.task_id) return { error: 'Missing task_id parameter' };
-    return tasks.updateTask(token, args.project_id, args.task_id, args);
-});
-
-shadowman.tools.register('delete_task', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.task_id) return { error: 'Missing task_id parameter' };
-    return tasks.deleteTask(token, args.project_id, args.task_id);
-});
-
-shadowman.tools.register('my_tasks', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    return tasks.getMyTasks(token, args);
-});
-
-shadowman.tools.register('search_tasks', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.query) return { error: 'Missing query parameter' };
-    return tasks.searchTasks(token, args);
-});
-
-// Comment tools
-shadowman.tools.register('add_comment', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.task_id) return { error: 'Missing task_id parameter' };
-    if (!args.content) return { error: 'Missing content parameter' };
-    return tasks.addComment(token, args.project_id, args.task_id, args.content);
-});
-
-shadowman.tools.register('update_comment', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.task_id) return { error: 'Missing task_id parameter' };
-    if (!args.comment_id) return { error: 'Missing comment_id parameter' };
-    if (!args.content) return { error: 'Missing content parameter' };
-    return tasks.updateComment(token, args.project_id, args.task_id, args.comment_id, args.content);
-});
-
-shadowman.tools.register('delete_comment', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.task_id) return { error: 'Missing task_id parameter' };
-    if (!args.comment_id) return { error: 'Missing comment_id parameter' };
-    return tasks.deleteComment(token, args.project_id, args.task_id, args.comment_id);
-});
-
-shadowman.tools.register('log_time', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.task_id) return { error: 'Missing task_id parameter' };
-    if (args.hours === undefined) return { error: 'Missing hours parameter' };
-    return tasks.logTime(token, args.project_id, args);
-});
-
-shadowman.tools.register('list_timesheets', function(args, context) {
-    var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    return tasks.listTimesheets(token, args.project_id, args);
-});
+shadowman.log.info('Zoho Tasks plugin v2.2.0 loaded');
 
-shadowman.tools.register('delete_timesheet', function(args, context) {
+function requireAuth() {
     var token = auth.getValidToken();
-    if (!token) return { error: 'Not authenticated. Run setup_auth with a grant token first.' };
-    if (!args.project_id) return { error: 'Missing project_id parameter' };
-    if (!args.log_id) return { error: 'Missing log_id parameter' };
-    return tasks.deleteTimesheet(token, args.project_id, args.log_id);
+    if (!token) return { token: null, error: 'Not authenticated. Run zoho_auth action=setup with a grant token first.' };
+    return { token: token };
+}
+
+function missing(field) { return { error: 'Missing ' + field + ' parameter' }; }
+
+// ────────────────────────────────────────────────────────────────────────
+// zoho_auth — actions: setup, status, resolve_portal
+// ────────────────────────────────────────────────────────────────────────
+shadowman.tools.register('zoho_auth', function(args, context) {
+    var action = args.action;
+    if (!action) return missing('action');
+
+    if (action === 'setup') {
+        if (!args.grant_token) return missing('grant_token');
+        shadowman.storage.set('auth_conversation_id', context.conversationId);
+        return auth.setupAuth(args.grant_token);
+    }
+    if (action === 'status') {
+        return auth.getStatus();
+    }
+    if (action === 'resolve_portal') {
+        var a = requireAuth(); if (a.error) return { error: a.error };
+        return auth.resolvePortal(a.token, args);
+    }
+    return { error: 'Unknown action: ' + action };
+});
+
+// ────────────────────────────────────────────────────────────────────────
+// zoho_project — actions: list, get, users, statuses
+// ────────────────────────────────────────────────────────────────────────
+shadowman.tools.register('zoho_project', function(args) {
+    var a = requireAuth(); if (a.error) return { error: a.error };
+    var action = args.action;
+    if (!action) return missing('action');
+
+    if (action === 'list') {
+        return tasks.listProjects(a.token, args.index || 1);
+    }
+    if (action === 'get') {
+        if (!args.project_id) return missing('project_id');
+        return tasks.getProject(a.token, args.project_id);
+    }
+    if (action === 'users') {
+        return tasks.listUsers(a.token, args);
+    }
+    if (action === 'statuses') {
+        if (!args.project_id) return missing('project_id');
+        return tasks.listTaskStatuses(a.token, args.project_id);
+    }
+    return { error: 'Unknown action: ' + action };
+});
+
+// ────────────────────────────────────────────────────────────────────────
+// zoho_task — actions: list, get, create, update, delete, subtasks, my, search
+// ────────────────────────────────────────────────────────────────────────
+shadowman.tools.register('zoho_task', function(args) {
+    var a = requireAuth(); if (a.error) return { error: a.error };
+    var action = args.action;
+    if (!action) return missing('action');
+
+    if (action === 'my') {
+        return tasks.getMyTasks(a.token, args);
+    }
+    if (action === 'search') {
+        if (!args.query) return missing('query');
+        return tasks.searchTasks(a.token, args);
+    }
+
+    if (!args.project_id) return missing('project_id');
+
+    if (action === 'list') {
+        return tasks.listTasks(a.token, args.project_id, args);
+    }
+    if (action === 'get') {
+        if (!args.task_id) return missing('task_id');
+        return tasks.getTask(a.token, args.project_id, args.task_id);
+    }
+    if (action === 'create') {
+        if (!args.name) return missing('name');
+        return tasks.createTask(a.token, args.project_id, args);
+    }
+    if (action === 'update') {
+        if (!args.task_id) return missing('task_id');
+        return tasks.updateTask(a.token, args.project_id, args.task_id, args);
+    }
+    if (action === 'delete') {
+        if (!args.task_id) return missing('task_id');
+        return tasks.deleteTask(a.token, args.project_id, args.task_id);
+    }
+    if (action === 'subtasks') {
+        if (!args.task_id) return missing('task_id');
+        return tasks.listSubtasks(a.token, args.project_id, args.task_id);
+    }
+    return { error: 'Unknown action: ' + action };
+});
+
+// ────────────────────────────────────────────────────────────────────────
+// zoho_comment — actions: list, add, update, delete
+// ────────────────────────────────────────────────────────────────────────
+shadowman.tools.register('zoho_comment', function(args) {
+    var a = requireAuth(); if (a.error) return { error: a.error };
+    var action = args.action;
+    if (!action) return missing('action');
+    if (!args.project_id) return missing('project_id');
+    if (!args.task_id) return missing('task_id');
+
+    if (action === 'list') {
+        return tasks.listComments(a.token, args.project_id, args.task_id, args.index);
+    }
+    if (action === 'add') {
+        if (!args.content) return missing('content');
+        return tasks.addComment(a.token, args.project_id, args.task_id, args.content);
+    }
+    if (action === 'update') {
+        if (!args.comment_id) return missing('comment_id');
+        if (!args.content) return missing('content');
+        return tasks.updateComment(a.token, args.project_id, args.task_id, args.comment_id, args.content);
+    }
+    if (action === 'delete') {
+        if (!args.comment_id) return missing('comment_id');
+        return tasks.deleteComment(a.token, args.project_id, args.task_id, args.comment_id);
+    }
+    return { error: 'Unknown action: ' + action };
+});
+
+// ────────────────────────────────────────────────────────────────────────
+// zoho_milestone — actions: list, create, update, delete
+// ────────────────────────────────────────────────────────────────────────
+shadowman.tools.register('zoho_milestone', function(args) {
+    var a = requireAuth(); if (a.error) return { error: a.error };
+    var action = args.action;
+    if (!action) return missing('action');
+    if (!args.project_id) return missing('project_id');
+
+    if (action === 'list') {
+        return tasks.listMilestones(a.token, args.project_id, args);
+    }
+    if (action === 'create') {
+        if (!args.name) return missing('name');
+        return tasks.createMilestone(a.token, args.project_id, args);
+    }
+    if (action === 'update') {
+        if (!args.milestone_id) return missing('milestone_id');
+        return tasks.updateMilestone(a.token, args.project_id, args.milestone_id, args);
+    }
+    if (action === 'delete') {
+        if (!args.milestone_id) return missing('milestone_id');
+        return tasks.deleteMilestone(a.token, args.project_id, args.milestone_id);
+    }
+    return { error: 'Unknown action: ' + action };
+});
+
+// ────────────────────────────────────────────────────────────────────────
+// zoho_tasklist — actions: list, get, create, update, delete
+// ────────────────────────────────────────────────────────────────────────
+shadowman.tools.register('zoho_tasklist', function(args) {
+    var a = requireAuth(); if (a.error) return { error: a.error };
+    var action = args.action;
+    if (!action) return missing('action');
+
+    if (action === 'list') {
+        return tasks.listTasklists(a.token, args.project_id, args);
+    }
+
+    if (!args.project_id) return missing('project_id');
+
+    if (action === 'get') {
+        if (!args.tasklist_id) return missing('tasklist_id');
+        return tasks.getTasklist(a.token, args.project_id, args.tasklist_id);
+    }
+    if (action === 'create') {
+        if (!args.name) return missing('name');
+        return tasks.createTasklist(a.token, args.project_id, args);
+    }
+    if (action === 'update') {
+        if (!args.tasklist_id) return missing('tasklist_id');
+        return tasks.updateTasklist(a.token, args.project_id, args.tasklist_id, args);
+    }
+    if (action === 'delete') {
+        if (!args.tasklist_id) return missing('tasklist_id');
+        return tasks.deleteTasklist(a.token, args.project_id, args.tasklist_id);
+    }
+    return { error: 'Unknown action: ' + action };
+});
+
+// ────────────────────────────────────────────────────────────────────────
+// zoho_timesheet — actions: list, create, update, delete
+// ────────────────────────────────────────────────────────────────────────
+shadowman.tools.register('zoho_timesheet', function(args) {
+    var a = requireAuth(); if (a.error) return { error: a.error };
+    var action = args.action;
+    if (!action) return missing('action');
+    if (!args.project_id) return missing('project_id');
+
+    if (action === 'list') {
+        return tasks.listTimesheets(a.token, args.project_id, args);
+    }
+    if (action === 'create') {
+        if (!args.task_id) return missing('task_id');
+        if (args.hours === undefined) return missing('hours');
+        return tasks.logTime(a.token, args.project_id, args);
+    }
+    if (action === 'update') {
+        if (!args.log_id) return missing('log_id');
+        return tasks.updateTimesheet(a.token, args.project_id, args.log_id, args);
+    }
+    if (action === 'delete') {
+        if (!args.log_id) return missing('log_id');
+        return tasks.deleteTimesheet(a.token, args.project_id, args.log_id);
+    }
+    return { error: 'Unknown action: ' + action };
 });

+ 209 - 28
lib/tasks.js

@@ -89,23 +89,25 @@ function apiCall(token, method, path, jsonBody, queryParams) {
     return result;
 }
 
-// --- Projects (v1 API — v3 returns 400) ---
+// --- Projects ---
 
 function listProjects(token, index) {
     var portalId = shadowman.config.value('portal_id');
     if (!portalId) {
         return { error: 'Missing portal_id in plugin config. Run resolve_portal first.' };
     }
-
     var params = {};
     if (index) params.index = String(index);
+    return apiCall(token, 'GET', '/api/v3/portal/{PORTALID}/projects', null, params);
+}
 
+function getProject(token, projectId) {
     return apiCall(token, 'GET',
-        '/api/v3/portal/{PORTALID}/projects'
-        .replace('{PORTALID}', portalId), null, params);
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}'
+        .replace('{PROJECTID}', projectId), null, {});
 }
 
-// --- Tasks (v3) ---
+// --- Tasks ---
 
 function listTasks(token, projectId, filters) {
     var params = {};
@@ -137,7 +139,11 @@ function createTask(token, projectId, args) {
     if (args.end_date) body.end_date = args.end_date;
     if (args.tasklist_id) body.tasklist_id = String(args.tasklist_id);
     if (args.milestone_id) body.milestone_id = String(args.milestone_id);
-    if (args.percent_complete) body.completion_percentage = String(args.percent_complete);
+    if (args.percent_complete !== undefined) body.completion_percentage = String(args.percent_complete);
+    // v3 extras
+    if (args.status_id) body.status = { id: String(args.status_id) };
+    if (args.parent_task_id) body.parental_info = { parent_task_id: String(args.parent_task_id) };
+    if (args.billing_type) body.billing_type = args.billing_type;
 
     return apiCall(token, 'POST',
         '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
@@ -153,6 +159,10 @@ function updateTask(token, projectId, taskId, args) {
     if (args.start_date) body.start_date = args.start_date;
     if (args.end_date) body.end_date = args.end_date;
     if (args.percent_complete !== undefined) body.completion_percentage = String(args.percent_complete);
+    if (args.status_id) body.status = { id: String(args.status_id) };
+    if (args.tasklist_id) body.tasklist = { id: String(args.tasklist_id) };
+    if (args.milestone_id) body.milestone_id = String(args.milestone_id);
+    if (args.billing_type) body.billing_type = args.billing_type;
 
     return apiCall(token, 'PATCH',
         '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
@@ -167,6 +177,13 @@ function deleteTask(token, projectId, taskId) {
         .replace('{TASKID}', taskId), null, {});
 }
 
+function listSubtasks(token, projectId, taskId) {
+    return apiCall(token, 'GET',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/subtasks'
+        .replace('{PROJECTID}', projectId)
+        .replace('{TASKID}', taskId), null, {});
+}
+
 // --- My Tasks ---
 
 function getMyTasks(token, filters) {
@@ -174,7 +191,6 @@ function getMyTasks(token, filters) {
     if (filters.status) params.status = filters.status;
     if (filters.priority) params.priority = filters.priority;
     if (filters.index) params.index = String(filters.index);
-
     return apiCall(token, 'GET', '/api/v3/portal/{PORTALID}/mytasks', null, params);
 }
 
@@ -230,6 +246,15 @@ function filterTasks(tasks, query) {
 
 // --- Comments ---
 
+function listComments(token, projectId, taskId, index) {
+    var params = {};
+    if (index) params.index = String(index);
+    return apiCall(token, 'GET',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments'
+        .replace('{PROJECTID}', projectId)
+        .replace('{TASKID}', taskId), null, params);
+}
+
 function addComment(token, projectId, taskId, content) {
     return apiCall(token, 'POST',
         '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments'
@@ -253,11 +278,136 @@ function deleteComment(token, projectId, taskId, commentId) {
         .replace('{COMMENTID}', commentId), null, {});
 }
 
+// --- Milestones ---
+
+function listMilestones(token, projectId, filters) {
+    var params = {};
+    if (filters && filters.status) params.status = filters.status;
+    if (filters && filters.index) params.index = String(filters.index);
+    return apiCall(token, 'GET',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones'
+        .replace('{PROJECTID}', projectId), null, params);
+}
+
+function createMilestone(token, projectId, args) {
+    var body = { name: args.name };
+    if (args.start_date) body.start_date = args.start_date;
+    if (args.end_date) body.end_date = args.end_date;
+    if (args.owner) body.owner = String(args.owner);
+    if (args.flag) body.flag = args.flag;                // "internal" | "external"
+    if (args.milestone_type) body.milestone_type = args.milestone_type;
+    return apiCall(token, 'POST',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones'
+        .replace('{PROJECTID}', projectId), body);
+}
+
+function updateMilestone(token, projectId, milestoneId, args) {
+    var body = {};
+    if (args.name) body.name = args.name;
+    if (args.start_date) body.start_date = args.start_date;
+    if (args.end_date) body.end_date = args.end_date;
+    if (args.owner) body.owner = String(args.owner);
+    if (args.flag) body.flag = args.flag;
+    if (args.status) body.status = args.status;
+    return apiCall(token, 'PATCH',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones/{MILESTONEID}'
+        .replace('{PROJECTID}', projectId)
+        .replace('{MILESTONEID}', milestoneId), body);
+}
+
+function deleteMilestone(token, projectId, milestoneId) {
+    return apiCall(token, 'DELETE',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones/{MILESTONEID}'
+        .replace('{PROJECTID}', projectId)
+        .replace('{MILESTONEID}', milestoneId), null, {});
+}
+
+// --- Tasklists ---
+
+function listTasklists(token, projectId, filters) {
+    var params = {};
+    if (filters && filters.flag) params.flag = filters.flag;
+    if (filters && filters.index) params.index = String(filters.index);
+
+    if (projectId) {
+        return apiCall(token, 'GET',
+            '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists'
+            .replace('{PROJECTID}', projectId), null, params);
+    }
+    // Portal-wide
+    return apiCall(token, 'GET',
+        '/api/v3/portal/{PORTALID}/all-tasklists', null, params);
+}
+
+function getTasklist(token, projectId, tasklistId) {
+    return apiCall(token, 'GET',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists/{TASKLISTID}'
+        .replace('{PROJECTID}', projectId)
+        .replace('{TASKLISTID}', tasklistId), null, {});
+}
+
+function createTasklist(token, projectId, args) {
+    var body = { name: args.name };
+    if (args.milestone_id) body.milestone = { id: String(args.milestone_id) };
+    if (args.flag) body.flag = args.flag;                // "internal" | "external"
+    if (args.status) body.status = args.status;          // "active" | "archived"
+    return apiCall(token, 'POST',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists'
+        .replace('{PROJECTID}', projectId), body);
+}
+
+function updateTasklist(token, projectId, tasklistId, args) {
+    var body = {};
+    if (args.name) body.name = args.name;
+    if (args.milestone_id) body.milestone = { id: String(args.milestone_id) };
+    if (args.flag) body.flag = args.flag;
+    if (args.status) body.status = args.status;
+    return apiCall(token, 'PATCH',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists/{TASKLISTID}'
+        .replace('{PROJECTID}', projectId)
+        .replace('{TASKLISTID}', tasklistId), body);
+}
+
+function deleteTasklist(token, projectId, tasklistId) {
+    return apiCall(token, 'DELETE',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists/{TASKLISTID}'
+        .replace('{PROJECTID}', projectId)
+        .replace('{TASKLISTID}', tasklistId), null, {});
+}
+
+// --- Users ---
+// Returns zpuid + name + email for every portal user. Needed before
+// assigning a task so you can resolve a name/email to a zpuid.
+
+function listUsers(token, filters) {
+    var params = {};
+    if (filters && filters.index) params.index = String(filters.index);
+    if (filters && filters.user_type) params.user_type = filters.user_type;
+    return apiCall(token, 'GET',
+        '/api/v3/portal/{PORTALID}/users', null, params);
+}
+
+// --- Task Statuses ---
+// v3 task body expects status = { id: <statusId> }. IDs are project-local,
+// so call this first if you don't already know the target status's id.
+
+function listTaskStatuses(token, projectId) {
+    return apiCall(token, 'GET',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/taskstatuses'
+        .replace('{PROJECTID}', projectId), null, {});
+}
+
 // --- Timesheet ---
 
 function logTime(token, projectId, args) {
-    var logDate = args.date || '2026-04-15';
-    if (logDate.indexOf('-') === 2) {
+    var logDate = args.date;
+    if (!logDate) {
+        var now = new Date();
+        logDate = now.getFullYear() + '-' +
+                  String(now.getMonth() + 1).padStart(2, '0') + '-' +
+                  String(now.getDate()).padStart(2, '0');
+    } else if (logDate.length === 10 && logDate.charAt(2) === '-') {
+        // MM-DD-YYYY → YYYY-MM-DD
         var parts = logDate.split('-');
         logDate = parts[2] + '-' + parts[0] + '-' + parts[1];
     }
@@ -268,7 +418,7 @@ function logTime(token, projectId, args) {
     var body = {
         log_name: args.notes || 'Time logged',
         date: logDate,
-        bill_status: 'Billable',
+        bill_status: args.bill_status || 'Billable',
         hours: String(totalHours),
         notes: args.notes || '',
         module: { id: String(args.task_id), type: 'task' },
@@ -281,22 +431,34 @@ function logTime(token, projectId, args) {
         .replace('{PROJECTID}', projectId), body);
 }
 
-function getProject(token, projectId) {
-    var portalId = shadowman.config.value('portal_id');
-    if (!portalId) return { error: 'Missing portal_id' };
-    return apiCall(token, 'GET',
-        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}'
-        .replace('{PORTALID}', portalId)
-        .replace('{PROJECTID}', projectId), null, {});
+function updateTimesheet(token, projectId, logId, args) {
+    var body = { module: 'task', from_page: 'timesheetdetails' };
+    if (args.hours !== undefined) {
+        var h = Number(args.hours) + (Number(args.minutes || 0) / 60);
+        body.hours = String(Math.round(h * 100) / 100);
+    }
+    if (args.notes !== undefined) body.notes = args.notes;
+    if (args.log_name !== undefined) body.log_name = args.log_name;
+    if (args.bill_status) body.bill_status = args.bill_status;
+    if (args.date) {
+        var d = args.date;
+        if (d.length === 10 && d.charAt(2) === '-') {
+            var parts = d.split('-');
+            d = parts[2] + '-' + parts[0] + '-' + parts[1];
+        }
+        body.date = d;
+    }
+    return apiCall(token, 'PATCH',
+        '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/logs/{LOGID}'
+        .replace('{PROJECTID}', projectId)
+        .replace('{LOGID}', String(logId)), body);
 }
 
 function listTimesheets(token, projectId, filters) {
     var portalId = shadowman.config.value('portal_id');
     if (!portalId) return { error: 'Missing portal_id in plugin config.' };
 
-    var params = {
-        view_type: 'customdate'
-    };
+    var params = { view_type: 'customdate' };
 
     if (filters.from_date) {
         var parts = filters.from_date.split('-');
@@ -307,16 +469,16 @@ function listTimesheets(token, projectId, filters) {
         d.setDate(d.getDate() - 30);
         params.start_date = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
     }
-    
+
     if (filters.to_date) {
-        var parts = filters.to_date.split('-');
-        if (parts.length === 3) params.end_date = parts[2] + '-' + parts[0] + '-' + parts[1];
+        var parts2 = filters.to_date.split('-');
+        if (parts2.length === 3) params.end_date = parts2[2] + '-' + parts2[0] + '-' + parts2[1];
         else params.end_date = filters.to_date;
     } else {
         var today = new Date();
         params.end_date = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
     }
-    
+
     if (filters.index) params.page = String(filters.index);
 
     if (filters.task_id) {
@@ -325,11 +487,9 @@ function listTimesheets(token, projectId, filters) {
         params.module = JSON.stringify({ type: 'task' });
     }
 
-    var url = '/api/v3/portal/{PORTALID}/timelogs'.replace('{PORTALID}', portalId);
+    var url = '/api/v3/portal/{PORTALID}/timelogs';
     if (projectId) {
-        // Ha van projekt ID, a projekt szintű végpontot hívjuk
         url = '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/timelogs'
-            .replace('{PORTALID}', portalId)
             .replace('{PROJECTID}', projectId);
     }
 
@@ -339,24 +499,45 @@ function listTimesheets(token, projectId, filters) {
 function deleteTimesheet(token, projectId, logId) {
     return apiCall(token, 'DELETE',
         '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/logs/{LOGID}'
-        .replace('{PORTALID}', shadowman.config.value('portal_id'))
         .replace('{PROJECTID}', projectId)
         .replace('{LOGID}', String(logId)), { module: 'task', from_page: 'timesheetdetails' }, {});
 }
 
 module.exports = {
+    // Projects
     listProjects: listProjects,
+    getProject: getProject,
+    // Tasks
     listTasks: listTasks,
     getTask: getTask,
     createTask: createTask,
     updateTask: updateTask,
     deleteTask: deleteTask,
+    listSubtasks: listSubtasks,
     getMyTasks: getMyTasks,
     searchTasks: searchTasks,
+    // Comments
+    listComments: listComments,
     addComment: addComment,
     updateComment: updateComment,
     deleteComment: deleteComment,
+    // Milestones
+    listMilestones: listMilestones,
+    createMilestone: createMilestone,
+    updateMilestone: updateMilestone,
+    deleteMilestone: deleteMilestone,
+    // Tasklists
+    listTasklists: listTasklists,
+    getTasklist: getTasklist,
+    createTasklist: createTasklist,
+    updateTasklist: updateTasklist,
+    deleteTasklist: deleteTasklist,
+    // Users + Statuses
+    listUsers: listUsers,
+    listTaskStatuses: listTaskStatuses,
+    // Timesheet
     logTime: logTime,
+    updateTimesheet: updateTimesheet,
     listTimesheets: listTimesheets,
     deleteTimesheet: deleteTimesheet
 };

+ 410 - 193
manifest.json

@@ -2,7 +2,7 @@
   "id": "zoho-tasks",
   "name": "Zoho Tasks",
   "description": "Zoho Projects task management integration for EU data center",
-  "version": "2.0.0",
+  "version": "2.2.0",
   "author": "Ferenc Szontagh & Zoë",
   "sdkVersion": 1,
   "entry": "index.js",
@@ -18,223 +18,440 @@
   ],
   "tools": [
     {
-      "name": "setup_auth",
-      "description": "Exchange a Zoho grant token for access + refresh tokens. Run this ONCE after getting a grant token from the Zoho Developer Console.",
+      "name": "zoho_auth",
+      "description": "Authenticate with Zoho Projects (EU). One-time setup exchanges a grant token for refresh+access tokens; status checks current token; resolve_portal picks the portal the plugin operates against.",
       "parameters": {
-        "type": "object",
-        "properties": {
-          "grant_token": { "type": "string", "description": "Grant token from Zoho Developer Console (Self Client → Generate)" }
-        },
-        "required": ["grant_token"]
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "setup", "description": "Exchange a Zoho grant_token for refresh + access tokens. Run ONCE." },
+              "grant_token": { "type": "string", "description": "Grant token from Zoho Developer Console (Self Client → Generate)." }
+            },
+            "required": ["action", "grant_token"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "status", "description": "Check authentication status and token validity." }
+            },
+            "required": ["action"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "resolve_portal", "description": "Resolve a portal URL or display name to its numeric portal_id and persist it in plugin config." },
+              "portal_url": { "type": "string", "description": "Full portal URL or portal name slug." },
+              "portal_name": { "type": "string", "description": "Portal display name to match." }
+            },
+            "required": ["action"],
+            "additionalProperties": false
+          }
+        ]
       }
     },
     {
-      "name": "resolve_portal",
-      "description": "Resolve a portal URL or name to its numeric portal ID. Call this after setup_auth to auto-configure the portal. Optionally pass portal_url or portal_name to match a specific portal.",
+      "name": "zoho_project",
+      "description": "Read-only project queries + reference data needed for task authoring (user zpuids, status ids).",
       "parameters": {
-        "type": "object",
-        "properties": {
-          "portal_url": { "type": "string", "description": "Full portal URL (e.g. https://projects.zoho.eu/portal/aicaller) or portal name slug" },
-          "portal_name": { "type": "string", "description": "Portal display name to match" }
-        }
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "list", "description": "List all accessible projects in the portal." },
+              "index": { "type": "number", "description": "Page index (default 1)." }
+            },
+            "required": ["action"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "get", "description": "Get details for a single project." },
+              "project_id": { "type": "string" }
+            },
+            "required": ["action", "project_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "users", "description": "List portal users. Returns zpuid, name, email — resolve these before assigning a task owner." },
+              "user_type": { "type": "string", "description": "active or inactive (optional)." },
+              "index": { "type": "number", "description": "Page index (default 1)." }
+            },
+            "required": ["action"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "statuses", "description": "List project-local task status IDs. Required input for zoho_task status_id." },
+              "project_id": { "type": "string" }
+            },
+            "required": ["action", "project_id"],
+            "additionalProperties": false
+          }
+        ]
       }
     },
     {
-      "name": "auth_status",
-      "description": "Check authentication status and token validity",
-      "parameters": { "type": "object", "properties": {} }
-    },
-    {
-      "name": "list_projects",
-      "description": "List all accessible Zoho Projects in the portal",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "index": { "type": "number", "description": "Page index (default 1)" }
-        }
-      }
-    },
-    {
-      "name": "list_tasks",
-      "description": "List tasks from a Zoho Projects project. Supports filtering by status, priority, assignee, and date range.",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "status": { "type": "string", "description": "Filter: all, completed, notcompleted (default: all)" },
-          "priority": { "type": "string", "description": "Filter: all, none, low, medium, high" },
-          "owner": { "type": "string", "description": "Filter: all or user ID" },
-          "index": { "type": "number", "description": "Page index (default 1)" }
-        },
-        "required": ["project_id"]
-      }
-    },
-    {
-      "name": "get_task",
-      "description": "Get full details of a single task",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Task ID" }
-        },
-        "required": ["project_id", "task_id"]
-      }
-    },
-    {
-      "name": "create_task",
-      "description": "Create a new task in a Zoho Projects project",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "name": { "type": "string", "description": "Task name/title" },
-          "description": { "type": "string", "description": "Task description (HTML allowed)" },
-          "person_responsible": { "type": "string", "description": "Owner user ID(s), comma-separated" },
-          "priority": { "type": "string", "description": "none, low, medium, high" },
-          "start_date": { "type": "string", "description": "Start date (MM-DD-YYYY format)" },
-          "end_date": { "type": "string", "description": "Due date (MM-DD-YYYY format)" },
-          "tasklist_id": { "type": "string", "description": "Task list ID to add the task to" }
-        },
-        "required": ["project_id", "name"]
-      }
-    },
-    {
-      "name": "update_task",
-      "description": "Update an existing task in Zoho Projects",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Task ID to update" },
-          "name": { "type": "string", "description": "New task name" },
-          "description": { "type": "string", "description": "New description (HTML allowed)" },
-          "person_responsible": { "type": "string", "description": "New owner user ID(s), comma-separated" },
-          "priority": { "type": "string", "description": "none, low, medium, high" },
-          "start_date": { "type": "string", "description": "New start date (MM-DD-YYYY)" },
-          "end_date": { "type": "string", "description": "New due date (MM-DD-YYYY)" },
-          "percent_complete": { "type": "string", "description": "Completion percentage (0-100)" }
-        },
-        "required": ["project_id", "task_id"]
-      }
-    },
-    {
-      "name": "delete_task",
-      "description": "Delete a task from Zoho Projects",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Task ID to delete" }
-        },
-        "required": ["project_id", "task_id"]
-      }
-    },
-    {
-      "name": "add_comment",
-      "description": "Add a comment to a task",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Task ID" },
-          "content": { "type": "string", "description": "Comment text (HTML allowed)" }
-        },
-        "required": ["project_id", "task_id", "content"]
-      }
-    },
-    {
-      "name": "update_comment",
-      "description": "Update an existing comment on a task",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Task ID" },
-          "comment_id": { "type": "string", "description": "Comment ID to update" },
-          "content": { "type": "string", "description": "New comment text (HTML allowed)" }
-        },
-        "required": ["project_id", "task_id", "comment_id", "content"]
-      }
-    },
-    {
-      "name": "delete_comment",
-      "description": "Delete a comment from a task",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Task ID" },
-          "comment_id": { "type": "string", "description": "Comment ID to delete" }
-        },
-        "required": ["project_id", "task_id", "comment_id"]
-      }
-    },
-    {
-      "name": "my_tasks",
-      "description": "Get tasks assigned to the authenticated user across all projects",
+      "name": "zoho_task",
+      "description": "Manage tasks. Resolve owner zpuid via zoho_project action=users; resolve status_id via zoho_project action=statuses.",
       "parameters": {
-        "type": "object",
-        "properties": {
-          "status": { "type": "string", "description": "Filter: all, completed, notcompleted (default: all)" },
-          "priority": { "type": "string", "description": "Filter: all, none, low, medium, high" },
-          "index": { "type": "number", "description": "Page index (default 1)" }
-        }
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "list", "description": "List tasks in a project with optional filters." },
+              "project_id": { "type": "string" },
+              "status": { "type": "string", "description": "all, completed, notcompleted (default all)." },
+              "priority": { "type": "string", "description": "all, none, low, medium, high." },
+              "owner": { "type": "string", "description": "all or zpuid." },
+              "milestone_id": { "type": "string" },
+              "tasklist_id": { "type": "string" },
+              "index": { "type": "number", "description": "Page index (default 1)." }
+            },
+            "required": ["action", "project_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "get", "description": "Get full details of a single task." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "task_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "create", "description": "Create a new task." },
+              "project_id": { "type": "string" },
+              "name": { "type": "string", "description": "Task name/title." },
+              "description": { "type": "string", "description": "HTML allowed." },
+              "person_responsible": { "type": "string", "description": "Owner zpuid(s), comma-separated." },
+              "priority": { "type": "string", "description": "none, low, medium, high." },
+              "start_date": { "type": "string", "description": "MM-DD-YYYY." },
+              "end_date": { "type": "string", "description": "MM-DD-YYYY." },
+              "tasklist_id": { "type": "string" },
+              "milestone_id": { "type": "string" },
+              "status_id": { "type": "string", "description": "From zoho_project action=statuses." },
+              "parent_task_id": { "type": "string", "description": "If set, the new task becomes a subtask of this one." },
+              "billing_type": { "type": "string", "description": "billable, non_billable." },
+              "percent_complete": { "type": "string", "description": "0-100." }
+            },
+            "required": ["action", "project_id", "name"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "update", "description": "Modify an existing task." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" },
+              "name": { "type": "string" },
+              "description": { "type": "string", "description": "HTML allowed." },
+              "person_responsible": { "type": "string", "description": "Owner zpuid(s), comma-separated." },
+              "priority": { "type": "string", "description": "none, low, medium, high." },
+              "start_date": { "type": "string", "description": "MM-DD-YYYY." },
+              "end_date": { "type": "string", "description": "MM-DD-YYYY." },
+              "tasklist_id": { "type": "string" },
+              "milestone_id": { "type": "string" },
+              "status_id": { "type": "string" },
+              "billing_type": { "type": "string" },
+              "percent_complete": { "type": "string" }
+            },
+            "required": ["action", "project_id", "task_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "delete", "description": "Delete a task." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "task_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "subtasks", "description": "List subtasks of a given task." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "task_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "my", "description": "Tasks assigned to the authenticated user across all projects." },
+              "status": { "type": "string", "description": "all, completed, notcompleted." },
+              "priority": { "type": "string" },
+              "index": { "type": "number" }
+            },
+            "required": ["action"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "search", "description": "Keyword search across all projects (or a single project if project_id is set)." },
+              "query": { "type": "string" },
+              "project_id": { "type": "string" }
+            },
+            "required": ["action", "query"],
+            "additionalProperties": false
+          }
+        ]
       }
     },
     {
-      "name": "search_tasks",
-      "description": "Search tasks by keyword across all projects in the portal",
+      "name": "zoho_comment",
+      "description": "Manage task comments.",
       "parameters": {
-        "type": "object",
-        "properties": {
-          "query": { "type": "string", "description": "Search term" },
-          "project_id": { "type": "string", "description": "Optional: limit search to a specific project" }
-        },
-        "required": ["query"]
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "list", "description": "List comments on a task." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" },
+              "index": { "type": "number" }
+            },
+            "required": ["action", "project_id", "task_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "add", "description": "Add a comment to a task." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" },
+              "content": { "type": "string", "description": "HTML allowed." }
+            },
+            "required": ["action", "project_id", "task_id", "content"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "update", "description": "Update an existing comment." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" },
+              "comment_id": { "type": "string" },
+              "content": { "type": "string", "description": "HTML allowed." }
+            },
+            "required": ["action", "project_id", "task_id", "comment_id", "content"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "delete", "description": "Delete a comment." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" },
+              "comment_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "task_id", "comment_id"],
+            "additionalProperties": false
+          }
+        ]
       }
     },
     {
-      "name": "log_time",
-      "description": "Log time (timesheet entry) against a task in Zoho Projects",
+      "name": "zoho_milestone",
+      "description": "Manage project milestones.",
       "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Task ID to log time against" },
-          "hours": { "type": "number", "description": "Hours worked (can include decimals, e.g. 1.5 for 1h 30m)" },
-          "minutes": { "type": "number", "description": "Additional minutes (optional)" },
-          "date": { "type": "string", "description": "Date of work (MM-DD-YYYY format, default: today)" },
-          "notes": { "type": "string", "description": "Description of the work done" }
-        },
-        "required": ["project_id", "task_id", "hours"]
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "list", "description": "List milestones in a project." },
+              "project_id": { "type": "string" },
+              "status": { "type": "string", "description": "internal, external, upcoming, delayed, completed, all." },
+              "index": { "type": "number" }
+            },
+            "required": ["action", "project_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "create", "description": "Create a new milestone." },
+              "project_id": { "type": "string" },
+              "name": { "type": "string" },
+              "start_date": { "type": "string", "description": "MM-DD-YYYY." },
+              "end_date": { "type": "string", "description": "MM-DD-YYYY." },
+              "owner": { "type": "string", "description": "Owner zpuid." },
+              "flag": { "type": "string", "description": "internal or external." },
+              "milestone_type": { "type": "string", "description": "general or custom." }
+            },
+            "required": ["action", "project_id", "name"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "update", "description": "Update a milestone." },
+              "project_id": { "type": "string" },
+              "milestone_id": { "type": "string" },
+              "name": { "type": "string" },
+              "start_date": { "type": "string" },
+              "end_date": { "type": "string" },
+              "owner": { "type": "string" },
+              "flag": { "type": "string" },
+              "status": { "type": "string", "description": "active, archived, completed." }
+            },
+            "required": ["action", "project_id", "milestone_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "delete", "description": "Delete a milestone." },
+              "project_id": { "type": "string" },
+              "milestone_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "milestone_id"],
+            "additionalProperties": false
+          }
+        ]
       }
     },
     {
-      "name": "list_timesheets",
-      "description": "List timesheet entries for a project, optionally filtered by task and date range",
+      "name": "zoho_tasklist",
+      "description": "Manage tasklists within a project (or list portal-wide).",
       "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "task_id": { "type": "string", "description": "Optional: filter by task ID" },
-          "from_date": { "type": "string", "description": "Start date (MM-DD-YYYY)" },
-          "to_date": { "type": "string", "description": "End date (MM-DD-YYYY)" },
-          "index": { "type": "number", "description": "Page index (default 1)" }
-        },
-        "required": ["project_id"]
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "list", "description": "List tasklists in a project, or portal-wide if project_id is omitted." },
+              "project_id": { "type": "string" },
+              "flag": { "type": "string", "description": "internal or external." },
+              "index": { "type": "number" }
+            },
+            "required": ["action"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "get", "description": "Get details of a single tasklist." },
+              "project_id": { "type": "string" },
+              "tasklist_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "tasklist_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "create", "description": "Create a new tasklist." },
+              "project_id": { "type": "string" },
+              "name": { "type": "string" },
+              "milestone_id": { "type": "string" },
+              "flag": { "type": "string", "description": "internal or external." },
+              "status": { "type": "string", "description": "active or archived." }
+            },
+            "required": ["action", "project_id", "name"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "update", "description": "Update a tasklist." },
+              "project_id": { "type": "string" },
+              "tasklist_id": { "type": "string" },
+              "name": { "type": "string" },
+              "milestone_id": { "type": "string" },
+              "flag": { "type": "string" },
+              "status": { "type": "string" }
+            },
+            "required": ["action", "project_id", "tasklist_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "delete", "description": "Delete a tasklist." },
+              "project_id": { "type": "string" },
+              "tasklist_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "tasklist_id"],
+            "additionalProperties": false
+          }
+        ]
       }
     },
     {
-      "name": "delete_timesheet",
-      "description": "Delete a timesheet entry from a Zoho Projects project",
+      "name": "zoho_timesheet",
+      "description": "Manage time log entries against tasks.",
       "parameters": {
-        "type": "object",
-        "properties": {
-          "project_id": { "type": "string", "description": "Zoho Projects project ID" },
-          "log_id": { "type": "string", "description": "Timesheet log ID to delete" }
-        },
-        "required": ["project_id", "log_id"]
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "list", "description": "List timesheet entries for a project, optionally filtered by task and date range." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string", "description": "Filter by task id (optional)." },
+              "from_date": { "type": "string", "description": "MM-DD-YYYY. Default: 30 days ago." },
+              "to_date": { "type": "string", "description": "MM-DD-YYYY. Default: today." },
+              "index": { "type": "number", "description": "Page index (default 1)." }
+            },
+            "required": ["action", "project_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "create", "description": "Log time against a task." },
+              "project_id": { "type": "string" },
+              "task_id": { "type": "string" },
+              "hours": { "type": "number", "description": "Hours worked (decimals allowed, e.g. 1.5)." },
+              "minutes": { "type": "number", "description": "Additional minutes (optional)." },
+              "date": { "type": "string", "description": "MM-DD-YYYY. Default: today." },
+              "notes": { "type": "string", "description": "Description of the work done." },
+              "bill_status": { "type": "string", "description": "Billable or Non Billable. Default: Billable." }
+            },
+            "required": ["action", "project_id", "task_id", "hours"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "update", "description": "Modify an existing time log entry." },
+              "project_id": { "type": "string" },
+              "log_id": { "type": "string" },
+              "hours": { "type": "number" },
+              "minutes": { "type": "number" },
+              "date": { "type": "string", "description": "MM-DD-YYYY." },
+              "notes": { "type": "string" },
+              "log_name": { "type": "string" },
+              "bill_status": { "type": "string" }
+            },
+            "required": ["action", "project_id", "log_id"],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "action": { "const": "delete", "description": "Delete a time log entry." },
+              "project_id": { "type": "string" },
+              "log_id": { "type": "string" }
+            },
+            "required": ["action", "project_id", "log_id"],
+            "additionalProperties": false
+          }
+        ]
       }
     }
   ],