Phase 2 Complete: Message queuing for offline/inactive projects Queue Storage: - File-based queue in ~/.innervoice/queues/ - Separate JSON file per project - Persistent storage survives restarts - Auto-cleanup of old delivered tasks (7 days) Queue Manager (queue-manager.ts): - enqueueTask() - Add task to project queue - getPendingTasks() - Get undelivered tasks - markTaskDelivered() - Mark task as complete - getQueueSummary() - Get overview of all queues - cleanupOldTasks() - Remove old delivered tasks API Endpoints: - POST /queue/add - Queue message for project - GET /queue/:projectName - Get pending tasks - POST /queue/:projectName/mark-delivered - Mark delivered - GET /queue/summary - Get all project summaries Telegram Bot Features: - /queue command - Show all queued messages - Project-targeted messages: "ProjectName: message" - Auto-detect if project is online or offline - Queue for offline, deliver immediately if online MCP Tool: - telegram_check_queue - Check for queued messages on startup - Shows pending messages with timestamps - Perfect for checking what happened while offline Usage Scenarios: 1. Send to offline project: You: "ESO-MCP: Continue with roadmap" Bot: "📥 Message queued for ESO-MCP (offline)" 2. Open Claude in ESO-MCP: Claude auto-checks queue on startup Shows: "📬 You have 1 queued message: Continue with roadmap" 3. Check queue status: You: "/queue" Bot: Shows all projects with pending messages This solves the "no one listening" problem - messages are stored and delivered when Claude opens in that project. Next: Phase 3 (remote Claude spawner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
// Message Queue Manager for InnerVoice
|
|
// Stores messages for offline/inactive projects
|
|
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import { existsSync } from 'fs';
|
|
|
|
const QUEUE_DIR = path.join(process.env.HOME || '~', '.innervoice', 'queues');
|
|
|
|
export interface QueuedTask {
|
|
id: string;
|
|
projectName: string;
|
|
projectPath: string;
|
|
message: string;
|
|
from: string;
|
|
timestamp: Date;
|
|
priority: 'low' | 'normal' | 'high';
|
|
status: 'pending' | 'delivered' | 'expired';
|
|
}
|
|
|
|
// Ensure queue directory exists
|
|
async function ensureQueueDir(): Promise<void> {
|
|
if (!existsSync(QUEUE_DIR)) {
|
|
await fs.mkdir(QUEUE_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Get queue file path for a project
|
|
function getQueuePath(projectName: string): string {
|
|
const safeName = projectName.replace(/[^a-z0-9-_]/gi, '_').toLowerCase();
|
|
return path.join(QUEUE_DIR, `${safeName}.json`);
|
|
}
|
|
|
|
// Load queue for a project
|
|
export async function loadQueue(projectName: string): Promise<QueuedTask[]> {
|
|
await ensureQueueDir();
|
|
const queuePath = getQueuePath(projectName);
|
|
|
|
if (!existsSync(queuePath)) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const content = await fs.readFile(queuePath, 'utf-8');
|
|
const tasks = JSON.parse(content);
|
|
// Convert timestamp strings back to Date objects
|
|
return tasks.map((t: any) => ({
|
|
...t,
|
|
timestamp: new Date(t.timestamp)
|
|
}));
|
|
} catch (error) {
|
|
console.error(`Error loading queue for ${projectName}:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Save queue for a project
|
|
export async function saveQueue(projectName: string, tasks: QueuedTask[]): Promise<void> {
|
|
await ensureQueueDir();
|
|
const queuePath = getQueuePath(projectName);
|
|
await fs.writeFile(queuePath, JSON.stringify(tasks, null, 2));
|
|
}
|
|
|
|
// Add a task to the queue
|
|
export async function enqueueTask(task: Omit<QueuedTask, 'id' | 'status'>): Promise<QueuedTask> {
|
|
const queue = await loadQueue(task.projectName);
|
|
|
|
const newTask: QueuedTask = {
|
|
...task,
|
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
status: 'pending'
|
|
};
|
|
|
|
queue.push(newTask);
|
|
await saveQueue(task.projectName, queue);
|
|
|
|
console.log(`📥 Queued task for ${task.projectName}: ${task.message.substring(0, 50)}...`);
|
|
return newTask;
|
|
}
|
|
|
|
// Get pending tasks for a project
|
|
export async function getPendingTasks(projectName: string): Promise<QueuedTask[]> {
|
|
const queue = await loadQueue(projectName);
|
|
return queue.filter(t => t.status === 'pending');
|
|
}
|
|
|
|
// Mark a task as delivered
|
|
export async function markTaskDelivered(projectName: string, taskId: string): Promise<void> {
|
|
const queue = await loadQueue(projectName);
|
|
const task = queue.find(t => t.id === taskId);
|
|
|
|
if (task) {
|
|
task.status = 'delivered';
|
|
await saveQueue(projectName, queue);
|
|
console.log(`✅ Task delivered: ${taskId}`);
|
|
}
|
|
}
|
|
|
|
// Clear delivered tasks older than N days
|
|
export async function cleanupOldTasks(projectName: string, daysOld: number = 7): Promise<number> {
|
|
const queue = await loadQueue(projectName);
|
|
const cutoff = new Date();
|
|
cutoff.setDate(cutoff.getDate() - daysOld);
|
|
|
|
const filtered = queue.filter(t => {
|
|
if (t.status === 'delivered' && t.timestamp < cutoff) {
|
|
return false; // Remove old delivered tasks
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const removed = queue.length - filtered.length;
|
|
if (removed > 0) {
|
|
await saveQueue(projectName, filtered);
|
|
console.log(`🧹 Cleaned up ${removed} old tasks for ${projectName}`);
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
// List all projects with queued tasks
|
|
export async function listProjectsWithQueues(): Promise<string[]> {
|
|
await ensureQueueDir();
|
|
const files = await fs.readdir(QUEUE_DIR);
|
|
return files
|
|
.filter(f => f.endsWith('.json'))
|
|
.map(f => f.replace('.json', '').replace(/_/g, '-'));
|
|
}
|
|
|
|
// Get queue summary for all projects
|
|
export async function getQueueSummary(): Promise<{ projectName: string; pending: number; total: number }[]> {
|
|
const projects = await listProjectsWithQueues();
|
|
const summaries = await Promise.all(
|
|
projects.map(async (projectName) => {
|
|
const queue = await loadQueue(projectName);
|
|
return {
|
|
projectName,
|
|
pending: queue.filter(t => t.status === 'pending').length,
|
|
total: queue.length
|
|
};
|
|
})
|
|
);
|
|
|
|
return summaries.filter(s => s.total > 0);
|
|
}
|