simple-task-master
Version:
A simple command-line task management tool
531 lines (450 loc) • 13.6 kB
JavaScript
/**
* Generic Tool Integration Example for Simple Task Master
*
* This example demonstrates a reusable pattern for integrating any external tool
* with STM using unknown fields. It provides a base class that can be extended
* for specific tool integrations.
*/
const { exec } = require('child_process');
const { promisify } = require('util');
// Example imports - uncomment as needed:
// const fs = require('fs').promises;
// const path = require('path');
// const yaml = require('js-yaml');
const execAsync = promisify(exec);
/**
* Base class for STM tool integrations
*/
class STMToolIntegration {
constructor(toolName, toolVersion, options = {}) {
this.toolName = toolName;
this.toolVersion = toolVersion;
this.fieldPrefix = options.fieldPrefix || toolName.toLowerCase().replace(/\s+/g, '_');
this.workspacePath = options.workspacePath || process.cwd();
this.debug = options.debug || false;
}
/**
* Log debug messages
*/
log(...args) {
if (this.debug) {
console.log(`[${this.toolName}]`, ...args);
}
}
/**
* Execute STM CLI command
*/
async runSTM(args) {
const command = `stm ${args}`;
this.log(`Executing: ${command}`);
try {
const { stdout, stderr } = await execAsync(command, {
cwd: this.workspacePath
});
if (stderr && this.debug) {
console.error(`[${this.toolName}] stderr:`, stderr);
}
return stdout.trim();
} catch (error) {
throw new Error(`STM command failed: ${error.message}`);
}
}
/**
* Get a task by ID
*/
async getTask(taskId) {
const output = await this.runSTM(`show ${taskId} --json`);
return JSON.parse(output);
}
/**
* List all tasks
*/
async listTasks(filters = {}) {
let args = 'list --json';
if (filters.status) {
args += ` --status ${filters.status}`;
}
if (filters.tags) {
args += ` --tags ${filters.tags.join(',')}`;
}
const output = await this.runSTM(args);
return JSON.parse(output);
}
/**
* Update task with tool-specific fields
*/
async updateTaskFields(taskId, fields) {
const updates = [];
for (const [key, value] of Object.entries(fields)) {
const fieldName = this.prefixField(key);
const fieldValue = this.serializeValue(value);
updates.push(`${fieldName}=${fieldValue}`);
}
if (updates.length > 0) {
await this.runSTM(`update ${taskId} ${updates.join(' ')}`);
this.log(`Updated task ${taskId} with ${updates.length} fields`);
}
}
/**
* Get tool-specific fields from a task
*/
getToolFields(task) {
const toolFields = {};
const prefix = `${this.fieldPrefix}_`;
for (const [key, value] of Object.entries(task)) {
if (key.startsWith(prefix)) {
const fieldName = key.substring(prefix.length);
toolFields[fieldName] = this.deserializeValue(value);
}
}
return toolFields;
}
/**
* Initialize tool metadata on a task
*/
async initializeTask(taskId, config = {}) {
const metadata = {
tool_name: this.toolName,
tool_version: this.toolVersion,
initialized_at: new Date().toISOString(),
config: config
};
await this.updateTaskFields(taskId, metadata);
this.log(`Initialized task ${taskId} for ${this.toolName}`);
}
/**
* Mark task as processed by this tool
*/
async markProcessed(taskId, result) {
const fields = {
last_processed: new Date().toISOString(),
process_count: await this.incrementCounter(taskId, 'process_count'),
last_result: result
};
await this.updateTaskFields(taskId, fields);
}
/**
* Add an event to task history
*/
async addEvent(taskId, event) {
const task = await this.getTask(taskId);
const historyField = this.prefixField('history');
// Get existing history or create new array
let history = [];
if (task[historyField]) {
try {
history = JSON.parse(task[historyField]);
} catch {
this.log('Warning: Could not parse history, starting fresh');
}
}
// Add new event
history.push({
timestamp: new Date().toISOString(),
event: event.type,
details: event.details,
user: event.user || 'system'
});
// Keep only last 50 events
if (history.length > 50) {
history = history.slice(-50);
}
await this.runSTM(`update ${taskId} ${historyField}='${JSON.stringify(history)}'`);
}
/**
* Helper to prefix field names
*/
prefixField(fieldName) {
return `${this.fieldPrefix}_${fieldName}`;
}
/**
* Serialize value for CLI
*/
serializeValue(value) {
if (typeof value === 'string') {
return value;
}
return JSON.stringify(value);
}
/**
* Deserialize value from string
*/
deserializeValue(value) {
if (typeof value !== 'string') {
return value;
}
// Try to parse as JSON
try {
return JSON.parse(value);
} catch {
// Return as string if not valid JSON
return value;
}
}
/**
* Increment a counter field
*/
async incrementCounter(taskId, counterName) {
const task = await this.getTask(taskId);
const fieldName = this.prefixField(counterName);
const currentValue = parseInt(task[fieldName] || '0', 10);
return currentValue + 1;
}
/**
* Find tasks that need processing by this tool
*/
async findUnprocessedTasks() {
const allTasks = await this.listTasks();
const toolField = this.prefixField('last_processed');
return allTasks.filter(task => !task[toolField]);
}
/**
* Sync task with external system
*/
async syncTask(taskId, externalData) {
const syncData = {
last_sync: new Date().toISOString(),
sync_source: externalData.source,
external_id: externalData.id,
external_status: externalData.status,
sync_data: externalData.data
};
await this.updateTaskFields(taskId, syncData);
await this.addEvent(taskId, {
type: 'sync',
details: {
source: externalData.source,
external_id: externalData.id,
fields_updated: Object.keys(syncData).length
}
});
}
/**
* Generate a report of tool usage
*/
async generateReport() {
const allTasks = await this.listTasks();
const toolPrefix = `${this.fieldPrefix}_`;
const report = {
tool: this.toolName,
version: this.toolVersion,
total_tasks: allTasks.length,
processed_tasks: 0,
unprocessed_tasks: 0,
total_events: 0,
last_processed_times: []
};
for (const task of allTasks) {
const hasToolFields = Object.keys(task).some(key => key.startsWith(toolPrefix));
if (hasToolFields) {
report.processed_tasks++;
const lastProcessed = task[`${toolPrefix}last_processed`];
if (lastProcessed) {
report.last_processed_times.push(new Date(lastProcessed));
}
const processCount = parseInt(task[`${toolPrefix}process_count`] || '0', 10);
report.total_events += processCount;
} else {
report.unprocessed_tasks++;
}
}
// Calculate average time between processes
if (report.last_processed_times.length > 0) {
report.last_processed_times.sort((a, b) => b - a);
report.most_recent_process = report.last_processed_times[0];
report.oldest_process = report.last_processed_times[report.last_processed_times.length - 1];
}
return report;
}
}
/**
* Example: Project Management Tool Integration
*/
class ProjectManagementIntegration extends STMToolIntegration {
constructor(options = {}) {
super('Project Manager Pro', '2.0.0', {
fieldPrefix: 'pmp',
...options
});
}
/**
* Assign task to a team member
*/
async assignTask(taskId, assignee, role = 'developer') {
await this.updateTaskFields(taskId, {
assignee: assignee,
assignee_role: role,
assigned_at: new Date().toISOString()
});
await this.addEvent(taskId, {
type: 'assignment',
details: {
assignee: assignee,
role: role
}
});
}
/**
* Update task priority
*/
async setPriority(taskId, priority, reason) {
const validPriorities = ['low', 'medium', 'high', 'critical'];
if (!validPriorities.includes(priority)) {
throw new Error(`Invalid priority: ${priority}`);
}
await this.updateTaskFields(taskId, {
priority: priority,
priority_reason: reason,
priority_updated: new Date().toISOString()
});
await this.addEvent(taskId, {
type: 'priority_change',
details: {
new_priority: priority,
reason: reason
}
});
}
/**
* Add time tracking entry
*/
async logTime(taskId, hours, description, user) {
const task = await this.getTask(taskId);
const timeLogField = this.prefixField('time_log');
let timeLog = [];
if (task[timeLogField]) {
try {
timeLog = JSON.parse(task[timeLogField]);
} catch {
this.log('Warning: Could not parse time log, starting fresh');
}
}
timeLog.push({
date: new Date().toISOString(),
hours: hours,
description: description,
user: user
});
// Calculate total hours
const totalHours = timeLog.reduce((sum, entry) => sum + entry.hours, 0);
await this.updateTaskFields(taskId, {
time_log: timeLog,
total_hours: totalHours,
last_time_entry: new Date().toISOString()
});
await this.addEvent(taskId, {
type: 'time_logged',
details: {
hours: hours,
total_hours: totalHours,
user: user
}
});
}
/**
* Link task to external project
*/
async linkToProject(taskId, projectId, projectName) {
await this.updateTaskFields(taskId, {
project_id: projectId,
project_name: projectName,
project_linked_at: new Date().toISOString()
});
await this.addEvent(taskId, {
type: 'project_linked',
details: {
project_id: projectId,
project_name: projectName
}
});
}
/**
* Get team workload report
*/
async getTeamWorkload() {
const tasks = await this.listTasks({ status: 'in-progress' });
const workload = {};
for (const task of tasks) {
const toolFields = this.getToolFields(task);
if (toolFields.assignee) {
if (!workload[toolFields.assignee]) {
workload[toolFields.assignee] = {
tasks: 0,
total_hours: 0,
priorities: { low: 0, medium: 0, high: 0, critical: 0 }
};
}
workload[toolFields.assignee].tasks++;
workload[toolFields.assignee].total_hours += toolFields.total_hours || 0;
const priority = toolFields.priority || 'medium';
workload[toolFields.assignee].priorities[priority]++;
}
}
return workload;
}
}
/**
* Example usage
*/
async function demonstrateIntegration() {
// Create integration instance
const pm = new ProjectManagementIntegration({ debug: true });
console.log('=== Project Management Integration Demo ===\n');
// Find or create a test task
let taskId;
const tasks = await pm.listTasks();
if (tasks.length > 0) {
taskId = tasks[0].id;
console.log(`Using existing task: ${taskId}`);
} else {
console.log('No tasks found. Please create a task first using: stm add "Test Task"');
return;
}
// Initialize the task for our tool
console.log('\n1. Initializing task for Project Manager Pro...');
await pm.initializeTask(taskId, {
billable: true,
department: 'Engineering'
});
// Assign the task
console.log('\n2. Assigning task...');
await pm.assignTask(taskId, 'john.doe@company.com', 'senior-developer');
// Set priority
console.log('\n3. Setting priority...');
await pm.setPriority(taskId, 'high', 'Customer deadline approaching');
// Log some time
console.log('\n4. Logging time...');
await pm.logTime(taskId, 2.5, 'Initial implementation', 'john.doe@company.com');
await pm.logTime(taskId, 1.5, 'Code review and fixes', 'john.doe@company.com');
// Link to project
console.log('\n5. Linking to external project...');
await pm.linkToProject(taskId, 'PROJ-2024-Q1', 'Q1 Feature Release');
// Mark as processed
console.log('\n6. Marking task as processed...');
await pm.markProcessed(taskId, 'success');
// Get the updated task
console.log('\n7. Retrieving updated task...');
const updatedTask = await pm.getTask(taskId);
const toolFields = pm.getToolFields(updatedTask);
console.log('\nTool-specific fields:');
console.log(JSON.stringify(toolFields, null, 2));
// Generate workload report
console.log('\n8. Generating team workload report...');
const workload = await pm.getTeamWorkload();
console.log('\nTeam Workload:');
console.log(JSON.stringify(workload, null, 2));
// Generate usage report
console.log('\n9. Generating tool usage report...');
const report = await pm.generateReport();
console.log('\nTool Usage Report:');
console.log(JSON.stringify(report, null, 2));
}
// Run demo if executed directly
if (require.main === module) {
demonstrateIntegration().catch(console.error);
}
// Export for use in other projects
module.exports = {
STMToolIntegration,
ProjectManagementIntegration
};