feat(phase2): add message queue system for offline projects
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>
This commit is contained in:
131
src/index.ts
131
src/index.ts
@@ -3,6 +3,13 @@ import express from 'express';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {
|
||||
enqueueTask,
|
||||
getPendingTasks,
|
||||
markTaskDelivered,
|
||||
getQueueSummary,
|
||||
cleanupOldTasks
|
||||
} from './queue-manager.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -144,11 +151,33 @@ bot.command('sessions', async (ctx) => {
|
||||
}).join('\n\n');
|
||||
|
||||
await ctx.reply(
|
||||
`*Active Claude Sessions* (${sessions.length})\n\n${sessionList}\n\n_Reply with #sessionId to send a message to a specific session_`,
|
||||
`*Active Claude Sessions* (${sessions.length})\n\n${sessionList}\n\n_To send message to specific project: ProjectName: your message_`,
|
||||
{ parse_mode: 'Markdown' }
|
||||
);
|
||||
});
|
||||
|
||||
bot.command('queue', async (ctx) => {
|
||||
try {
|
||||
const summary = await getQueueSummary();
|
||||
|
||||
if (summary.length === 0) {
|
||||
await ctx.reply('📭 No queued messages');
|
||||
return;
|
||||
}
|
||||
|
||||
const queueList = summary.map((s, i) => {
|
||||
return `${i + 1}. *${s.projectName}*\n 📥 ${s.pending} pending (${s.total} total)`;
|
||||
}).join('\n\n');
|
||||
|
||||
await ctx.reply(
|
||||
`*Queued Messages* (${summary.length} projects)\n\n${queueList}`,
|
||||
{ parse_mode: 'Markdown' }
|
||||
);
|
||||
} catch (error: any) {
|
||||
await ctx.reply(`❌ Error: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for any text messages from user
|
||||
bot.on('text', async (ctx) => {
|
||||
const message = ctx.message.text;
|
||||
@@ -167,7 +196,45 @@ bot.on('text', async (ctx) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to message queue for processing
|
||||
// Check if message is targeted to a specific project: "ProjectName: message"
|
||||
const projectMatch = message.match(/^([a-zA-Z0-9-_]+):\s*(.+)/);
|
||||
if (projectMatch) {
|
||||
const [, targetProject, actualMessage] = projectMatch;
|
||||
|
||||
// Check if project has an active session
|
||||
const activeSession = Array.from(activeSessions.values())
|
||||
.find(s => s.projectName.toLowerCase() === targetProject.toLowerCase());
|
||||
|
||||
if (activeSession) {
|
||||
// Add to message queue with session ID
|
||||
messageQueue.push({
|
||||
from,
|
||||
message: actualMessage,
|
||||
timestamp: new Date(),
|
||||
read: false,
|
||||
sessionId: activeSession.id
|
||||
});
|
||||
await ctx.reply(`💬 Message sent to active session: *${activeSession.projectName}*`, { parse_mode: 'Markdown' });
|
||||
} else {
|
||||
// Queue for when project becomes active
|
||||
try {
|
||||
await enqueueTask({
|
||||
projectName: targetProject,
|
||||
projectPath: '/unknown',
|
||||
message: actualMessage,
|
||||
from,
|
||||
priority: 'normal',
|
||||
timestamp: new Date()
|
||||
});
|
||||
await ctx.reply(`📥 Message queued for *${targetProject}* (offline)\n\nIt will be delivered when Claude starts in that project.`, { parse_mode: 'Markdown' });
|
||||
} catch (error: any) {
|
||||
await ctx.reply(`❌ Failed to queue message: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No project specified - add to general message queue
|
||||
messageQueue.push({
|
||||
from,
|
||||
message,
|
||||
@@ -245,6 +312,66 @@ app.get('/sessions', (req, res) => {
|
||||
res.json({ sessions, count: sessions.length });
|
||||
});
|
||||
|
||||
// Queue management endpoints
|
||||
app.post('/queue/add', async (req, res) => {
|
||||
const { projectName, projectPath, message, from, priority = 'normal' } = req.body;
|
||||
|
||||
if (!projectName || !message || !from) {
|
||||
return res.status(400).json({ error: 'projectName, message, and from are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await enqueueTask({
|
||||
projectName,
|
||||
projectPath: projectPath || '/unknown',
|
||||
message,
|
||||
from,
|
||||
priority,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
res.json({ success: true, taskId: task.id, task });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/queue/:projectName', async (req, res) => {
|
||||
const { projectName } = req.params;
|
||||
|
||||
try {
|
||||
const tasks = await getPendingTasks(projectName);
|
||||
res.json({ projectName, tasks, count: tasks.length });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/queue/:projectName/mark-delivered', async (req, res) => {
|
||||
const { projectName } = req.params;
|
||||
const { taskId } = req.body;
|
||||
|
||||
if (!taskId) {
|
||||
return res.status(400).json({ error: 'taskId is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await markTaskDelivered(projectName, taskId);
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/queue/summary', async (req, res) => {
|
||||
try {
|
||||
const summary = await getQueueSummary();
|
||||
res.json({ summary, totalProjects: summary.length });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// HTTP endpoint for sending notifications
|
||||
app.post('/notify', async (req, res) => {
|
||||
if (!ENABLED) {
|
||||
|
||||
Reference in New Issue
Block a user