254 lines
No EOL
7.2 KiB
Dart
254 lines
No EOL
7.2 KiB
Dart
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<String, Map<String, dynamic>> _tasks = {};
|
|
static int _taskCounter = 1;
|
|
static bool _initialized = false;
|
|
|
|
@override
|
|
Future<String> execute(Map<String, dynamic> 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<String> _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<String> _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<String> _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<void> _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<String, dynamic>;
|
|
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<void> _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'));
|
|
}
|
|
} |