# Project export: Pokestrator

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: A fully conversational, self-improving agent that brings limitless capabilities directly to your messages app.
- Devpost: https://devpost.com/software/pokestrator
- GitHub: https://github.com/sabdulmajid/Pokestrator
- Video: https://www.youtube.com/embed/yn8wwUMBMzg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Larris Xie (5 commits)

## Devpost submission (written by the team)

### Inspiration

Imagine this: you're out grabbing coffee, far from your laptop, when an urgent Slack ping comes from a C-suite executive: “Send me the key metrics for an impending board meeting”. Panic sets in...or does it? With Pokestrator, you simply one-shot text your request, and boom, those insights land in your phone within seconds. No panic, no delays, just pure efficiency! We built Pokestrator as a fully conversational, self-improving iMessage agent that weaves seamlessly into your favorite apps and tools. It's your on-the-go AI assistant, born from those all-too-real moments of mobile frustration. Pokestrator empowers you to stay productive anywhere at anytime, redefining how we interact with our digital world.

### What it does

Pokestrator is your personal intelligent sidekick that not just responds but also evolves. It can handle anything, from simple queries to complicated tasks, whether it's analyzing a file on your computer from your phone, or messaging between 3rd parties like Stripe (asking about recent transactions), Twilio (sending texts/calls to a group of friends periodically), and Slack (automating alerts with root cause analysis), by autonomously building tools for itself. It was built to handle ambiguity. When faced with a challenge that cannot be handled by default, Pokestrator automatically recognizes the limitations and autonomously creates sub-agents to fill the void. The brain of the decision-making process is the orchestrator agent, first giving a token-based score to existing sub-agents based on the task determined by Poke. It then routes the request to the best existing match OR creates a new spec, all asynchronously in the background, ensuring that the chatting remains uninterrupted. It then spins up the headless sub-agent in the background, completes the task, and uses Poke's webhook to drop the final output straight into your messages app. This means that you get a personalized, evolving set of agents that can adapt to your needs, making your applications work for you like never before!

### How we built it

We used Poke as our iMessage gateway, letting users interact through the messages app everyone already has. Behind the scenes, the Claude Agent SDK powers both the main Pokestrator orchestrator and every sub-agent it spawns, giving the reasoning power needed for complex task decomposition. We used FastMCP to keep communication quick between the interface and the orchestrator, while Render Postgres acts as our system's memory, storing every sub-agent spec so the platform gets smarter with each interaction. We set up automations within Poke that self-detect when it hits a limitation, triggering the autonomous Pokestrator agent-creation loop that makes this whole self-improving system tick. Lastly, to visualize the agent workflow, we created an interactive React app that shows the request flow between Poke, the Pokestrator agent, and the worker sub-agents.

### Accomplishments we're proud of

We walked into this weekend with a wild idea and walked out with a working, end-to-end product that delivers on its promise. In under 36 hours, we built a system that actively expands its own capabilities by creating new sub-agents on the fly. We have ambitious goals for Pokestrator and have a solid roadmap forward for new features!

### What's next

Next up is our Recursive Enhancement agent, which is a background sub-agent that analyzes logs from every sub-agent and automatically suggests fixes and improvements to the existing specs. Long term, we're building Pokestrator into an enterprise orchestrator tool that lets companies spin up reliable, task-specific agents just by describing what they need. As more use cases flow through the system, those agent templates will become incredibly refined and powerful. We also see a natural partnership with Poke itself, embedding our orchestration layer as the core intelligence for their platform. Hit us up if you have any cool suggestions!

## README (from the GitHub repository)

# Pokestrator

Asynchronous FastMCP server for Poke. It exposes exactly one tool: `orchestrate`.
The tool stores subagent definitions in PostgreSQL, selects an execution path,
and sends the final result back to Poke via webhook callback.

## Runtime architecture

- MCP entrypoint: `src/server.py`
- Orchestrator + execution logic: `src/agent.py`
- Postgres persistence: `src/db.py`
- Poke callback sender: `src/poke.py`
- Templates: `src/templates/*.json`

## Flow

1. Poke invokes MCP tool `orchestrate(task_description)`.
2. Server returns immediate ack (`{"status": "accepted", ...}`) and starts a background job.
3. Orchestrator queries PostgreSQL `subagents` table.
4. It routes to one of:
   - `match`: run an existing DB subagent
   - `template`: load matching JSON template
   - `build_new`: persist a generated subagent and ask user to retry
5. Subagent execution streams through Claude Agent SDK with headless permissions.
6. Result is POSTed back to Poke callback.

## Environment variables

- `DATABASE_URL`: Render PostgreSQL connection string (required in deployment)
- `POKE_API_KEY`: callback auth token for Poke
- `POKE_WEBHOOK_URL`: optional override for Poke inbound webhook URL
- `POKE_DRY_RUN`: set to `1` for local non-network callback tests
- `POKESTRATOR_AGENT_TIMEOUT`: Claude execution timeout in seconds (default `90`)
- `DB_POOL_MIN_SIZE`: minimum DB connections in asyncpg pool (default `1`)
- `DB_POOL_MAX_SIZE`: maximum DB connections in asyncpg pool (default `5`)
- `LOG_LEVEL`: logging verbosity (default `INFO`)

Optional DB tuning:

Render compatibility note: `postgres://` and `postgresql://` URLs are both accepted.

## Local setup

```bash
pip install -r requirements.txt
python src/server.py
```

## Demo frontend (React)

A lightweight React dashboard is included under `frontend/` to visualize live
orchestrator/subagent activity from the runtime log file.

```bash
cd frontend
npm install
npm run dev
```

Open `http://localhost:5173` while the MCP server is running.

Notes:
- The frontend polls `/api/demo-state` every ~1.2 seconds.
- The API is served by Vite middleware and parses `logs/pokestrator.log` by default.
- Override log source with `POKESTRATOR_LOG_FILE` (absolute path or workspace-relative).

## Deploy (Render)

`render.yaml` already includes a web service and startup command.

1. Add a Render PostgreSQL service to your project.
2. Set one of these env vars on the web service: `DATABASE_URL`, `POSTGRES_URL`, `POSTGRESQL_URL`, `RENDER_DATABASE_URL`, `DB_URL`.
3. Set `POKE_API_KEY` and `POKE_WEBHOOK_URL` in web service env.
4. Deploy.

Verification after deploy:

- In web logs: `PostgreSQL initialized and subagents table is ready` (or acceptable: `database init failed...` if running degraded mode for local fallback).
- In logs for live runs: `accepted` request logs followed by callback status.

Server endpoint should be reachable at:

- `https://<your-service>.onrender.com/mcp`

## Render/PostgreSQL checklist

1. Confirm the Postgres service is `Running` in Render.
2. Copy the external/managed connection URL and set it as `DATABASE_URL` in the web service.
3. Confirm migrations are auto-created on boot; no manual DDL is required.
4. Use `POKE_DRY_RUN=1` for local or Render logless smoke checks if needed.

## Notes

- The repository is intentionally minimal; template and dynamic subagent behavior is a starting implementation.
- `build_new` branch persists generated subagents so future matching can improve quickly.


## Detected evidence (automated analysis)

Indexed codebase: 19 recognized source files, 201 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (25 of 25)

```
.gitignore
docs/agent_sdk.md
docs/issue.txt
docs/project.md
frontend/demo/logParser.js
frontend/index.html
frontend/package.json
frontend/public/.gitkeep
frontend/src/App.jsx
frontend/src/main.jsx
frontend/src/styles.css
frontend/vite.config.js
README.md
render.yaml
requirements.txt
src/agent.py
src/db.py
src/poke.py
src/routing.py
src/server.py
src/templates/stripe_analyst.json
src/tests/reset_db.py
src/tests/test_orchestrator.py
src/tests/test_subagent.py
src/tests/view_db.py
```

### Dependencies

- frontend/package.json: @vitejs/plugin-react@^4.4.1, @xyflow/react@^12.10.0, react@^19.0.0, react-dom@^19.0.0, vite@^6.2.0
- requirements.txt: asyncpg@>=0.30.0, claude-agent-sdk@>=0.1.36, fastmcp@>=2.12.0, python-dotenv@>=1.0.1, requests@>=2.32.5, uvicorn@>=0.35.0

### Recent commits (newest first)

- Polishes agentic workflow
- Adds frontend to visualize agents
- Updates routing and agent response
- Improves agent pipeline to orchestrate and run subagents
- Adds tests and docs for DB and Agent SDK
- Fixes server async bug
- Fixes asyncio error
- rewrite README for async webhook architecture
- config: add env var placeholders to render.yaml for deployment
- wire async MCP entrypoint with fire-and-forget orchestration
- implement orchestrator with routing and subagent execution
- refactor callback client for async orchestration
- add stripe_analyst template spec
- add async PostgreSQL persistence layer
- add asyncpg and python-dotenv
- Integrates Poke with MCP
- Logs task description
- Updates requirements.txt
- Updates requirements.txt
- Tests basic Poke integration

## Key source files (fetched from GitHub, selected and truncated for size)

### docs/project.md

```markdown
# project.md — Pokestrator

## Context

This is a hackathon project where **Poke automatically improves its own capabilities** when it hits limitations (missing integrations, repeated failure patterns, poor retrieval on long docs, etc.). When that happens, Poke calls your **MCP server** (deployed on Render) which runs an **orchestrator agent**.

The orchestrator:
1) receives either **a description of the limitation**,
2) decides whether a previously-created "subagent" can solve it,
3) if so, runs it using the subagent's spec from the DB (using a custom function to integrate it with Claude Agent SDK),
4) if not, **creates a new subagent** specialized for that need (e.g., "Stripe analyst"), persists it, and makes it usable going forward.

Note that:
- Poke can connect to any MCP server via an MCP Server URL (and optional API key) you create in its Integration Library.  
  Source: https://poke.com/docs/managing-integrations
- Poke can set up its own automations (on its own platform, not related to this codebase) to call the MCP server when it needs to (detecting limitations).

---

## Design principle (important)

**Subagents should be "agents-as-data," not new code deployments.**

A subagent is a **versioned configuration package** stored in Render PostgreSQL:
- name + description (used for routing)
- system prompt / playbook
- optional connector configs (e.g., OpenAPIProvider config)
- observed success metrics
This schema is just a suggestion for now, it should end up being whatever is most compatible with the Claude Agent SDK to run and most accurate (the subagent is actually able to solve the queries it was created for).

Suggested minimal schema (CAN BE CHANGED FOR WHATEVER IS BETTER):

- `id` (uuid)
- `name` (string) — e.g., `stripe_analyst`
- `description` (string) — natural-language summary of what this subagent does (used for routing)
- `prompt_md`: system instructions/playbook
- `connectors` (optional):
  - OpenAPIProvider configs
  - ProxyProvider endpoints (remote MCP servers)
  - auth requirements
- `metrics` (implement later, after core product):
  - uses, success rate, avg latency, last_used

---

## System architecture

### 1) Poke ↔ Pokestrator MCP server (single permanent integration)

You create a single Poke custom integration:
- Name: `Pokestrator`
- MCP Server URL: `https://<your-render-service>/sse`

Poke calls a **single stable entry tool** such as:
- `pokestrator.orchestrate(task_description)`

### 2) Pokestrator MCP server components

**A. Orchestrator (router + lifecycle manager)**
- Input: task description
- Output: completed answer/action

**B. Subagent creator**
- Constructs new SubagentSpec when needed
- Assumes API keys are already available in the environment (we can hardcode them for now)
- We will have to experiment to see what works best to actually solve the task, could involve
  - finding existing MCP servers or APIs to use
  - finding the right library which the agent can run custom code to solve the task
  - e
[truncated — 753 more characters]
```

### requirements.txt

```
fastmcp>=2.12.0
uvicorn>=0.35.0
claude-agent-sdk>=0.1.36
requests>=2.32.5
python-dotenv>=1.0.1
asyncpg>=0.30.0

```

### frontend/package.json

```
{
  "name": "pokestrator-demo-frontend",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@xyflow/react": "^12.10.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.4.1",
    "vite": "^6.2.0"
  }
}

```

### src/server.py

```python
#!/usr/bin/env python3
from __future__ import annotations

import asyncio
import json
import logging
import os
import uuid
from logging.handlers import RotatingFileHandler
from pathlib import Path

from fastmcp import FastMCP

from agent import PokestratorOrchestrator

def configure_logging() -> None:
    log_level = os.getenv("LOG_LEVEL", "INFO").upper()
    log_format = "%(asctime)s %(levelname)s %(name)s %(message)s"
    handlers: list[logging.Handler] = [logging.StreamHandler()]

    log_file = os.getenv("POKESTRATOR_LOG_FILE", "logs/pokestrator.log").strip()
    if log_file:
        log_path = Path(log_file)
        if not log_path.is_absolute():
            log_path = Path.cwd() / log_path
        log_path.parent.mkdir(parents=True, exist_ok=True)

        max_bytes = int(os.getenv("POKESTRATOR_LOG_FILE_MAX_BYTES", "5242880"))
        backup_count = int(os.getenv("POKESTRATOR_LOG_FILE_BACKUP_COUNT", "3"))
        handlers.append(
            RotatingFileHandler(
                log_path,
                maxBytes=max_bytes,
                backupCount=backup_count,
                encoding="utf-8",
            )
        )

    logging.basicConfig(
        level=log_level,
        format=log_format,
        handlers=handlers,
        force=True,
    )


configure_logging()
logger = logging.getLogger("pokestrator")

mcp = FastMCP("Pokestrator")
orchestrator = PokestratorOrchestrator()
background_tasks: set[asyncio.Task] = set()


@mcp.tool(
    description=(
        "Call this tool whenever you cannot fully complete a task with your built-in capabilities — "
        "whether it requires an external API you lack (e.g. Stripe, Shopify), a system-level "
        "action (e.g. checking CPU or memory usage), or any other skill you do not natively have. "
        "When external providers are needed, Pokestrator uses managed credentials from a 1Password-backed key store; "
        "users should never paste secrets into chat. "
        "Pokestrator maintains a growing library of specialized subagents: if one already exists for "
        "the task (even from a previous conversation), it will be reused automatically. If not, a new "
        "one will be created on the fly. Always prefer calling this tool over telling the user you "
        "cannot do something."
    )
)
async def orchestrate(task_description: str, metadata: str = "") -> str:
    request_id = str(uuid.uuid4())
    logger.info(
        "accepted orchestrate request_id=%s task_description=%s metadata=%s",
        request_id,
        task_description,
        metadata,
    )

    task = asyncio.create_task(orchestrator.orchestrate(request_id, task_description, metadata))

    background_tasks.add(task)
    task.add_done_callback(lambda done: background_tasks.discard(done))

    return json.dumps(
        {
            "status": "accepted",
            "request_id": request_id,
            "message": (
                "Task accepted and running asynchronously. "
                "Result will be posted back to Poke when complete."
            ),
        }
    )

def main() -> None:
    mcp.run(
        transport="sse",
        host=os.getenv("HOST", "0.0.0.0"),
        port=int(os.getenv("PORT", "8000")),
        path=os.getenv("MCP_PATH", "/mcp"),
    )


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        logger.info("Shutting down Pokestrator")

```

### frontend/src/main.jsx

```javascript
import React from "react";
import { createRoot } from "react-dom/client";

import App from "./App.jsx";
import "./styles.css";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### frontend/src/App.jsx

```javascript
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
  Background,
  Controls,
  Handle,
  Position,
  ReactFlow,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const POLL_INTERVAL_MS = 1200;

const EMPTY_STATE = {
  generatedAt: null,
  logPath: "",
  warnings: [],
  orchestrator: {
    status: "idle",
    requestId: null,
    taskDescription: "",
    branch: "unknown",
    startedAt: null,
    lastUpdatedAt: null,
    logs: [],
  },
  subagents: [],
  recentRequests: [],
};

const STATUS_LABELS = {
  idle: "Idle",
  running: "Running",
  completed: "Completed",
  failed: "Failed",
};

function normalizeStatus(status) {
  if (status === "running" || status === "completed" || status === "failed") {
    return status;
  }
  return "idle";
}

function formatClock(timestamp) {
  if (!timestamp) {
    return "--:--:--";
  }
  const parsed = new Date(timestamp);
  if (Number.isNaN(parsed.getTime())) {
    return "--:--:--";
  }
  return parsed.toLocaleTimeString([], { hour12: false });
}

function StatusTag({ status }) {
  const normalized = normalizeStatus(status);
  return (
    <span className={`status-tag status-${normalized}`}>
      {STATUS_LABELS[normalized]}
    </span>
  );
}

function LogWindow({ logs }) {
  const logRef = useRef(null);

  const lastLogKey =
    logs && logs.length > 0
      ? `${logs[logs.length - 1].timestamp ?? ""}-${logs[logs.length - 1].text ?? ""}`
      : "";

  useLayoutEffect(() => {
    const element = logRef.current;
    if (!element) {
      return;
    }
    element.scrollTop = element.scrollHeight;
  }, [lastLogKey, logs?.length]);

  function handleWheelCapture(event) {
    const element = logRef.current;
    if (!element) {
      return;
    }
    const maxScroll = element.scrollHeight - element.clientHeight;
    if (maxScroll <= 0) {
      return;
    }
    event.preventDefault();
    event.stopPropagation();
    const nextTop = element.scrollTop + event.deltaY;
    element.scrollTop = Math.max(0, Math.min(maxScroll, nextTop));
  }

  if (!logs || logs.length === 0) {
    return <div className="log-empty">Waiting for new events...</div>;
  }

  return (
    <div
      ref={logRef}
      className="log-window nowheel nodrag nopan"
      onWheelCapture={handleWheelCapture}
      onPointerDown={(event) => event.stopPropagation()}
      onMouseDown={(event) => event.stopPropagation()}
    >
      {logs.map((entry, index) => (
        <div
          className="log-line"
          key={`${entry.timestamp ?? "no-time"}-${index}-${entry.text ?? ""}`}
        >
          <span className="log-time">{formatClock(entry.timestamp)}</span>
          <span className="log-text">{entry.text}</span>
        </div>
      ))}
    </div>
  );
}

function AgentCard({
  name,
  subtitle,
  status,
  requestId,
  logs,
  className = "",
}) {
  const normalizedStatus = normalizeStatus(status);

  return (
    <article className={`agent-card status-${normalizedStatus} ${className}`.trim()}>
      <div className="card-topline">
        <h2>{name}</h2>
        <StatusTag status={normalizedStatus} />
      </div>
      <p className="card-subtitle">{subtitle}</p>
      <p className="card-meta">request: {requestId ?? "none"}</p>
      <LogWindow logs={logs} />
    </article>
  );
}

const HIDDEN_HANDLE_STYLE = {
  opacity: 0,
  width: 12,
  height: 12,
  background: "transparent",
  border: "none",
  pointerEvents: "none",
};

const AgentFlowNode = memo(function AgentFlowNode({ data }) {
  return (
    <div className="flow-node-shell">
      <Handle
        type="target"
        position={Position.Top}
        isConnectable={false}
        style={HIDDEN_HANDLE_STYLE}
      />
      <AgentCard
        name={data.name}
        subtitle={data.subtitle}
        status={data.status}
        requestId={data.requestId}
        logs={data.logs}
        className={data.className ?? ""}
      />
      <Handle
        type="source"
        position={Position.Bottom}
        isConnectable={false}
        style={HIDDEN_HANDLE_STYLE}
      />
    </div>
  );
});

const FLOW_NODE_TYPES = { agent: AgentFlowNode };

function subagentNodeId(name) {
  return `subagent-${String(name ?? "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, "_")}`;
}

export default function App() {
  const [state, setState] = useState(EMPTY_STATE);
  const [fetchError, setFetchError] = useState("");

  useEffect(() => {
    let isCancelled = false;

    async function refresh() {
      try {
        const response = await fetch("/api/demo-state", {
          cache: "no-store",
        });
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const payload = await response.json();
        if (!isCancelled) {
          setState(payload);
          setFetchError("");
        }
      } catch (error) {
        if (!isCancelled) {
          setFetchError(
            `Dashboard API unavailable: ${String(error?.message ?? error)}`
          );
        }
      }
    }

    refresh();
    const timer = window.setInterval(refresh, POLL_INTERVAL_MS);
    return () => {
      isCancelled = true;
      window.clearInterval(timer);
    };
  }, []);

  const orchestrator = state.orchestrator ?? EMPTY_STATE.orchestrator;
  const subagents = state.subagents ?? [];
  const recentRequests = state.recentRequests ?? [];

  const warnings = useMemo(() => {
    const warningList = [];
    if (fetchError) {
      warningList.push(fetchError);
    }
    if (Array.isArray(state.warnings)) {
      warningList.push(...state.warnings);
    }
    return warningList;
  }, [fetchError, state.warnings]);

  const flowNodes = useMemo(() => {
    const orchestratorSubtitle = orchestrator.taskDescription
      ? `Task: ${orchestrator.taskDescription}`
      : "No active orchestration request";

    const nodes = [
      {
        id: "orchestrator",
        type: "agent",
        position: { x: 0, y: 20 },
        draggable: false,
       
[truncated — 4602 more characters]
```

### render.yaml

```yaml
services:
  - type: web
    name: pokestrator-mcp-server
    runtime: python
    buildCommand: pip install -r requirements.txt
    startCommand: python src/server.py
    plan: free
    autoDeploy: false
    envVars:
      - key: ENVIRONMENT
        value: production
      - key: MCP_PATH
        value: /mcp
      - key: DATABASE_URL
        value: <set-in-render-dashboard>
      - key: POKE_WEBHOOK_URL
        value: https://poke.com/api/v1/inbound-sms/webhook
      - key: POKESTRATOR_AGENT_TIMEOUT
        value: "90"
      - key: LOG_LEVEL
        value: INFO

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Pokestrator Dashboard</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### src/poke.py

```python
import os
from typing import Any, Mapping

import requests
from dotenv import load_dotenv

load_dotenv()


def _bool_env(name: str, default: bool = False) -> bool:
    value = os.getenv(name)
    if value is None:
        return default
    return value.lower() in {"1", "true", "yes", "on"}


def _webhook_url() -> str:
    return (
        os.getenv("POKE_WEBHOOK_URL")
        or "https://poke.com/api/v1/inbound-sms/webhook"
    )


def send_poke_message(message: str, metadata: Mapping[str, Any] | None = None) -> dict:
    payload = {"message": message}
    if metadata:
        payload["metadata"] = dict(metadata)

    if _bool_env("POKE_DRY_RUN", False):
        return {"ok": True, "dry_run": True, "payload": payload}

    api_key = os.getenv("POKE_API_KEY")
    if not api_key:
        raise ValueError("POKE_API_KEY is not set")

    response = requests.post(
        _webhook_url(),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        json=payload,
        timeout=15,
    )
    response.raise_for_status()
    return {
        "ok": True,
        "status_code": response.status_code,
        "response": response.text,
    }


if __name__ == "__main__":
    print(send_poke_message("This is a test message from the Poke API"))

```

### frontend/vite.config.js

```javascript
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

import { buildDemoStateFromLog } from "./demo/logParser.js";

const configDir = path.dirname(fileURLToPath(import.meta.url));
const workspaceRoot = path.resolve(configDir, "..");

function resolveLogPath() {
  const configuredPath =
    process.env.POKESTRATOR_LOG_FILE ||
    process.env.VITE_POKESTRATOR_LOG_FILE ||
    "logs/pokestrator.log";
  if (path.isAbsolute(configuredPath)) {
    return configuredPath;
  }
  return path.resolve(workspaceRoot, configuredPath);
}

async function readDemoState(logPath) {
  try {
    const contents = await fs.readFile(logPath, "utf-8");
    return buildDemoStateFromLog(contents, logPath);
  } catch (error) {
    return {
      generatedAt: new Date().toISOString(),
      logPath,
      warnings: [
        `Could not read log file at ${logPath}: ${String(
          error?.message ?? error
        )}`,
      ],
      orchestrator: {
        status: "idle",
        requestId: null,
        taskDescription: "",
        branch: "unknown",
        startedAt: null,
        lastUpdatedAt: null,
        logs: [],
      },
      subagents: [],
      recentRequests: [],
    };
  }
}

function registerStateEndpoint(middlewares, logPath) {
  middlewares.use(async (req, res, next) => {
    if (!req.url || !req.url.startsWith("/api/demo-state")) {
      return next();
    }

    const state = await readDemoState(logPath);
    res.statusCode = 200;
    res.setHeader("Content-Type", "application/json; charset=utf-8");
    res.setHeader("Cache-Control", "no-store");
    res.end(JSON.stringify(state));
  });
}

function demoStatePlugin() {
  const logPath = resolveLogPath();

  return {
    name: "pokestrator-demo-state",
    configureServer(server) {
      registerStateEndpoint(server.middlewares, logPath);
    },
    configurePreviewServer(server) {
      registerStateEndpoint(server.middlewares, logPath);
    },
  };
}

export default defineConfig({
  plugins: [react(), demoStatePlugin()],
});

```

[9 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]