import React, { useEffect, useMemo, useState } from "react"; import { createRoot } from "react-dom/client"; import { Activity, AppWindow, ArrowUpRight, Bot, Brain, CalendarDays, CheckCircle2, ChevronRight, CircleDot, Clock3, Command, Cpu, Database, ExternalLink, FileText, Gauge, Globe2, Goal, HardDrive, History, KeyRound, Layers3, LayoutDashboard, Library, MessageSquareText, Play, Plus, RefreshCw, Rocket, Search, Send, ShieldCheck, Sparkles, TerminalSquare, TimerReset, WandSparkles, Workflow } from "lucide-react"; import "./styles.css"; const seedAgents = [ { id: "hermes", name: "Hermes", accent: "#42dcbf", icon: Sparkles, role: "Research, orchestration, skills, crons", description: "Memory-backed research, scheduled workflows, keyword strategy, and SEO skill execution." }, { id: "codex", name: "Codex", accent: "#f66aa5", icon: TerminalSquare, role: "Build partner and local workspace", description: "Implements the operating system, updates files, verifies the app, and keeps the workspace tidy." }, { id: "claude", name: "Claude", accent: "#f5a866", icon: Brain, role: "Planning, writing, review", description: "Long-form reasoning, article review, prompts, strategy, and beginner-friendly explanations." } ]; const memoryItems = [ "Default human-assisted web work to ChatGPT Atlas.", "Use the Sodexo form design standard for branded documents and sheets.", "Keep Agent OS local-first and write durable memory to the Obsidian vault.", "Route research through Hermes, builds through Codex, and planning through Claude." ]; const defaultWorkspace = { keywords: [], deployments: [], generated: [], history: [], caseStudies: [], sites: [], calendar: [], journal: [], skill: { title: "James SEO Content Skill", body: "Given a keyword, case study, and target site, create a practical SEO article with a clear title, meta description, outline, internal links, FAQ, original James-specific angle, and deploy checklist." } }; const navItems = [ { id: "mission", label: "Mission", icon: LayoutDashboard, title: "Mission Control", subtitle: "Live agent status, local health, queue pressure, and current operating signals." }, { id: "kanban", label: "Kanban", icon: Layers3, title: "Hermes Kanban", subtitle: "Shared task board for Hal, Oracle, and future agent workers." }, { id: "seo", label: "SEO", icon: Search, title: "SEO Command", subtitle: "Keyword queue, topic clusters, quick publish, and weekly targeting." }, { id: "generator", label: "Generator", icon: WandSparkles, title: "Content Generator", subtitle: "Turn keywords, case studies, and memory into article drafts and deploy checklists." }, { id: "deploys", label: "Deploys", icon: Rocket, title: "Deploys", subtitle: "Recently deployed URLs, indexing state, multi-site toggles, and alerts." }, { id: "memory", label: "Memory", icon: Database, title: "Memory", subtitle: "Obsidian journal, shared rules, skill editor, and durable local context." }, { id: "cases", label: "Cases", icon: Library, title: "Case Studies", subtitle: "Store results and proof points to make articles feel specific to your work." }, { id: "prompts", label: "Prompts", icon: KeyRound, title: "Prompt Library", subtitle: "The prompt pack adapted for your Hermes, Codex, and Claude stack." }, { id: "calendar", label: "Calendar", icon: CalendarDays, title: "Calendar", subtitle: "Weekly publishing rhythm and ownership across the agent team." }, { id: "history", label: "History", icon: History, title: "History", subtitle: "Journal entries, generated drafts, deploy events, and agent handoffs." } ]; function resolveViewFromPath(pathname = "/") { const normalized = typeof pathname === "string" ? pathname.replace(/^\/+|\/+$/g, "").toLowerCase() : ""; if (!normalized) return "mission"; return navItems.find((item) => item.id === normalized)?.id || "mission"; } const promptGroups = [ { title: "Build Agent OS", range: "1-10", owner: "Codex", prompts: [ "Build a local Agent OS dashboard for Claude, Hermes, and Codex.", "Open a command palette to switch views, launch apps, and jump to agents.", "Show recently deployed URLs with keyword, site, status, and timestamp.", "Maintain a keyword queue that can be generated, drafted, and published.", "Save daily notes into the Obsidian Agent OS journal.", "Monitor local health for Hermes, Codex, and Claude.", "Edit the Hermes SEO content skill from the dashboard.", "Store case studies for article proof points.", "Choose one, three, or all enabled sites before publishing.", "Show weekly keyword and publishing analytics." ] }, { title: "Keyword Research", range: "11-20", owner: "Hermes", prompts: [ "Generate trending AI-agent SEO keywords for this week.", "Find low-competition high-intent keywords from James's active tools.", "Prioritize pasted Search Console terms by impressions and position.", "Create Hermes Agent problem-solution keyword ideas.", "Build a topical cluster around Agent OS and AI SEO workflows." ] }, { title: "Content Generation", range: "21-35", owner: "Claude", prompts: [ "Create a practical SEO article using a keyword and case study.", "Rewrite drafts for beginners without losing authority.", "Add James-specific proof, callouts, FAQ, and CTAs.", "Generate outlines, meta descriptions, H2s, conclusions, and comparisons." ] }, { title: "Deployment", range: "36-40", owner: "Hermes", prompts: [ "Track pending, deployed, indexed, and failed publish states.", "Prepare Netlify and indexer handoff checklists without storing credentials.", "Publish to selected enabled sites and log alerts when a step fails." ] }, { title: "Memory and Obsidian", range: "41-50", owner: "Hermes", prompts: [ "Extract reusable facts from daily notes into memory.", "Create bio snippets, case-study templates, and topic authority maps.", "Save summaries into Obsidian so future content can reuse them." ] }, { title: "Agent Setup", range: "51-60", owner: "Hermes", prompts: [ "Check Hermes setup, skills, models, workflows, and scheduled jobs.", "Use the SEO skill to create articles and repurpose YouTube transcripts.", "Confirm Obsidian access and local agent memory rules." ] }, { title: "SEO Strategy", range: "61-75", owner: "Claude", prompts: [ "Build 90-day SEO strategy, topical authority maps, and internal linking.", "Improve originality with anecdotes, proof, schema, and external trust links.", "Review drafts for on-page SEO and content quality." ] }, { title: "Publishing Automation", range: "76-80", owner: "Hermes", prompts: [ "Monitor publishing health and deployment workflows inside Mission Control.", "List tools, diagnose slowdowns, and coordinate local execution.", "Prepare Obsidian read-access workflows without exposing secrets." ] }, { title: "Advanced Growth", range: "81-105", owner: "Codex", prompts: [ "Add notifications, analytics, content calendar, competitor research, and repurposing.", "Bulk import keywords and queue them for publishing.", "Track backlinks, quick publish, monthly reports, ROI, and 6-month projections." ] } ]; const ideaSeeds = [ "Hermes Agent OS tutorial", "Claude desktop SEO writing workflow", "AI agent content calendar", "local AI agent dashboard", "AI SEO operating system", "Obsidian memory for SEO content", "AI agent keyword research", "Netlify SEO publishing workflow" ]; function makeId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 7)}`; } function slugify(value) { return String(value || "article") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") .slice(0, 80); } function formatTime(value) { if (!value) return "not checked"; return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit" }).format(new Date(value)); } function formatDate(value) { if (!value) return "unknown"; return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(value)); } function statusLabel(agent) { if (agent?.installed === undefined || agent?.installed === null) return "checking"; if (!agent?.installed) return "missing"; if (agent.health === "live" || agent.running) return "live"; if (agent.health === "ready") return "ready"; return "offline"; } function statusTone(label) { if (label === "live" || label === "indexed" || label === "done") return "good"; if (label === "ready" || label === "checking" || label === "deployed" || label === "drafting") return "warn"; return "bad"; } function gatewaySignal(agent) { if (agent?.installed === undefined || agent?.installed === null) return "checking"; if (!agent?.installed) return "missing"; return agent?.running ? "up" : "down"; } function gatewayTone(agent) { const state = gatewaySignal(agent); if (state === "up") return "blue"; if (state === "down") return "amber"; return "pink"; } function gatewayDetail(agent) { const state = gatewaySignal(agent); if (state === "up") return "gateway process running"; if (state === "down") return "installed, not running"; return "status unknown"; } function isUnassignedAssignee(value) { const normalized = String(value || "").trim().toLowerCase(); return !normalized || normalized === "unassigned" || normalized === "none" || normalized === "default"; } function displayAssignee(value) { if (isUnassignedAssignee(value)) return "Unassigned"; const normalized = String(value).trim(); const normalizedLower = normalized.toLowerCase(); if (normalizedLower === "hal") return "Hal"; if (normalizedLower === "lcar") return "LCAR"; if (normalizedLower === "trinity") return "Trinity"; return normalized .split(/[\s_-]+/) .map((part) => { const lower = part.toLowerCase(); return lower.charAt(0).toUpperCase() + lower.slice(1); }) .join(" "); } function createDraft({ keyword, caseStudy, site, angle }) { const cleanKeyword = keyword || "Hermes Agent OS"; const title = `${cleanKeyword}: A Practical Workflow for James's Agent OS`; const meta = `A local-first guide to using ${cleanKeyword} with Hermes, Claude, Codex, memory, and SEO publishing.`; return { id: makeId("draft"), title, keyword: cleanKeyword, site, status: "draft", timestamp: new Date().toISOString(), body: [ `Title: ${title}`, "", `Meta: ${meta}`, "", "Angle:", angle || "Show how James can turn one keyword into a researched, memory-aware, publish-ready article from the Agent OS dashboard.", "", "Outline:", "1. What the workflow does", "2. Which agent owns each step", "3. How the keyword moves from queue to draft", "4. Where case studies and Obsidian memory improve originality", "5. Deploy and indexing checklist", "", "James-specific proof point:", caseStudy?.result || "The local dashboard already monitors Hermes, Codex, and Claude in one place.", "", "FAQ:", `Q: Can this be used for ${cleanKeyword}?`, "A: Yes. Add the keyword to the queue, generate a draft, attach a case study, then publish to the selected site.", "", "CTA:", "Open the Agent OS dashboard, pick the next keyword, and run the workflow from queue to deploy." ].join("\n") }; } function App() { const [status, setStatus] = useState(null); const [workspace, setWorkspace] = useState(defaultWorkspace); const [kanbanBoard, setKanbanBoard] = useState({ tasks: [], profiles: ["hal", "oracle"], statuses: ["ready", "running", "blocked", "done"] }); const [selectedId, setSelectedId] = useState("hermes"); const [activeView, setActiveView] = useState(() => resolveViewFromPath(window.location.pathname)); function goToView(viewId, syncRoute = true) { const nextView = navItems.find((item) => item.id === viewId)?.id || "mission"; if (syncRoute) { const path = nextView === "mission" ? "/" : `/${nextView}`; if (window.location.pathname !== path) window.history.pushState({}, "", path); } setActiveView(nextView); } const [toast, setToast] = useState(""); const [isRefreshing, setRefreshing] = useState(false); const [paletteOpen, setPaletteOpen] = useState(false); const [paletteQuery, setPaletteQuery] = useState(""); const [message, setMessage] = useState(""); const [note, setNote] = useState(""); const [keywordForm, setKeywordForm] = useState({ keyword: "", intent: "tutorial", priority: "high", site: "main" }); const [generator, setGenerator] = useState({ keyword: "", caseId: "", site: "main", angle: "" }); const [caseForm, setCaseForm] = useState({ title: "", topic: "", result: "", proof: "" }); const [skillDraft, setSkillDraft] = useState(defaultWorkspace.skill.body); const [kanbanForm, setKanbanForm] = useState({ title: "", body: "", assignee: "unassigned", priority: 0 }); const [kanbanFilter, setKanbanFilter] = useState({ assignee: "all", status: "active" }); const [kanbanComments, setKanbanComments] = useState({}); const mergedAgents = useMemo(() => seedAgents.map((agent) => ({ ...agent, ...(status?.agents?.find((item) => item.id === agent.id) || {}) })), [status]); const selected = mergedAgents.find((agent) => agent.id === selectedId) || mergedAgents[0]; const liveCount = mergedAgents.filter((agent) => statusLabel(agent) === "live").length; const viewMeta = navItems.find((item) => item.id === activeView) || navItems[0]; const enabledSites = workspace.sites?.filter((site) => site.enabled) || []; async function refreshStatus() { setRefreshing(true); try { const response = await fetch("/api/status"); setStatus(await response.json()); } catch { setToast("The local helper server is not responding yet."); } finally { setRefreshing(false); } } async function loadWorkspace() { try { const response = await fetch("/api/workspace"); const data = await response.json(); setWorkspace({ ...defaultWorkspace, ...data }); setSkillDraft(data.skill?.body || defaultWorkspace.skill.body); } catch { setToast("Could not load Agent OS workspace data."); } } async function loadKanban() { try { const response = await fetch("/api/kanban"); const data = await response.json(); if (data.ok === false) { setToast(data.message || "Could not load Hermes Kanban."); return; } setKanbanBoard(data); } catch { setToast("Could not reach the Hermes Kanban API."); } } async function saveWorkspace(nextWorkspace, messageText = "Saved.") { setWorkspace(nextWorkspace); try { const response = await fetch("/api/workspace", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(nextWorkspace) }); setWorkspace(await response.json()); setToast(messageText); } catch { setToast("Could not save workspace data."); } } function withHistory(nextWorkspace, title, detail, type = "event") { return { ...nextWorkspace, history: [{ id: makeId("hist"), type, title, detail, timestamp: new Date().toISOString() }, ...(nextWorkspace.history || [])].slice(0, 80) }; } useEffect(() => { refreshStatus(); loadWorkspace(); loadKanban(); const timer = window.setInterval(refreshStatus, 15000); const kanbanTimer = window.setInterval(loadKanban, 10000); return () => { window.clearInterval(timer); window.clearInterval(kanbanTimer); }; }, []); useEffect(() => { function onKeyDown(event) { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); setPaletteOpen((value) => !value); } if (event.key === "Escape") setPaletteOpen(false); } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); useEffect(() => { const syncFromPath = () => { goToView(resolveViewFromPath(window.location.pathname), false); }; window.addEventListener("popstate", syncFromPath); return () => window.removeEventListener("popstate", syncFromPath); }, []); async function launch(target, label) { try { const response = await fetch("/api/launch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ target }) }); const data = await response.json(); setToast(data.ok ? `${label} requested.` : data.message || "Launch failed."); } catch { setToast("The local helper server could not launch that target."); } } function selectAgent(id) { setSelectedId(id); goToView("mission"); } async function createKanbanTask(event) { event.preventDefault(); const title = kanbanForm.title.trim(); if (!title) return; try { const response = await fetch("/api/kanban/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(kanbanForm) }); const data = await response.json(); if (!data.ok) throw new Error(data.message || data.stderr || "Task creation failed."); setKanbanBoard(data.board); setKanbanForm({ title: "", body: "", assignee: kanbanForm.assignee, priority: 0 }); setToast("Hermes Kanban task created."); } catch (error) { setToast(error.message); } } async function updateKanbanTask(taskId, action, payload = {}) { const endpoints = { assign: `/api/kanban/tasks/${taskId}/assign`, status: `/api/kanban/tasks/${taskId}/status`, comment: `/api/kanban/tasks/${taskId}/comment` }; try { const response = await fetch(endpoints[action], { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); const data = await response.json(); if (!data.ok) throw new Error(data.message || data.stderr || "Kanban update failed."); setKanbanBoard(data.board); if (action === "comment") setKanbanComments((items) => ({ ...items, [taskId]: "" })); setToast("Kanban updated."); } catch (error) { setToast(error.message); } } function addKeyword(event) { event.preventDefault(); const keyword = keywordForm.keyword.trim(); if (!keyword) return; const item = { id: makeId("kw"), ...keywordForm, keyword, status: "queued", createdAt: new Date().toISOString() }; const next = withHistory({ ...workspace, keywords: [item, ...(workspace.keywords || [])] }, "Keyword queued", keyword, "keyword"); saveWorkspace(next, "Keyword added to the queue."); setKeywordForm({ ...keywordForm, keyword: "" }); } function generateKeywordIdeas() { const existing = new Set((workspace.keywords || []).map((item) => item.keyword.toLowerCase())); const ideas = ideaSeeds .filter((keyword) => !existing.has(keyword.toLowerCase())) .slice(0, 5) .map((keyword, index) => ({ id: makeId("kw"), keyword, intent: index % 2 ? "comparison" : "tutorial", priority: index < 2 ? "high" : "medium", status: "queued", site: enabledSites[index % Math.max(enabledSites.length, 1)]?.id || "main", createdAt: new Date().toISOString() })); const next = withHistory({ ...workspace, keywords: [...ideas, ...(workspace.keywords || [])] }, "Keyword ideas generated", `${ideas.length} new AI-agent SEO keywords added.`, "keyword"); saveWorkspace(next, "Keyword ideas added."); } function generateDraft(sourceKeyword) { const keyword = sourceKeyword?.keyword || generator.keyword.trim(); if (!keyword) { setToast("Add a keyword before generating a draft."); return; } const caseStudy = (workspace.caseStudies || []).find((item) => item.id === generator.caseId) || workspace.caseStudies?.[0]; const draft = createDraft({ keyword, caseStudy, site: sourceKeyword?.site || generator.site, angle: generator.angle }); const keywords = sourceKeyword ? (workspace.keywords || []).map((item) => (item.id === sourceKeyword.id ? { ...item, status: "drafting" } : item)) : workspace.keywords; const next = withHistory({ ...workspace, keywords, generated: [draft, ...(workspace.generated || [])] }, "Draft generated", draft.title, "draft"); saveWorkspace(next, "Draft generated."); setGenerator({ ...generator, keyword }); goToView("generator"); } function quickPublish(keywordItem) { const item = keywordItem || (workspace.keywords || []).find((keyword) => keyword.status !== "published"); if (!item) { setToast("No queued keyword is ready to publish."); return; } const site = (workspace.sites || []).find((entry) => entry.id === item.site) || workspace.sites?.[0] || { id: "main", name: "Main Site" }; const deployment = { id: makeId("dep"), title: `${item.keyword}: practical guide`, keyword: item.keyword, url: `https://example.com/${slugify(item.keyword)}`, site: site.id, status: "deployed", timestamp: new Date().toISOString() }; const keywords = (workspace.keywords || []).map((keyword) => (keyword.id === item.id ? { ...keyword, status: "published" } : keyword)); const next = withHistory({ ...workspace, keywords, deployments: [deployment, ...(workspace.deployments || [])] }, "URL deployed", deployment.url, "deploy"); saveWorkspace(next, "Deployment logged."); goToView("deploys"); } function cycleDeployment(id) { const order = { pending: "deployed", deployed: "indexed", indexed: "failed", failed: "pending" }; const deployments = (workspace.deployments || []).map((item) => (item.id === id ? { ...item, status: order[item.status] || "pending" } : item)); saveWorkspace(withHistory({ ...workspace, deployments }, "Deploy status updated", "Status moved to next stage.", "deploy"), "Deploy status updated."); } function toggleSite(id) { const sites = (workspace.sites || []).map((site) => (site.id === id ? { ...site, enabled: !site.enabled } : site)); saveWorkspace(withHistory({ ...workspace, sites }, "Site toggle updated", id, "site"), "Site selection updated."); } async function saveJournal() { const body = note.trim(); if (!body) return; try { const response = await fetch("/api/journal", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "Agent OS Daily Note", body }) }); const data = await response.json(); await loadWorkspace(); setNote(""); setToast(data.filePath ? `Journal saved to Obsidian.` : "Journal saved."); } catch { setToast("Could not save the journal note."); } } function saveSkill() { const next = withHistory({ ...workspace, skill: { title: workspace.skill?.title || "James SEO Content Skill", body: skillDraft } }, "SEO skill updated", "Hermes content instructions were updated.", "skill"); saveWorkspace(next, "SEO skill saved."); } function addCaseStudy(event) { event.preventDefault(); if (!caseForm.title.trim() || !caseForm.result.trim()) return; const item = { id: makeId("case"), ...caseForm, updatedAt: new Date().toISOString() }; const next = withHistory({ ...workspace, caseStudies: [item, ...(workspace.caseStudies || [])] }, "Case study added", item.title, "case"); saveWorkspace(next, "Case study added."); setCaseForm({ title: "", topic: "", result: "", proof: "" }); } const commands = [ ...navItems.map((item) => ({ label: `Open ${item.title}`, hint: item.subtitle, run: () => goToView(item.id), icon: item.icon })), ...mergedAgents.map((agent) => ({ label: `Control ${agent.name}`, hint: agent.role, run: () => selectAgent(agent.id), icon: agent.icon })), { label: "Generate keyword ideas", hint: "Add AI-agent SEO ideas to the queue", run: generateKeywordIdeas, icon: Sparkles }, { label: "Quick publish next keyword", hint: "Move the next queued keyword to deploys", run: () => quickPublish(), icon: Rocket }, { label: "Refresh agent health", hint: "Read local Hermes, Codex, and Claude status", run: refreshStatus, icon: RefreshCw } ].filter((command) => command.label.toLowerCase().includes(paletteQuery.toLowerCase())); return (
Local · {status?.machine?.platform || "macOS"}

{viewMeta.title}

{viewMeta.subtitle}

{activeView === "mission" && ( )} {activeView === "kanban" && ( )} {activeView === "seo" && ( )} {activeView === "generator" && ( )} {activeView === "deploys" && } {activeView === "memory" && ( )} {activeView === "cases" && } {activeView === "prompts" && } {activeView === "calendar" && } {activeView === "history" && }
{paletteOpen && (
setPaletteOpen(false)}>
event.stopPropagation()}>
setPaletteQuery(event.target.value)} placeholder="Search commands, agents, and views" />
{commands.map((command) => { const Icon = command.icon; return ( ); })}
)} {toast ? ( ) : null}
); } function MissionView({ status, agents, selected, selectedId, selectAgent, launch, workspace, quickPublish }) { const queued = workspace.keywords?.filter((item) => item.status === "queued").length || 0; const deployed = workspace.deployments?.length || 0; const indexed = workspace.deployments?.filter((item) => item.status === "indexed").length || 0; const agentsById = Object.fromEntries(agents.map((agent) => [agent.id, agent])); const gatewayCards = [ { id: "hermes", label: "Hermes Gateway", icon: ShieldCheck }, { id: "codex", label: "Codex Gateway", icon: Cpu }, { id: "claude", label: "Claude Gateway", icon: MessageSquareText } ]; return ( <>
{gatewayCards.map(({ id, label, icon: GatewayIcon }) => { const agent = agentsById[id]; const state = gatewaySignal(agent); return ( ); })}
{agents.map((agent) => ( selectAgent(agent.id)} onLaunch={launch} /> ))}
); } function KanbanView({ board, form, setForm, filter, setFilter, comments, setComments, createTask, updateTask, refresh }) { const statusLabels = { ready: "Ready", running: "In Progress", blocked: "Blocked", done: "Completed", scheduled: "Scheduled", review: "Review", todo: "Todo", triage: "Triage" }; const activeStatuses = ["ready", "todo", "running", "blocked", "review", "scheduled"]; const profiles = board.profiles?.length ? board.profiles : ["hal"]; const visibleTasks = (board.tasks || []).filter((task) => { const assigneeMatch = filter.assignee === "all" || (filter.assignee === "unassigned" ? isUnassignedAssignee(task.assignee) : task.assignee === filter.assignee); const statusMatch = filter.status === "all" || (filter.status === "active" ? activeStatuses.includes(task.status) : task.status === filter.status); return assigneeMatch && statusMatch; }); const grouped = activeStatuses.concat(["done"]).reduce((acc, status) => { acc[status] = visibleTasks.filter((task) => task.status === status); return acc; }, {}); return (

Create Task