{
  "$comment": "Machine-readable entry point for AI agents. The prose version is /.well-known/llms.txt — read that if you can read prose. (/llms.txt is the same file, but it sits behind an access gate and will hand you a sign-in page instead.)",
  "name": "EasyMakeQuiz",
  "description": "Korean math worksheet builder. An AI driving the user's own browser can read exam PDFs into their library at no credit cost.",
  "origin": "https://easymakequiz.com",
  "documentation": "https://easymakequiz.com/.well-known/llms.txt",
  "documentationGated": "https://easymakequiz.com/llms.txt",
  "mathSyntax": {
    "url": "https://easymakequiz.com/.well-known/llms-easylatex.txt",
    "note": "Formulas in problem text are Easy LaTeX, not raw LaTeX: frac(1,2) and sqrt(x), not \\frac{1}{2} and \\sqrt{x}. Read this before writing or editing any formula, and before telling a user how to type one."
  },
  "updated": "2026-08-27",
  "lanes": [
    {
      "id": "browser",
      "title": "Drive the user's browser (figures supported, costs the user nothing)",
      "requires": [
        "a subscription on the user's account",
        "the ability to launch Chrome on the user's machine and attach over CDP"
      ],
      "headless": false,
      "headlessNote": "Must NOT be headless. The user confirms boxes on screen, and the login cookie lives in their Chrome profile.",
      "port": {
        "hardcode": false,
        "flag": "--remote-debugging-port=0",
        "why": "Never hardcode 9222. It may already be held by another agent, an IDE, or your own earlier run, and then either Chrome opens no port at all or you attach to somebody else's browser and drive a screen nobody is watching. Neither failure announces itself.",
        "file": "<user-data-dir>/DevToolsActivePort",
        "fileFormat": "line 1 = the port Chrome chose, line 2 = /devtools/browser/<uuid>",
        "fileIsTheSourceOfTruth": "Read it again whenever you need the port back — new session, compacted conversation, second terminal, spawned script. Nothing has to remember a number.",
        "onlyForEphemeral": "Chrome writes this file ONLY when the requested port is 0. Ask for a fixed port and it writes nothing at all.",
        "reuseBeforeLaunching": "Read the file before launching. If GET http://localhost:<port>/json/version answers 200, attach — do not launch a second browser over a profile that already has one.",
        "staleFile": "A crash leaves the file behind, so the port in it can be dead. That GET is what tells you.",
        "missingFileButProfileInUse": "A lockfile in that directory, or a chrome process whose command line names that --user-data-dir, means a browser started with a FIXED port is already running and therefore never wrote the file. Read the port off its command line. Launching again cannot help: the second process hands your tab to the first and exits."
      },
      "launch": {
        "detached": "Launch it OUTSIDE your own process group, or your shell kills it the moment your command returns — the window appears for a second and vanishes while your command reports exit 0. Hand the launch to a system service.",
        "windows": "Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = '\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=0 --user-data-dir=\"%USERPROFILE%\\.emq\\chrome-profile\" --no-first-run --no-default-browser-check https://easymakequiz.com/ko/dashboard' }",
        "windowsNote": "Start-Process is NOT enough — it stays inside your job object. WMI creates the process outside it.",
        "macos": "open -na \"Google Chrome\" --args --remote-debugging-port=0 --user-data-dir=\"$HOME/.emq/chrome-profile\" --no-first-run --no-default-browser-check https://easymakequiz.com/ko/dashboard",
        "linux": "setsid google-chrome --remote-debugging-port=0 --user-data-dir=\"$HOME/.emq/chrome-profile\" --no-first-run --no-default-browser-check https://easymakequiz.com/ko/dashboard </dev/null >/dev/null 2>&1 &",
        "note": "Chrome 136+ refuses a debugging port on the default profile, so a dedicated profile is required. The user signs in there once by hand.",
        "verify": "Wait for DevToolsActivePort to appear (up to 15 s), read the port, then GET http://localhost:<port>/json/version once. Do not count windows or process handles — a browser about to be killed still has both. Say localhost, not 127.0.0.1: recent Chrome answers the IP-form Host header with an empty 404 on every DevTools endpoint."
      },
      "connect": {
        "call": "await window.__EMQ_AI__.connect()",
        "note": "This call is the only way to connect. There is deliberately no button a human can press.",
        "failures": {
          "NOT_LOGGED_IN": "That window has no user session. Ask the user to sign in inside that window and STOP — wait for them to say they are done, then call connect() once. Do not poll, reload, or navigate in a loop, and never type into a sign-in form.",
          "SUBSCRIPTION_REQUIRED": "Stop and tell the user. Retrying returns the same answer.",
          "BLOCKED": "Usually a stale CSRF token. Reload the page and call connect() once more.",
          "NETWORK": "Navigate the page to easymakequiz.com first."
        },
        "accessGate": "Being bounced to easymakequiz.cloudflareaccess.com is normal, not an error. It is the user's sign-in — same rule as NOT_LOGGED_IN."
      },
      "status": {
        "call": "window.__EMQ_AI__.status()",
        "returns": ["connected", "watching", "human", "lastSeenSecondsAgo", "bridge", "lane"]
      },
      "heartbeat": {
        "call": "window.__EMQ_AI__.ping()",
        "idleInterval": "about every 30 minutes",
        "note": "The connection is tied to your liveness, not to the browser staying open. 60 minutes of silence flags the badge; 3 hours ends the session and recognition goes back to costing the user credits."
      },
      "watch": {
        "why": "connect() returning ok is not the end of setup — it is the start of waiting. An agent runtime does not wake up because a web page changed, so ending your turn with 'tell me once you have uploaded' means nobody is listening when the user presses the button, and their counter sits at 0 until they give up and pay.",
        "how": "Run a FOREGROUND blocking script that polls getFindRequest()/getCleansingRequest() every 2 s and EXITS the moment one is non-null, printing it. Your tool call returns, you are still inside your own turn, and the looking happens where the vision model is.",
        "timeout": "Keep the watch window under your runtime's command timeout (some are 2 minutes). On timeout print one idle line and run it again.",
        "output": "One line, at the end. Not one per poll.",
        "notABackgroundWorker": "The script may fetch and submit. It must not decide anything — there is no vision model inside a node process.",
        "details": "https://easymakequiz.com/.well-known/llms.txt — §3, \"stand watch\""
      },
      "work": {
        "surface": "window.__EMQAI__",
        "availableWhen": "the user has uploaded a PDF and opened the review overlay; undefined before that is normal — that is what the watch above is for",
        "start": "window.__EMQAI__.manifest()",
        "manifestFields": ["context", "flow", "methods"],
        "rules": [
          "All coordinates are percentages (0-100) of the image you were handed, never pixels. Wrong units are rejected whole.",
          "Do not navigate the user's screen. Every method takes a pageIndex; use requestNavigate(i) to ask.",
          "Detection is not your choice: call getDetectSpec() and run its prompt verbatim against your own vision model. A local detector or a layout heuristic is how column-height boxes happen.",
          "You look at every image yourself. A script you spawn can fetch and submit, but it cannot see — a worker told to 'analyse the pages' polls forever while nothing arrives.",
          "An order's pages are the whole job. Never widen a one-page order; a shrinking list on re-poll is progress, not a new request.",
          "Handwriting removal asks about the same page twice on purpose; two independent answers are intersected. Do not cache your first answer.",
          "Submissions save automatically in batches moments after they arrive; there is no save button. A server-rejected problem is not resent on its own — read getSubmitState() and fix what it lists."
        ]
      }
    }
  ],
  "endpoints": [
    { "method": "GET", "path": "/api/ai-connect/agent-session", "returns": "{ agent: boolean, human: boolean }" },
    { "method": "POST", "path": "/api/ai-connect/attach", "note": "what __EMQ_AI__.connect() calls; needs cookies and the CSRF header" },
    { "method": "DELETE", "path": "/api/ai-connect/attach", "note": "disconnect" }
  ],
  "pages": {
    "connect": "/ko/connect",
    "work": "/ko/dashboard",
    "library": "/ko/problems",
    "pricing": "/pricing"
  },
  "locales": ["ko", "en", "ja"]
}
