feat(#25): implement mosaic-plugin-tasks skill
This commit is contained in:
429
packages/skills/tasks/scripts/tasks.js
Executable file
429
packages/skills/tasks/scripts/tasks.js
Executable file
@@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Mosaic Tasks CLI
|
||||
*
|
||||
* Command-line interface for Mosaic Stack's Tasks API
|
||||
* Supports create, list, get, update, and delete operations
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const { URL } = require('url');
|
||||
|
||||
// Configuration from environment
|
||||
const MOSAIC_API_URL = process.env.MOSAIC_API_URL || 'http://localhost:3001';
|
||||
const MOSAIC_WORKSPACE_ID = process.env.MOSAIC_WORKSPACE_ID;
|
||||
const MOSAIC_API_TOKEN = process.env.MOSAIC_API_TOKEN;
|
||||
|
||||
// Valid enum values
|
||||
const TASK_STATUSES = ['NOT_STARTED', 'IN_PROGRESS', 'PAUSED', 'COMPLETED', 'ARCHIVED'];
|
||||
const TASK_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH'];
|
||||
|
||||
/**
|
||||
* Make HTTP request to Mosaic API
|
||||
*/
|
||||
function apiRequest(method, path, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!MOSAIC_API_TOKEN) {
|
||||
reject(new Error('MOSAIC_API_TOKEN environment variable is required'));
|
||||
return;
|
||||
}
|
||||
if (!MOSAIC_WORKSPACE_ID) {
|
||||
reject(new Error('MOSAIC_WORKSPACE_ID environment variable is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(path, MOSAIC_API_URL);
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const options = {
|
||||
method,
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${MOSAIC_API_TOKEN}`,
|
||||
'X-Workspace-ID': MOSAIC_WORKSPACE_ID,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = client.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = data ? JSON.parse(data) : null;
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(parsed);
|
||||
} else {
|
||||
const error = new Error(
|
||||
parsed?.message || `HTTP ${res.statusCode}: ${res.statusMessage}`
|
||||
);
|
||||
error.statusCode = res.statusCode;
|
||||
error.response = parsed;
|
||||
reject(error);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error(`Failed to parse response: ${err.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(new Error(`Request failed: ${err.message}`));
|
||||
});
|
||||
|
||||
if (body) {
|
||||
req.write(JSON.stringify(body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command-line arguments
|
||||
*/
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
const positional = [];
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
|
||||
if (arg.startsWith('--')) {
|
||||
const key = arg.slice(2);
|
||||
const value = argv[i + 1];
|
||||
|
||||
if (value && !value.startsWith('--')) {
|
||||
args[key] = value;
|
||||
i++; // Skip next arg since we consumed it
|
||||
} else {
|
||||
args[key] = true; // Flag without value
|
||||
}
|
||||
} else {
|
||||
positional.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return { args, positional };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query string from filters
|
||||
*/
|
||||
function buildQueryString(filters) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const qs = params.toString();
|
||||
return qs ? `?${qs}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task
|
||||
*/
|
||||
async function createTask(args) {
|
||||
const body = {
|
||||
title: args.title,
|
||||
};
|
||||
|
||||
if (args.description) body.description = args.description;
|
||||
if (args.status) {
|
||||
if (!TASK_STATUSES.includes(args.status)) {
|
||||
throw new Error(`Invalid status. Must be one of: ${TASK_STATUSES.join(', ')}`);
|
||||
}
|
||||
body.status = args.status;
|
||||
}
|
||||
if (args.priority) {
|
||||
if (!TASK_PRIORITIES.includes(args.priority)) {
|
||||
throw new Error(`Invalid priority. Must be one of: ${TASK_PRIORITIES.join(', ')}`);
|
||||
}
|
||||
body.priority = args.priority;
|
||||
}
|
||||
if (args.due) body.dueDate = args.due;
|
||||
if (args['assignee-id']) body.assigneeId = args['assignee-id'];
|
||||
if (args['project-id']) body.projectId = args['project-id'];
|
||||
if (args['parent-id']) body.parentId = args['parent-id'];
|
||||
if (args['sort-order']) body.sortOrder = parseInt(args['sort-order'], 10);
|
||||
|
||||
if (!body.title) {
|
||||
throw new Error('--title is required');
|
||||
}
|
||||
|
||||
const result = await apiRequest('POST', '/api/tasks', body);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* List tasks with optional filters
|
||||
*/
|
||||
async function listTasks(args) {
|
||||
const filters = {};
|
||||
|
||||
if (args.status) {
|
||||
if (!TASK_STATUSES.includes(args.status)) {
|
||||
throw new Error(`Invalid status. Must be one of: ${TASK_STATUSES.join(', ')}`);
|
||||
}
|
||||
filters.status = args.status;
|
||||
}
|
||||
if (args.priority) {
|
||||
if (!TASK_PRIORITIES.includes(args.priority)) {
|
||||
throw new Error(`Invalid priority. Must be one of: ${TASK_PRIORITIES.join(', ')}`);
|
||||
}
|
||||
filters.priority = args.priority;
|
||||
}
|
||||
if (args['assignee-id']) filters.assigneeId = args['assignee-id'];
|
||||
if (args['project-id']) filters.projectId = args['project-id'];
|
||||
if (args['parent-id']) filters.parentId = args['parent-id'];
|
||||
if (args.page) filters.page = args.page;
|
||||
if (args.limit) filters.limit = args.limit;
|
||||
|
||||
// Handle overdue filter
|
||||
if (args.overdue) {
|
||||
filters.dueDateTo = new Date().toISOString();
|
||||
// Exclude completed and archived
|
||||
if (!filters.status) {
|
||||
console.error('Note: Overdue filter shows tasks with due date < now, excluding COMPLETED and ARCHIVED');
|
||||
}
|
||||
}
|
||||
|
||||
if (args['due-from']) filters.dueDateFrom = args['due-from'];
|
||||
if (args['due-to']) filters.dueDateTo = args['due-to'];
|
||||
|
||||
const query = buildQueryString(filters);
|
||||
const result = await apiRequest('GET', `/api/tasks${query}`);
|
||||
|
||||
// Filter out completed/archived for overdue query
|
||||
if (args.overdue && result.data) {
|
||||
result.data = result.data.filter(
|
||||
task => task.status !== 'COMPLETED' && task.status !== 'ARCHIVED'
|
||||
);
|
||||
if (result.pagination) {
|
||||
result.pagination.total = result.data.length;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single task by ID
|
||||
*/
|
||||
async function getTask(taskId) {
|
||||
if (!taskId) {
|
||||
throw new Error('Task ID is required');
|
||||
}
|
||||
|
||||
const result = await apiRequest('GET', `/api/tasks/${taskId}`);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task
|
||||
*/
|
||||
async function updateTask(taskId, args) {
|
||||
if (!taskId) {
|
||||
throw new Error('Task ID is required');
|
||||
}
|
||||
|
||||
const body = {};
|
||||
|
||||
if (args.title) body.title = args.title;
|
||||
if (args.description !== undefined) body.description = args.description;
|
||||
if (args.status) {
|
||||
if (!TASK_STATUSES.includes(args.status)) {
|
||||
throw new Error(`Invalid status. Must be one of: ${TASK_STATUSES.join(', ')}`);
|
||||
}
|
||||
body.status = args.status;
|
||||
}
|
||||
if (args.priority) {
|
||||
if (!TASK_PRIORITIES.includes(args.priority)) {
|
||||
throw new Error(`Invalid priority. Must be one of: ${TASK_PRIORITIES.join(', ')}`);
|
||||
}
|
||||
body.priority = args.priority;
|
||||
}
|
||||
if (args.due !== undefined) body.dueDate = args.due || null;
|
||||
if (args['assignee-id'] !== undefined) body.assigneeId = args['assignee-id'] || null;
|
||||
if (args['project-id'] !== undefined) body.projectId = args['project-id'] || null;
|
||||
if (args['parent-id'] !== undefined) body.parentId = args['parent-id'] || null;
|
||||
if (args['sort-order']) body.sortOrder = parseInt(args['sort-order'], 10);
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
throw new Error('At least one field to update is required');
|
||||
}
|
||||
|
||||
const result = await apiRequest('PATCH', `/api/tasks/${taskId}`, body);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a task
|
||||
*/
|
||||
async function deleteTask(taskId) {
|
||||
if (!taskId) {
|
||||
throw new Error('Task ID is required');
|
||||
}
|
||||
|
||||
const result = await apiRequest('DELETE', `/api/tasks/${taskId}`);
|
||||
console.log(JSON.stringify(result || { success: true }, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show usage help
|
||||
*/
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
Mosaic Tasks CLI
|
||||
|
||||
Usage:
|
||||
node tasks.js <command> [options]
|
||||
|
||||
Commands:
|
||||
create Create a new task
|
||||
list List tasks with optional filters
|
||||
get <id> Get a specific task by ID
|
||||
update <id> Update a task
|
||||
delete <id> Delete a task
|
||||
help Show this help message
|
||||
|
||||
Create Options:
|
||||
--title <text> Task title (required)
|
||||
--description <text> Task description
|
||||
--status <status> Status: ${TASK_STATUSES.join(', ')}
|
||||
--priority <priority> Priority: ${TASK_PRIORITIES.join(', ')}
|
||||
--due <iso-date> Due date (ISO 8601 format)
|
||||
--assignee-id <uuid> Assign to user ID
|
||||
--project-id <uuid> Link to project ID
|
||||
--parent-id <uuid> Parent task ID (for subtasks)
|
||||
--sort-order <number> Sort order (integer)
|
||||
|
||||
List Options:
|
||||
--status <status> Filter by status
|
||||
--priority <priority> Filter by priority
|
||||
--assignee-id <uuid> Filter by assignee
|
||||
--project-id <uuid> Filter by project
|
||||
--parent-id <uuid> Filter by parent task
|
||||
--due-from <iso-date> Filter tasks due after this date
|
||||
--due-to <iso-date> Filter tasks due before this date
|
||||
--overdue Show only overdue tasks
|
||||
--page <number> Page number (default: 1)
|
||||
--limit <number> Results per page (default: 50, max: 100)
|
||||
|
||||
Update Options:
|
||||
--title <text> Update title
|
||||
--description <text> Update description
|
||||
--status <status> Update status
|
||||
--priority <priority> Update priority
|
||||
--due <iso-date> Update due date (empty string to clear)
|
||||
--assignee-id <uuid> Update assignee (empty string to clear)
|
||||
--project-id <uuid> Update project (empty string to clear)
|
||||
--parent-id <uuid> Update parent (empty string to clear)
|
||||
--sort-order <number> Update sort order
|
||||
|
||||
Environment Variables:
|
||||
MOSAIC_API_URL API base URL (default: http://localhost:3001)
|
||||
MOSAIC_WORKSPACE_ID Workspace ID (required)
|
||||
MOSAIC_API_TOKEN Authentication token (required)
|
||||
|
||||
Examples:
|
||||
# Create a task
|
||||
node tasks.js create --title "Review PR #123" --priority HIGH
|
||||
|
||||
# List high priority tasks
|
||||
node tasks.js list --priority HIGH
|
||||
|
||||
# List overdue tasks
|
||||
node tasks.js list --overdue
|
||||
|
||||
# Get task details
|
||||
node tasks.js get abc-123-def-456
|
||||
|
||||
# Mark task as completed
|
||||
node tasks.js update abc-123-def-456 --status COMPLETED
|
||||
|
||||
# Delete a task
|
||||
node tasks.js delete abc-123-def-456
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
async function main() {
|
||||
const { args, positional } = parseArgs(process.argv.slice(2));
|
||||
const command = positional[0];
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case 'create':
|
||||
await createTask(args);
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
await listTasks(args);
|
||||
break;
|
||||
|
||||
case 'get':
|
||||
await getTask(positional[1]);
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
await updateTask(positional[1], args);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
await deleteTask(positional[1]);
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
case undefined:
|
||||
showHelp();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
console.error('Run "node tasks.js help" for usage information');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
|
||||
if (error.statusCode === 401) {
|
||||
console.error('\nAuthentication failed. Check your MOSAIC_API_TOKEN.');
|
||||
} else if (error.statusCode === 403) {
|
||||
console.error('\nPermission denied. You may need higher workspace permissions.');
|
||||
} else if (error.statusCode === 404) {
|
||||
console.error('\nTask not found. Check the task ID.');
|
||||
} else if (error.response) {
|
||||
console.error('\nAPI Response:', JSON.stringify(error.response, null, 2));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { createTask, listTasks, getTask, updateTask, deleteTask };
|
||||
Reference in New Issue
Block a user