diff --git a/app/api/agent-outputs/route.ts b/app/api/agent-outputs/route.ts new file mode 100644 index 0000000..ccf91ae --- /dev/null +++ b/app/api/agent-outputs/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const STORAGE_DIR = path.join(process.cwd(), "data", "agent-outputs"); + +function ensureDir() { + if (!fs.existsSync(STORAGE_DIR)) { + fs.mkdirSync(STORAGE_DIR, { recursive: true }); + } +} + +function getAgentDir(agent: string) { + const dir = path.join(STORAGE_DIR, agent); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + return dir; +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const agent = searchParams.get("agent"); + const date = searchParams.get("date"); + + if (!agent) { + // Return list of all agent directories + ensureDir(); + const agents = fs.readdirSync(STORAGE_DIR).filter(f => + fs.statSync(path.join(STORAGE_DIR, f)).isDirectory() + ); + return NextResponse.json(agents); + } + + const agentDir = getAgentDir(agent); + + if (date) { + // Return specific file + const file = path.join(agentDir, `${date}.md`); + if (fs.existsSync(file)) { + const content = fs.readFileSync(file, "utf-8"); + return NextResponse.json({ agent, date, content }); + } + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + // Return list of files for agent + const files = fs.readdirSync(agentDir) + .filter(f => f.endsWith('.md')) + .map(f => f.replace('.md', '')) + .sort() + .reverse(); + + return NextResponse.json(files); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { action, agent, date, content } = body; + + if (action === "save") { + const agentDir = getAgentDir(agent); + const file = path.join(agentDir, `${date}.md`); + fs.writeFileSync(file, content); + return NextResponse.json({ success: true, file }); + } + + if (action === "delete") { + const agentDir = getAgentDir(agent); + const file = path.join(agentDir, `${date}.md`); + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + return NextResponse.json({ success: true }); + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); + + } catch (error) { + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/api/changelog/route.ts b/app/api/changelog/route.ts new file mode 100644 index 0000000..21dc992 --- /dev/null +++ b/app/api/changelog/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const STORAGE_DIR = path.join(process.cwd(), "data"); +const CHANGELOG_FILE = path.join(STORAGE_DIR, "changelog.json"); + +function ensureDir() { + if (!fs.existsSync(STORAGE_DIR)) { + fs.mkdirSync(STORAGE_DIR, { recursive: true }); + } +} + +function readJSON(file: string, defaultValue: any = []) { + ensureDir(); + if (!fs.existsSync(file)) { + fs.writeFileSync(file, JSON.stringify(defaultValue)); + return defaultValue; + } + try { + return JSON.parse(fs.readFileSync(file, "utf-8")); + } catch { + return defaultValue; + } +} + +function writeJSON(file: string, data: any) { + ensureDir(); + fs.writeFileSync(file, JSON.stringify(data, null, 2)); +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const agent = searchParams.get("agent"); + const type = searchParams.get("type"); + const limit = parseInt(searchParams.get("limit") || "50"); + + let changelog = readJSON(CHANGELOG_FILE, []); + + if (agent) { + changelog = changelog.filter((c: any) => c.agent === agent); + } + + if (type) { + changelog = changelog.filter((c: any) => c.type === type); + } + + changelog = changelog.slice(0, limit); + + return NextResponse.json(changelog); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { action, entry, entryId } = body; + + const changelog = readJSON(CHANGELOG_FILE, []); + + if (action === "add") { + const newEntry = { + id: `changelog-${Date.now()}`, + date: new Date().toISOString(), + ...entry + }; + changelog.unshift(newEntry); + writeJSON(CHANGELOG_FILE, changelog); + return NextResponse.json({ success: true, changelog }); + } + + if (action === "delete") { + const filtered = changelog.filter((c: any) => c.id !== entryId); + writeJSON(CHANGELOG_FILE, filtered); + return NextResponse.json({ success: true, changelog: filtered }); + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); + + } catch (error) { + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/api/execution-logs/route.ts b/app/api/execution-logs/route.ts new file mode 100644 index 0000000..5c27ae3 --- /dev/null +++ b/app/api/execution-logs/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const STORAGE_DIR = path.join(process.cwd(), "data"); +const LOGS_FILE = path.join(STORAGE_DIR, "execution-logs.json"); +const OUTPUTS_FILE = path.join(STORAGE_DIR, "agent-outputs.json"); + +function ensureDir() { + if (!fs.existsSync(STORAGE_DIR)) { + fs.mkdirSync(STORAGE_DIR, { recursive: true }); + } +} + +function readJSON(file: string, defaultValue: any = []) { + ensureDir(); + if (!fs.existsSync(file)) { + fs.writeFileSync(file, JSON.stringify(defaultValue)); + return defaultValue; + } + try { + return JSON.parse(fs.readFileSync(file, "utf-8")); + } catch { + return defaultValue; + } +} + +function writeJSON(file: string, data: any) { + ensureDir(); + fs.writeFileSync(file, JSON.stringify(data, null, 2)); +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const agent = searchParams.get("agent"); + const templateId = searchParams.get("templateId"); + const limit = parseInt(searchParams.get("limit") || "50"); + + let logs = readJSON(LOGS_FILE, []); + + // Filter by agent + if (agent) { + logs = logs.filter((l: any) => l.agent === agent); + } + + // Filter by template + if (templateId) { + logs = logs.filter((l: any) => l.templateId === templateId); + } + + // Limit + logs = logs.slice(0, limit); + + return NextResponse.json(logs); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { action, logId, output, status, agent } = body; + + const logs = readJSON(LOGS_FILE, []); + + if (action === "update") { + const idx = logs.findIndex((l: any) => l.id === logId); + if (idx >= 0) { + if (status) logs[idx].status = status; + if (output) logs[idx].output = output; + if (status === "completed" || status === "failed") { + logs[idx].completedAt = new Date().toISOString(); + } + writeJSON(LOGS_FILE, logs); + return NextResponse.json({ success: true, log: logs[idx] }); + } + } + + if (action === "clear") { + // Clear old logs (keep last 100) + const filtered = logs.slice(0, 100); + writeJSON(LOGS_FILE, filtered); + return NextResponse.json({ success: true, logs: filtered }); + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); + + } catch (error) { + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/api/recurring/route.ts b/app/api/recurring/route.ts new file mode 100644 index 0000000..e244f32 --- /dev/null +++ b/app/api/recurring/route.ts @@ -0,0 +1,116 @@ +import { NextRequest, NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +const STORAGE_DIR = path.join(process.cwd(), "data"); +const TEMPLATES_FILE = path.join(STORAGE_DIR, "recurring-templates.json"); +const LOGS_FILE = path.join(STORAGE_DIR, "execution-logs.json"); +const CHANGELOG_FILE = path.join(STORAGE_DIR, "changelog.json"); +const OUTPUTS_FILE = path.join(STORAGE_DIR, "agent-outputs.json"); + +function ensureDir() { + if (!fs.existsSync(STORAGE_DIR)) { + fs.mkdirSync(STORAGE_DIR, { recursive: true }); + } +} + +function readJSON(file: string, defaultValue: any = []) { + ensureDir(); + if (!fs.existsSync(file)) { + fs.writeFileSync(file, JSON.stringify(defaultValue)); + return defaultValue; + } + try { + return JSON.parse(fs.readFileSync(file, "utf-8")); + } catch { + return defaultValue; + } +} + +function writeJSON(file: string, data: any) { + ensureDir(); + fs.writeFileSync(file, JSON.stringify(data, null, 2)); +} + +// Templates CRUD +export async function GET() { + const templates = readJSON(TEMPLATES_FILE, []); + return NextResponse.json(templates); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { action, template, templateId, enabled } = body; + + const templates = readJSON(TEMPLATES_FILE, []); + + if (action === "toggle") { + const idx = templates.findIndex((t: any) => t.id === templateId); + if (idx >= 0) { + templates[idx].enabled = enabled; + writeJSON(TEMPLATES_FILE, templates); + return NextResponse.json({ success: true, templates }); + } + } + + if (action === "save") { + const idx = templates.findIndex((t: any) => t.id === template.id); + if (idx >= 0) { + templates[idx] = { ...templates[idx], ...template }; + } else { + templates.push({ ...template, runCount: 0, enabled: false }); + } + writeJSON(TEMPLATES_FILE, templates); + return NextResponse.json({ success: true, templates }); + } + + if (action === "delete") { + const filtered = templates.filter((t: any) => t.id !== templateId); + writeJSON(TEMPLATES_FILE, filtered); + return NextResponse.json({ success: true, templates: filtered }); + } + + if (action === "run-now") { + // Trigger immediate execution + const idx = templates.findIndex((t: any) => t.id === templateId); + if (idx >= 0) { + const template = templates[idx]; + + // Create execution log + const logs = readJSON(LOGS_FILE, []); + const logId = `log-${Date.now()}`; + const newLog = { + id: logId, + templateId: template.id, + agent: template.agent, + taskTitle: template.taskTemplate.title, + startedAt: new Date().toISOString(), + status: "running", + output: "Agent executing..." + }; + logs.unshift(newLog); + writeJSON(LOGS_FILE, logs); + + // Update template + templates[idx].lastRun = new Date().toISOString(); + templates[idx].runCount = (templates[idx].runCount || 0) + 1; + writeJSON(TEMPLATES_FILE, templates); + + // TODO: Actually dispatch to OpenClaw agent here + // For now, simulate completion after delay + + return NextResponse.json({ + success: true, + templates, + executionLog: newLog + }); + } + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); + + } catch (error) { + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/components/mission-control/AutoRunPanel.tsx b/components/mission-control/AutoRunPanel.tsx new file mode 100644 index 0000000..3d52f3b --- /dev/null +++ b/components/mission-control/AutoRunPanel.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { RecurringTemplate, defaultRecurringTemplates } from "@/lib/mission-control/recurring"; + +const AGENT_COLORS: Record = { + thoth: "#8b5cf6", + "thoth-trading": "#f59e0b", + ptah: "#10b981", + seshat: "#ec4899", + anubis: "#6366f1", + sekhmet: "#ef4444" +}; + +const AGENT_NAMES: Record = { + thoth: "Thoth", + "thoth-trading": "Thoth Trading", + ptah: "Ptah", + seshat: "Seshat", + anubis: "Anubis", + sekhmet: "Sekhmet" +}; + +export default function AutoRunPanel() { + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(true); + const [running, setRunning] = useState(null); + + useEffect(() => { + fetchTemplates(); + }, []); + + const fetchTemplates = async () => { + const res = await fetch("/api/recurring"); + const data = await res.json(); + + // Merge with defaults if empty + if (data.length === 0) { + await fetch("/api/recurring", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "save", + template: defaultRecurringTemplates[0] + }) + }); + setTemplates(defaultRecurringTemplates); + } else { + setTemplates(data); + } + setLoading(false); + }; + + const toggleTemplate = async (templateId: string, enabled: boolean) => { + await fetch("/api/recurring", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "toggle", templateId, enabled }) + }); + fetchTemplates(); + }; + + const runNow = async (templateId: string) => { + setRunning(templateId); + await fetch("/api/recurring", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "run-now", templateId }) + }); + setRunning(null); + fetchTemplates(); + }; + + const formatSchedule = (template: RecurringTemplate) => { + const { schedule } = template; + switch (schedule.type) { + case "hourly": return "Hourly"; + case "daily": return `Daily @ ${schedule.time}`; + case "weekly": + const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + return `Weekly: ${days[schedule.dayOfWeek || 0]} @ ${schedule.time}`; + case "monthly": return `Monthly: Day ${schedule.dayOfMonth}`; + default: return schedule.type; + } + }; + + if (loading) { + return
Loading templates...
; + } + + return ( +
+
+

πŸ”„ Auto-Run Templates

+ + {templates.filter(t => t.enabled).length} active + +
+ +
+ {templates.map((template) => ( +
+
+
+
+ + {AGENT_NAMES[template.agent] || template.agent} + + + {formatSchedule(template)} + +
+

{template.name}

+

{template.description}

+ + {template.preInstructions && ( +
+ + View pre-instructions + +

+ {template.preInstructions} +

+
+ )} +
+ +
+ + + +
+
+ +
+ Run count: {template.runCount || 0} + {template.lastRun && ( + Last: {new Date(template.lastRun).toLocaleString()} + )} + {template.nextRun && ( + Next: {new Date(template.nextRun).toLocaleString()} + )} +
+
+ ))} +
+ + {templates.length === 0 && ( +
+ No recurring templates. Create one to automate your agents. +
+ )} +
+ ); +} diff --git a/components/mission-control/BrainownPanel.tsx b/components/mission-control/BrainownPanel.tsx new file mode 100644 index 0000000..89ca201 --- /dev/null +++ b/components/mission-control/BrainownPanel.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface AgentOutput { + agent: string; + date: string; + content: string; +} + +const AGENT_NAMES: Record = { + thoth: "Thoth", + "thoth-trading": "Thoth Trading", + ptah: "Ptah", + seshat: "Seshat", + anubis: "Anubis", + sekhmet: "Sekhmet", + horus: "Horus" +}; + +const AGENT_COLORS: Record = { + thoth: "#8b5cf6", + "thoth-trading": "#f59e0b", + ptah: "#10b981", + seshat: "#ec4899", + anubis: "#6366f1", + sekhmet: "#ef4444", + horus: "#ff7bc0" +}; + +export default function BrainownPanel() { + const [agents, setAgents] = useState([]); + const [selectedAgent, setSelectedAgent] = useState(null); + const [dates, setDates] = useState([]); + const [selectedDate, setSelectedDate] = useState(null); + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + + useEffect(() => { + fetchAgents(); + }, []); + + const fetchAgents = async () => { + const res = await fetch("/api/agent-outputs"); + const data = await res.json(); + setAgents(data); + setLoading(false); + }; + + const fetchDates = async (agent: string) => { + const res = await fetch(`/api/agent-outputs?agent=${agent}`); + const data = await res.json(); + if (Array.isArray(data)) { + setDates(data); + } + }; + + const fetchContent = async (agent: string, date: string) => { + const res = await fetch(`/api/agent-outputs?agent=${agent}&date=${date}`); + const data = await res.json(); + if (data.content) { + setContent(data.content); + } else { + setContent(`# ${AGENT_NAMES[agent] || agent} - ${date}\n\nStart writing your notes here...`); + } + }; + + const handleAgentSelect = (agent: string) => { + setSelectedAgent(agent); + setSelectedDate(null); + setContent(""); + fetchDates(agent); + }; + + const handleDateSelect = (date: string) => { + setSelectedDate(date); + if (selectedAgent) { + fetchContent(selectedAgent, date); + } + }; + + const saveContent = async () => { + if (!selectedAgent || !selectedDate) return; + + await fetch("/api/agent-outputs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "save", + agent: selectedAgent, + date: selectedDate, + content + }) + }); + + setEditing(false); + }; + + const createNewNote = () => { + if (!selectedAgent) return; + const today = new Date().toISOString().split("T")[0]; + setSelectedDate(today); + setContent(`# ${AGENT_NAMES[selectedAgent]} - ${today}\n\n`); + setEditing(true); + }; + + if (loading) { + return
Loading...
; + } + + return ( +
+
+

🧠 Brainown

+ {selectedAgent && ( + + )} +
+ +
+ {/* Agent List */} +
+

Agents

+ {agents.map((agent) => ( + + ))} + + {agents.length === 0 && ( +

No agent outputs yet

+ )} +
+ + {/* Date List */} +
+

Notes

+ {dates.map((date) => ( + + ))} + + {selectedAgent && dates.length === 0 && ( +

No notes yet

+ )} +
+ + {/* Content Editor */} +
+ {selectedDate ? ( +
+
+

+ {AGENT_NAMES[selectedAgent]} - {selectedDate} +

+
+ {editing ? ( + <> + + + + ) : ( + + )} +
+
+ + {editing ? ( +