import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; import 'base_tool.dart'; /// Tool for managing background tasks /// Tasks are persisted to ~/.clawd_code/tasks/ directory class TaskTool extends BaseTool { @override final String name = 'Task'; @override final String description = 'Create, list, get, update, and stop background tasks'; // In-memory cache (loaded from disk) static final Map> _tasks = {}; static int _taskCounter = 1; static bool _initialized = false; @override Future execute(Map input) async { // Initialize task storage on first use if (!_initialized) { await _loadTasks(); _initialized = true; } final action = input['action'] as String? ?? 'list'; final taskId = input['task_id'] as String?; final taskName = input['name'] as String?; final command = input['command'] as String?; switch (action.toLowerCase()) { case 'create': return await _createTask(command: command, name: taskName); case 'list': return _listTasks(); case 'get': if (taskId == null) { return 'Error: task_id is required for get action'; } return _getTask(taskId); case 'update': if (taskId == null) { return 'Error: task_id is required for update action'; } final status = input['status'] as String?; final output = input['output'] as String?; return await _updateTask(taskId, status: status, output: output); case 'stop': if (taskId == null) { return 'Error: task_id is required for stop action'; } return await _stopTask(taskId); case 'output': if (taskId == null) { return 'Error: task_id is required for output action'; } return _getTaskOutput(taskId); default: return 'Error: Unknown action "$action". Available actions: create, list, get, update, stop, output'; } } Future _createTask({String? command, String? name}) async { final taskId = 'task_${_taskCounter++}'; final now = DateTime.now().toUtc(); _tasks[taskId] = { 'id': taskId, 'name': name ?? 'Unnamed task', 'command': command ?? '', 'status': 'running', 'created_at': now.toIso8601String(), 'updated_at': now.toIso8601String(), 'output': '', }; await _saveTasks(); return '''Created task: $taskId Name: ${name ?? 'Unnamed task'} Command: ${command ?? '(no command)'} Status: running Created: ${now.toLocal()} Use /tasks to list tasks or Task:get task_id=$taskId to check status.'''; } String _listTasks() { if (_tasks.isEmpty) { return 'No tasks found. Create one with Task:create command="your command"'; } final buffer = StringBuffer(); buffer.writeln('Tasks (${_tasks.length}):'); buffer.writeln('─' * 50); for (final task in _tasks.values) { final id = task['id'] as String; final name = task['name'] as String; final status = task['status'] as String; final createdAt = task['created_at'] as String; final updatedAt = task['updated_at'] as String; final created = DateTime.parse(createdAt).toLocal(); final updated = DateTime.parse(updatedAt).toLocal(); buffer.writeln('$id: $name'); buffer.writeln(' Status: $status'); buffer.writeln(' Created: ${created.toString().substring(0, 16)}'); buffer.writeln(' Updated: ${updated.toString().substring(0, 16)}'); if (task['command'] != null && (task['command'] as String).isNotEmpty) { buffer.writeln(' Command: ${task['command']}'); } buffer.writeln(); } return buffer.toString(); } String _getTask(String taskId) { final task = _tasks[taskId]; if (task == null) { return 'Error: Task "$taskId" not found'; } final buffer = StringBuffer(); buffer.writeln('Task: ${task['name']}'); buffer.writeln('ID: $taskId'); buffer.writeln('Status: ${task['status']}'); buffer.writeln('Command: ${task['command']}'); buffer.writeln('Created: ${DateTime.parse(task['created_at'] as String).toLocal()}'); buffer.writeln('Updated: ${DateTime.parse(task['updated_at'] as String).toLocal()}'); final output = task['output'] as String; if (output.isNotEmpty) { buffer.writeln(); buffer.writeln('Output:'); buffer.writeln(output); } return buffer.toString(); } Future _updateTask(String taskId, {String? status, String? output}) async { final task = _tasks[taskId]; if (task == null) { return 'Error: Task "$taskId" not found'; } if (status != null) { task['status'] = status; } if (output != null) { task['output'] = output; } task['updated_at'] = DateTime.now().toUtc().toIso8601String(); await _saveTasks(); return 'Updated task $taskId'; } Future _stopTask(String taskId) async { final task = _tasks[taskId]; if (task == null) { return 'Error: Task "$taskId" not found'; } task['status'] = 'stopped'; task['updated_at'] = DateTime.now().toUtc().toIso8601String(); await _saveTasks(); return 'Stopped task $taskId'; } String _getTaskOutput(String taskId) { final task = _tasks[taskId]; if (task == null) { return 'Error: Task "$taskId" not found'; } final output = task['output'] as String; if (output.isEmpty) { return 'No output recorded for task $taskId'; } return output; } // Persistence: load tasks from disk Future _loadTasks() async { try { final dir = _getTasksDirectory(); if (!await dir.exists()) { return; } _tasks.clear(); _taskCounter = 1; final files = dir.listSync(); for (final file in files) { if (file is! File || !file.path.endsWith('.json')) { continue; } try { final content = await file.readAsString(); final json = jsonDecode(content) as Map; final taskId = json['id'] as String?; if (taskId != null) { _tasks[taskId] = json; // Update counter final match = RegExp(r'task_(\d+)').firstMatch(taskId); if (match != null) { final num = int.tryParse(match.group(1)!); if (num != null && num >= _taskCounter) { _taskCounter = num + 1; } } } } catch (_) { // Skip malformed files } } } catch (_) { // If loading fails, continue with empty } } // Persistence: save tasks to disk Future _saveTasks() async { try { final dir = _getTasksDirectory(); await dir.create(recursive: true); for (final entry in _tasks.entries) { final file = File(path.join(dir.path, '${entry.key}.json')); await file.writeAsString(jsonEncode(entry.value)); } } catch (_) { // Silently fail - tasks stored in memory anyway } } Directory _getTasksDirectory() { final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? ''; return Directory(path.join(home, '.clawd_code', 'tasks')); } }