diff --git a/server.js b/server.js index ebdaadf..0d4e0de 100644 --- a/server.js +++ b/server.js @@ -229,152 +229,15 @@ app.patch('/api/tasks/:id', (req, res) => { } catch (wikiErr) { console.error('wiki_creation_error', wikiErr); } - -// Agents endpoint -app.get('/api/agents', (req, res) => { - try { - const agents = []; - - if (fs.existsSync(AGENTS_DIR)) { - const agentDirs = fs.readdirSync(AGENTS_DIR).filter(d => { - return fs.statSync(path.join(AGENTS_DIR, d)).isDirectory(); - }); - - agentDirs.forEach(agentName => { - const agentPath = path.join(AGENTS_DIR, agentName); - const workspacePath = path.join(agentPath, 'workspace'); - - const agent = { - name: agentName, - status: 'active', - currentTask: null, - tools: [], - files: [], - permissions: [] - }; - - // Read workspace files - if (fs.existsSync(workspacePath)) { - const files = fs.readdirSync(workspacePath); - agent.files = files.filter(f => f.endsWith('.md')); - - // Read MEMORY.md for tools - const memoryPath = path.join(workspacePath, 'MEMORY.md'); - if (fs.existsSync(memoryPath)) { - const memory = fs.readFileSync(memoryPath, 'utf8'); - // Extract tools from memory - const toolMatches = memory.match(/##\s+Tools([\s\S]*?)(?=##|$)/i); - if (toolMatches) { - agent.tools = toolMatches[1].split('\n') - .filter(line => line.trim().startsWith('-')) - .map(line => line.replace(/^-\s*/, '').trim()); - } - } - - // Read HEARTBEAT.md for current task - const heartbeatPath = path.join(workspacePath, 'HEARTBEAT.md'); - if (fs.existsSync(heartbeatPath)) { - const heartbeat = fs.readFileSync(heartbeatPath, 'utf8'); - const taskMatch = heartbeat.match(/Current Task:\s*(.+)/i); - if (taskMatch) { - agent.currentTask = taskMatch[1].trim(); - } - } - } - - agents.push(agent); - }); - } - - res.json(agents); - } catch (err) { - console.error('Error reading agents:', err); - res.status(500).json({ error: 'failed_to_fetch_agents' }); - } -}); - -// Usage endpoint -app.get('/api/usage', (req, res) => { - try { - const usage = { - providers: [], - lastUpdated: new Date().toISOString() - }; - - // Read OpenClaw config - if (fs.existsSync(OPENCLAW_CONFIG)) { - const config = JSON.parse(fs.readFileSync(OPENCLAW_CONFIG, 'utf8')); - - // Extract provider information - if (config.models) { - const providerMap = {}; - - Object.entries(config.models).forEach(([modelName, modelConfig]) => { - const provider = modelConfig.provider || 'unknown'; - - if (!providerMap[provider]) { - providerMap[provider] = { - name: provider, - models: [], - quota: { - requests: 0, - tokens: 0, - limit: 'unlimited' - } - }; - } - - providerMap[provider].models.push({ - name: modelName, - type: modelConfig.type || 'chat', - contextWindow: modelConfig.context_window || 'unknown' - }); - }); - - usage.providers = Object.values(providerMap); - } - } - - res.json(usage); - } catch (err) { - console.error('Error reading usage:', err); - res.status(500).json({ error: 'failed_to_fetch_usage' }); - } -}); - -// Heartbeat endpoint for agents -app.get('/api/heartbeat/:agent', (req, res) => { - const agent = req.params.agent; - - db.all( - 'SELECT * FROM tasks WHERE assignee = ? AND status IN (?, ?, ?) ORDER BY priority DESC, created_at ASC', - [agent, 'Todo', 'In Progress', 'Review'], - (err, rows) => { - if (err) { - return res.status(500).json({ error: 'failed_to_fetch_tasks' }); - } - - const tasks = rows.map(normalizeTask); - res.json({ - agent, - pending_tasks: tasks.length, - tasks - }); - } - ); -}); } broadcast('task_updated', task); return res.json(task); }); - } - ); + }); + }); }); -}); - -// Agents endpoint app.get('/api/agents', (req, res) => { try { const agents = []; diff --git a/server.js.bak b/server.js.bak new file mode 100644 index 0000000..ebdaadf --- /dev/null +++ b/server.js.bak @@ -0,0 +1,511 @@ +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const http = require('http'); +const sqlite3 = require('sqlite3').verbose(); +const { WebSocketServer } = require('ws'); + +const PORT = process.env.PORT || 8395; +const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'tasks.db'); +const WIKI_DIR = process.env.WIKI_DIR || '/home/bear/.openclaw/workspace/wiki'; + +const AGENTS_DIR = process.env.AGENTS_DIR || '/home/bear/.openclaw/agents'; +const OPENCLAW_CONFIG = process.env.OPENCLAW_CONFIG || '/home/bear/.openclaw/openclaw.json'; +const VALID_STATUSES = ['Backlog', 'Todo', 'In Progress', 'Review', 'Done']; +const VALID_PRIORITIES = ['Low', 'Medium', 'High', 'Critical']; + +fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }); +fs.mkdirSync(WIKI_DIR, { recursive: true }); + +const db = new sqlite3.Database(DB_PATH); + +db.serialize(() => { + db.run(` + CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT DEFAULT '', + assignee TEXT DEFAULT '', + priority TEXT NOT NULL DEFAULT 'Medium', + status TEXT NOT NULL DEFAULT 'Backlog', + tags TEXT NOT NULL DEFAULT '[]', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + completed_at TEXT + ) + `); +}); + +const app = express(); +const server = http.createServer(app); +const wss = new WebSocketServer({ server }); + +app.use(express.json()); +app.use(express.static(path.join(__dirname, 'public'))); + +function normalizeTask(row) { + return { + ...row, + tags: (() => { + try { + return JSON.parse(row.tags || '[]'); + } catch { + return []; + } + })(), + }; +} + +function writeWiki(task) { + const safeTitle = task.title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 80) || `task-${task.id}`; + + const fileName = `${new Date().toISOString().slice(0, 10)}-task-${task.id}-${safeTitle}.md`; + const filePath = path.join(WIKI_DIR, fileName); + + const md = `# ${task.title}\n\n` + + `- Task ID: ${task.id}\n` + + `- Assignee: ${task.assignee || 'Unassigned'}\n` + + `- Priority: ${task.priority}\n` + + `- Status: ${task.status}\n` + + `- Tags: ${task.tags.length ? task.tags.join(', ') : 'None'}\n` + + `- Created: ${task.created_at}\n` + + `- Completed: ${task.completed_at || new Date().toISOString()}\n\n` + + `## Description\n\n${task.description || 'No description provided.'}\n`; + + fs.writeFileSync(filePath, md, 'utf8'); +} + +function broadcast(type, payload) { + const data = JSON.stringify({ type, payload }); + for (const client of wss.clients) { + if (client.readyState === 1) { + client.send(data); + } + } +} + +function validatePayload(body, partial = false) { + const errors = []; + + if (!partial || body.title !== undefined) { + if (typeof body.title !== 'string' || body.title.trim().length === 0) { + errors.push('title is required'); + } + } + + if (body.status !== undefined && !VALID_STATUSES.includes(body.status)) { + errors.push(`status must be one of: ${VALID_STATUSES.join(', ')}`); + } + + if (body.priority !== undefined && !VALID_PRIORITIES.includes(body.priority)) { + errors.push(`priority must be one of: ${VALID_PRIORITIES.join(', ')}`); + } + + if (body.tags !== undefined && !Array.isArray(body.tags)) { + errors.push('tags must be an array of strings'); + } + + return errors; +} + +app.get('/api/tasks', (req, res) => { + db.all('SELECT * FROM tasks ORDER BY id DESC', (err, rows) => { + if (err) { + return res.status(500).json({ error: 'failed_to_fetch_tasks' }); + } + + return res.json(rows.map(normalizeTask)); + }); +}); + +app.post('/api/tasks', (req, res) => { + const errors = validatePayload(req.body, false); + if (errors.length) { + return res.status(400).json({ error: 'validation_error', details: errors }); + } + + const title = req.body.title.trim(); + const description = typeof req.body.description === 'string' ? req.body.description : ''; + const assignee = typeof req.body.assignee === 'string' ? req.body.assignee : ''; + const priority = req.body.priority || 'Medium'; + const status = req.body.status || 'Backlog'; + const tags = Array.isArray(req.body.tags) ? req.body.tags.filter((t) => typeof t === 'string') : []; + + db.run( + `INSERT INTO tasks (title, description, assignee, priority, status, tags) + VALUES (?, ?, ?, ?, ?, ?)`, + [title, description, assignee, priority, status, JSON.stringify(tags)], + function onInsert(err) { + if (err) { + return res.status(500).json({ error: 'failed_to_create_task' }); + } + + db.get('SELECT * FROM tasks WHERE id = ?', [this.lastID], (fetchErr, row) => { + if (fetchErr || !row) { + return res.status(500).json({ error: 'failed_to_fetch_created_task' }); + } + + const task = normalizeTask(row); + broadcast('task_created', task); + return res.status(201).json(task); + }); + } + ); +}); + +app.patch('/api/tasks/:id', (req, res) => { + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'invalid_task_id' }); + } + + const errors = validatePayload(req.body, true); + if (errors.length) { + return res.status(400).json({ error: 'validation_error', details: errors }); + } + + db.get('SELECT * FROM tasks WHERE id = ?', [id], (err, existing) => { + if (err) { + return res.status(500).json({ error: 'failed_to_find_task' }); + } + if (!existing) { + return res.status(404).json({ error: 'task_not_found' }); + } + + const existingTask = normalizeTask(existing); + const next = { + title: req.body.title !== undefined ? req.body.title.trim() : existingTask.title, + description: req.body.description !== undefined ? String(req.body.description) : existingTask.description, + assignee: req.body.assignee !== undefined ? String(req.body.assignee) : existingTask.assignee, + priority: req.body.priority !== undefined ? req.body.priority : existingTask.priority, + status: req.body.status !== undefined ? req.body.status : existingTask.status, + tags: req.body.tags !== undefined + ? req.body.tags.filter((t) => typeof t === 'string') + : existingTask.tags, + }; + + const nowDone = next.status === 'Done'; + const wasDone = existingTask.status === 'Done'; + const completedAt = nowDone && !wasDone + ? new Date().toISOString() + : nowDone + ? existing.completed_at + : null; + + db.run( + `UPDATE tasks + SET title = ?, description = ?, assignee = ?, priority = ?, status = ?, tags = ?, + completed_at = ?, updated_at = datetime('now') + WHERE id = ?`, + [ + next.title, + next.description, + next.assignee, + next.priority, + next.status, + JSON.stringify(next.tags), + completedAt, + id, + ], + (updateErr) => { + if (updateErr) { + return res.status(500).json({ error: 'failed_to_update_task' }); + } + + db.get('SELECT * FROM tasks WHERE id = ?', [id], (fetchErr, row) => { + if (fetchErr || !row) { + return res.status(500).json({ error: 'failed_to_fetch_updated_task' }); + } + + const task = normalizeTask(row); + + if (nowDone && !wasDone) { + try { + writeWiki(task); + } catch (wikiErr) { + console.error('wiki_creation_error', wikiErr); + } + +// Agents endpoint +app.get('/api/agents', (req, res) => { + try { + const agents = []; + + if (fs.existsSync(AGENTS_DIR)) { + const agentDirs = fs.readdirSync(AGENTS_DIR).filter(d => { + return fs.statSync(path.join(AGENTS_DIR, d)).isDirectory(); + }); + + agentDirs.forEach(agentName => { + const agentPath = path.join(AGENTS_DIR, agentName); + const workspacePath = path.join(agentPath, 'workspace'); + + const agent = { + name: agentName, + status: 'active', + currentTask: null, + tools: [], + files: [], + permissions: [] + }; + + // Read workspace files + if (fs.existsSync(workspacePath)) { + const files = fs.readdirSync(workspacePath); + agent.files = files.filter(f => f.endsWith('.md')); + + // Read MEMORY.md for tools + const memoryPath = path.join(workspacePath, 'MEMORY.md'); + if (fs.existsSync(memoryPath)) { + const memory = fs.readFileSync(memoryPath, 'utf8'); + // Extract tools from memory + const toolMatches = memory.match(/##\s+Tools([\s\S]*?)(?=##|$)/i); + if (toolMatches) { + agent.tools = toolMatches[1].split('\n') + .filter(line => line.trim().startsWith('-')) + .map(line => line.replace(/^-\s*/, '').trim()); + } + } + + // Read HEARTBEAT.md for current task + const heartbeatPath = path.join(workspacePath, 'HEARTBEAT.md'); + if (fs.existsSync(heartbeatPath)) { + const heartbeat = fs.readFileSync(heartbeatPath, 'utf8'); + const taskMatch = heartbeat.match(/Current Task:\s*(.+)/i); + if (taskMatch) { + agent.currentTask = taskMatch[1].trim(); + } + } + } + + agents.push(agent); + }); + } + + res.json(agents); + } catch (err) { + console.error('Error reading agents:', err); + res.status(500).json({ error: 'failed_to_fetch_agents' }); + } +}); + +// Usage endpoint +app.get('/api/usage', (req, res) => { + try { + const usage = { + providers: [], + lastUpdated: new Date().toISOString() + }; + + // Read OpenClaw config + if (fs.existsSync(OPENCLAW_CONFIG)) { + const config = JSON.parse(fs.readFileSync(OPENCLAW_CONFIG, 'utf8')); + + // Extract provider information + if (config.models) { + const providerMap = {}; + + Object.entries(config.models).forEach(([modelName, modelConfig]) => { + const provider = modelConfig.provider || 'unknown'; + + if (!providerMap[provider]) { + providerMap[provider] = { + name: provider, + models: [], + quota: { + requests: 0, + tokens: 0, + limit: 'unlimited' + } + }; + } + + providerMap[provider].models.push({ + name: modelName, + type: modelConfig.type || 'chat', + contextWindow: modelConfig.context_window || 'unknown' + }); + }); + + usage.providers = Object.values(providerMap); + } + } + + res.json(usage); + } catch (err) { + console.error('Error reading usage:', err); + res.status(500).json({ error: 'failed_to_fetch_usage' }); + } +}); + +// Heartbeat endpoint for agents +app.get('/api/heartbeat/:agent', (req, res) => { + const agent = req.params.agent; + + db.all( + 'SELECT * FROM tasks WHERE assignee = ? AND status IN (?, ?, ?) ORDER BY priority DESC, created_at ASC', + [agent, 'Todo', 'In Progress', 'Review'], + (err, rows) => { + if (err) { + return res.status(500).json({ error: 'failed_to_fetch_tasks' }); + } + + const tasks = rows.map(normalizeTask); + res.json({ + agent, + pending_tasks: tasks.length, + tasks + }); + } + ); +}); + } + + broadcast('task_updated', task); + return res.json(task); + }); + } + ); + }); +}); + + +// Agents endpoint +app.get('/api/agents', (req, res) => { + try { + const agents = []; + + if (fs.existsSync(AGENTS_DIR)) { + const agentDirs = fs.readdirSync(AGENTS_DIR).filter(d => { + return fs.statSync(path.join(AGENTS_DIR, d)).isDirectory(); + }); + + agentDirs.forEach(agentName => { + const agentPath = path.join(AGENTS_DIR, agentName); + const workspacePath = path.join(agentPath, 'workspace'); + + const agent = { + name: agentName, + status: 'active', + currentTask: null, + tools: [], + files: [], + permissions: [] + }; + + if (fs.existsSync(workspacePath)) { + const files = fs.readdirSync(workspacePath); + agent.files = files.filter(f => f.endsWith('.md')); + + const memoryPath = path.join(workspacePath, 'MEMORY.md'); + if (fs.existsSync(memoryPath)) { + const memory = fs.readFileSync(memoryPath, 'utf8'); + const toolMatches = memory.match(/##\\s+Tools([\\s\\S]*?)(?=##|$)/i); + if (toolMatches) { + agent.tools = toolMatches[1].split('\\n') + .filter(line => line.trim().startsWith('-')) + .map(line => line.replace(/^-\\s*/, '').trim()); + } + } + + const heartbeatPath = path.join(workspacePath, 'HEARTBEAT.md'); + if (fs.existsSync(heartbeatPath)) { + const heartbeat = fs.readFileSync(heartbeatPath, 'utf8'); + const taskMatch = heartbeat.match(/Current Task:\\s*(.+)/i); + if (taskMatch) { + agent.currentTask = taskMatch[1].trim(); + } + } + } + + agents.push(agent); + }); + } + + res.json(agents); + } catch (err) { + console.error('Error reading agents:', err); + res.status(500).json({ error: 'failed_to_fetch_agents' }); + } +}); + +// Usage endpoint +app.get('/api/usage', (req, res) => { + try { + const usage = { + providers: [], + lastUpdated: new Date().toISOString() + }; + + if (fs.existsSync(OPENCLAW_CONFIG)) { + const config = JSON.parse(fs.readFileSync(OPENCLAW_CONFIG, 'utf8')); + + if (config.models) { + const providerMap = {}; + + Object.entries(config.models).forEach(([modelName, modelConfig]) => { + const provider = modelConfig.provider || 'unknown'; + + if (!providerMap[provider]) { + providerMap[provider] = { + name: provider, + models: [], + quota: { + requests: 0, + tokens: 0, + limit: 'unlimited' + } + }; + } + + providerMap[provider].models.push({ + name: modelName, + type: modelConfig.type || 'chat', + contextWindow: modelConfig.context_window || 'unknown' + }); + }); + + usage.providers = Object.values(providerMap); + } + } + + res.json(usage); + } catch (err) { + console.error('Error reading usage:', err); + res.status(500).json({ error: 'failed_to_fetch_usage' }); + } +}); + +// Heartbeat endpoint for agents +app.get('/api/heartbeat/:agent', (req, res) => { + const agent = req.params.agent; + + db.all( + 'SELECT * FROM tasks WHERE assignee = ? AND status IN (?, ?, ?) ORDER BY priority DESC, created_at ASC', + [agent, 'Todo', 'In Progress', 'Review'], + (err, rows) => { + if (err) { + return res.status(500).json({ error: 'failed_to_fetch_tasks' }); + } + + const tasks = rows.map(normalizeTask); + res.json({ + agent, + pending_tasks: tasks.length, + tasks + }); + } + ); +}); + +wss.on('connection', (socket) => { + socket.send(JSON.stringify({ type: 'connected', payload: { ok: true } })); +}); + +server.listen(PORT, '0.0.0.0', () => { + console.log(`openclaw-taskboard listening on ${PORT}`); +});