Files
SuperCharged-Claude-Code-Up…/dexto/packages/tools-todo/src/todo-service.ts
admin b52318eeae feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 00:27:56 +04:00

210 lines
6.6 KiB
TypeScript

/**
* Todo Service
*
* Manages todo lists for tracking agent workflow and task progress.
* Emits events through the AgentEventBus using the service:event pattern.
*/
import { nanoid } from 'nanoid';
import type { Database, AgentEventBus, IDextoLogger } from '@dexto/core';
import { DextoRuntimeError } from '@dexto/core';
import { TodoError } from './errors.js';
import type { Todo, TodoInput, TodoUpdateResult, TodoConfig, TodoStatus } from './types.js';
import { TODO_STATUS_VALUES } from './types.js';
const DEFAULT_MAX_TODOS = 100;
const TODOS_KEY_PREFIX = 'todos:';
/**
* TodoService - Manages todo lists for agent workflow tracking
*/
export class TodoService {
private database: Database;
private eventBus: AgentEventBus;
private logger: IDextoLogger;
private config: Required<TodoConfig>;
private initialized: boolean = false;
constructor(
database: Database,
eventBus: AgentEventBus,
logger: IDextoLogger,
config: TodoConfig = {}
) {
this.database = database;
this.eventBus = eventBus;
this.logger = logger;
this.config = {
maxTodosPerSession: config.maxTodosPerSession ?? DEFAULT_MAX_TODOS,
enableEvents: config.enableEvents ?? true,
};
}
/**
* Initialize the service
*/
async initialize(): Promise<void> {
if (this.initialized) {
this.logger.debug('TodoService already initialized');
return;
}
this.initialized = true;
this.logger.info('TodoService initialized successfully');
}
/**
* Update todos for a session (replaces entire list)
*/
async updateTodos(sessionId: string, todoInputs: TodoInput[]): Promise<TodoUpdateResult> {
if (!this.initialized) {
throw TodoError.notInitialized();
}
// Validate todo count
if (todoInputs.length > this.config.maxTodosPerSession) {
throw TodoError.todoLimitExceeded(todoInputs.length, this.config.maxTodosPerSession);
}
try {
// Get existing todos
const existing = await this.getTodos(sessionId);
const existingMap = new Map(existing.map((t) => [this.getTodoKey(t), t]));
// Create new todos with IDs
const now = new Date();
const newTodos: Todo[] = [];
const stats = { created: 0, updated: 0, deleted: 0 };
for (let i = 0; i < todoInputs.length; i++) {
const input = todoInputs[i]!;
this.validateTodoStatus(input.status);
// Generate consistent key for matching
const todoKey = this.getTodoKeyFromInput(input);
const existingTodo = existingMap.get(todoKey);
if (existingTodo) {
// Update existing todo
const updated: Todo = {
...existingTodo,
status: input.status,
updatedAt: now,
position: i,
};
newTodos.push(updated);
stats.updated++;
existingMap.delete(todoKey);
} else {
// Create new todo
const created: Todo = {
id: nanoid(),
sessionId,
content: input.content,
activeForm: input.activeForm,
status: input.status,
position: i,
createdAt: now,
updatedAt: now,
};
newTodos.push(created);
stats.created++;
}
}
// Remaining items in existingMap are deleted
stats.deleted = existingMap.size;
// Save to database
const key = this.getTodosDatabaseKey(sessionId);
await this.database.set(key, newTodos);
// Emit event using the service:event pattern
if (this.config.enableEvents) {
this.eventBus.emit('service:event', {
service: 'todo',
event: 'updated',
sessionId,
data: {
todos: newTodos,
stats,
},
});
}
this.logger.debug(
`Updated todos for session ${sessionId}: ${stats.created} created, ${stats.updated} updated, ${stats.deleted} deleted`
);
return {
todos: newTodos,
sessionId,
...stats,
};
} catch (error) {
if (error instanceof DextoRuntimeError) {
throw error;
}
throw TodoError.databaseError(
'updateTodos',
error instanceof Error ? error.message : String(error)
);
}
}
/**
* Get todos for a session
*/
async getTodos(sessionId: string): Promise<Todo[]> {
if (!this.initialized) {
throw TodoError.notInitialized();
}
try {
const key = this.getTodosDatabaseKey(sessionId);
const todos = await this.database.get<Todo[]>(key);
return todos || [];
} catch (error) {
if (error instanceof DextoRuntimeError) {
throw error;
}
throw TodoError.databaseError(
'getTodos',
error instanceof Error ? error.message : String(error)
);
}
}
/**
* Generate database key for session todos
*/
private getTodosDatabaseKey(sessionId: string): string {
return `${TODOS_KEY_PREFIX}${sessionId}`;
}
/**
* Generate consistent key for todo matching (content + activeForm)
* Uses JSON encoding to prevent collisions when fields contain delimiters
*/
private getTodoKey(todo: Todo | TodoInput): string {
return JSON.stringify([todo.content, todo.activeForm]);
}
/**
* Generate key from TodoInput
* Uses JSON encoding to prevent collisions when fields contain delimiters
*/
private getTodoKeyFromInput(input: TodoInput): string {
return JSON.stringify([input.content, input.activeForm]);
}
/**
* Validate todo status
*/
private validateTodoStatus(status: TodoStatus): void {
if (!TODO_STATUS_VALUES.includes(status)) {
throw TodoError.invalidStatus(status);
}
}
}