diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 142e924..a962a30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,4 +16,23 @@ jobs: cache: npm - run: npm ci - run: npm test + - run: npm run build:example + plans: + # Replays the example's Gherkin plans through agent-browser against native WebMCP. + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx agent-browser install --with-deps || npx agent-browser install + - run: npm run build:example + - name: Start the example + run: | + npm run start:example & + for i in $(seq 1 30); do curl -sf http://localhost:3000/ >/dev/null && break; sleep 1; done + - run: npm run test:plans diff --git a/docs/board.png b/docs/board.png new file mode 100644 index 0000000..49b8081 Binary files /dev/null and b/docs/board.png differ diff --git a/docs/task-detail.png b/docs/task-detail.png new file mode 100644 index 0000000..4d0e0fb Binary files /dev/null and b/docs/task-detail.png differ diff --git a/examples/with-sightmap-webmcp/.gitignore b/examples/with-sightmap-webmcp/.gitignore new file mode 100644 index 0000000..8d9dac0 --- /dev/null +++ b/examples/with-sightmap-webmcp/.gitignore @@ -0,0 +1,3 @@ +# Local sightmap browser session state +.sightmap/.session +.sightmap/snapshots/ diff --git a/examples/with-sightmap-webmcp/.sightkick/journeys.yaml b/examples/with-sightmap-webmcp/.sightkick/journeys.yaml new file mode 100644 index 0000000..95be947 --- /dev/null +++ b/examples/with-sightmap-webmcp/.sightkick/journeys.yaml @@ -0,0 +1,28 @@ +version: 1 + +# Journeys are compile-time orderings over tools. They never execute; they +# become `guidance` breadcrumbs on each tool's result so a turn-by-turn agent +# knows what tends to come next. Which journeys matter is the second human +# decision in this layer: it comes from how people actually use the app — +# analytics, session replay, support tickets — not from the corpus. + +journeys: + - name: add_and_review + description: Add a task, then confirm it on the board. + steps: + - add_task + - tool: list_tasks + reason: confirm the task you just added is listed + + - name: triage + description: Open a task, finish it from its page, come back, check the board. + steps: + - open_task + - tool: read_task + reason: confirm you opened the right task + - tool: mark_done + reason: finish it + - tool: back_to_board + reason: return to the board + - tool: set_filter + reason: switch to Done to see it moved diff --git a/examples/with-sightmap-webmcp/.sightkick/tools.yaml b/examples/with-sightmap-webmcp/.sightkick/tools.yaml new file mode 100644 index 0000000..2125369 --- /dev/null +++ b/examples/with-sightmap-webmcp/.sightkick/tools.yaml @@ -0,0 +1,124 @@ +version: 1 +name: tasks + +# The tool layer: named, atomic actions over the corpus in ../.sightmap/. +# Tools never carry selectors — they address the app by component name and +# extracted property, and `sightkick build` refuses any name the corpus +# doesn't have. Human review happens here: which actions deserve to be tools +# is a product decision, not something to derive mechanically. + +tools: + - name: list_tasks + description: List the tasks currently shown on the board, with their done state. + ensure_view: TaskBoard + returns: + description: One row per visible task. + list: + rows: TaskRow + fields: + title: title + done: done + + - name: add_task + description: Add a task to the board by title. Idempotent — an existing title is left alone. + ensure_view: TaskBoard + params: + - name: title + type: string + required: true + description: The task title. + guard: + present: { query: 'TaskRow[title="{{title}}"]' } + steps: + - fill: { query: NewTaskInput, value: "{{title}}" } + - click: { query: AddTaskButton } + - wait_for: { query: 'TaskRow[title="{{title}}"]' } + returns: + description: The done state of the row just added ("false"). + value: { query: 'TaskRow[title="{{title}}"]', property: done } + + - name: complete_task + description: Mark a task done from the board by clicking its toggle. Idempotent. + ensure_view: TaskBoard + params: + - name: title + type: string + required: true + guard: + present: { query: 'TaskRow[title="{{title}}"][done="true"]' } + steps: + - click: { query: 'TaskRow[title="{{title}}"] TaskToggle' } + - wait_for: { query: 'TaskRow[title="{{title}}"][done="true"]' } + returns: + value: { query: 'TaskRow[title="{{title}}"]', property: done } + + - name: delete_task + description: Delete a task from the board by title, then return the remaining rows. + ensure_view: TaskBoard + params: + - name: title + type: string + required: true + steps: + - click: { query: 'TaskRow[title="{{title}}"] DeleteTaskButton' } + returns: + description: The rows left on the board. + list: + rows: TaskRow + fields: + title: title + done: done + + - name: set_filter + description: Show All, Active, or Done tasks, and return the rows now visible. + ensure_view: TaskBoard + params: + - name: filter + type: enum + required: true + values: [All, Active, Done] + steps: + - click: { query: 'FilterButton[label="{{filter}}"]' } + - wait_for: { query: 'FilterButton[label="{{filter}}"][active="true"]' } + returns: + list: + rows: TaskRow + fields: + title: title + done: done + + - name: open_task + description: Open a task's detail page from the board. Navigates to /tasks/:id. + ensure_view: TaskBoard + params: + - name: title + type: string + required: true + steps: + - click: { query: 'TaskRow[title="{{title}}"] TaskLink' } + - wait_for: { view: TaskDetail } + + - name: read_task + description: Read the open task's title and status from its detail page. + ensure_view: TaskDetail + returns: + description: The status shown on the page ("Active" or "Done"). + value: { query: TaskDetail, property: status } + + - name: mark_done + description: Mark the open task done from its detail page. Idempotent. + ensure_view: TaskDetail + guard: + present: { query: 'TaskDetail[status="Done"]' } + steps: + - click: { query: MarkDoneButton } + - wait_for: { query: 'TaskDetail[status="Done"]' } + returns: + value: { query: TaskDetail, property: status } + + - name: back_to_board + description: Return from a task's detail page to the board. + ensure_view: TaskDetail + steps: + - click: { query: TaskDetail BackToBoardLink } + - wait_for: { view: TaskBoard } diff --git a/examples/with-sightmap-webmcp/.sightmap/about.yaml b/examples/with-sightmap-webmcp/.sightmap/about.yaml new file mode 100644 index 0000000..80ecc0e --- /dev/null +++ b/examples/with-sightmap-webmcp/.sightmap/about.yaml @@ -0,0 +1,13 @@ +version: 1 + +views: + - name: About + route: /about + url: http://localhost:3000/about + source: app/about/page.tsx + dependencies: + - app/layout.tsx + description: Static explainer. No tools are registered here; that is correct, not a failure. + components: + - name: AboutText + selector: '[data-component="AboutText"]' diff --git a/examples/with-sightmap-webmcp/.sightmap/config.yaml b/examples/with-sightmap-webmcp/.sightmap/config.yaml new file mode 100644 index 0000000..b825518 --- /dev/null +++ b/examples/with-sightmap-webmcp/.sightmap/config.yaml @@ -0,0 +1 @@ +version: 1 diff --git a/examples/with-sightmap-webmcp/.sightmap/home.yaml b/examples/with-sightmap-webmcp/.sightmap/home.yaml new file mode 100644 index 0000000..f683bae --- /dev/null +++ b/examples/with-sightmap-webmcp/.sightmap/home.yaml @@ -0,0 +1,69 @@ +version: 1 + +# Seeded by `sightmap-next seed`, then curated against `next dev` with the +# sightmap-authoring skill (snapshot --coverage until 0 orphaned nodes). + +views: + - name: TaskBoard + route: / + url: http://localhost:3000/ + source: app/page.tsx + dependencies: + - app/layout.tsx + - components/TaskBoard.tsx + - lib/store.ts + description: The board — add a task, filter, toggle, delete, or open one. + memory: + - State is in-memory per tab and resets on a full page load; every fresh open shows the same three seed tasks + - Adding submits a form, so Enter in the input works as well as the button + - Filter labels are All / Active / Done (not "Completed") + components: + - name: TaskBoard + selector: '[data-component="TaskBoard"]' + source: components/TaskBoard.tsx + children: + - name: NewTaskInput + selector: '[data-component="NewTaskInput"]' + description: Controlled React input; the runtime fills it through the native value setter + - name: AddTaskButton + selector: '[data-component="AddTaskButton"]' + - name: FilterBar + selector: '[data-component="FilterBar"]' + children: + - name: FilterButton + selector: '[data-component="FilterButton"]' + properties: + - name: label + extract: text + - name: active + extract: attr=aria-pressed + - name: TaskList + selector: '[data-component="TaskList"]' + - name: TaskRow + selector: '[data-component="TaskRow"]' + description: One task. Title lives in the link; done state is mirrored onto data-done so it is filterable + properties: + - name: title + extract: TaskLink.text + - name: done + extract: attr=data-done + - name: id + extract: attr=data-id + children: + - name: TaskToggle + selector: '[data-component="TaskToggle"]' + description: Reads "Done" on an active task and "Undo" on a done one + properties: + - name: label + extract: text + - name: TaskLink + selector: '[data-component="TaskLink"]' + description: Client-side navigation to /tasks/:id + properties: + - name: text + extract: text + - name: DeleteTaskButton + selector: '[data-component="DeleteTaskButton"]' + - name: EmptyState + selector: '[data-component="EmptyState"]' + description: Rendered only when the current filter shows no rows diff --git a/examples/with-sightmap-webmcp/.sightmap/shared.yaml b/examples/with-sightmap-webmcp/.sightmap/shared.yaml new file mode 100644 index 0000000..55a71ed --- /dev/null +++ b/examples/with-sightmap-webmcp/.sightmap/shared.yaml @@ -0,0 +1,16 @@ +version: 1 + +# Content reachable from every route (the spec's shared.yaml convention). +components: + - name: SiteNav + selector: '[data-component="SiteNav"]' + source: app/layout.tsx + children: + - name: NavLink + selector: '[data-component="NavLink"]' + properties: + - name: label + extract: text + - name: RouteAnnouncer + selector: '#__next-route-announcer__' + description: Next.js client-navigation announcer for screen readers; empty until a navigation, never a target diff --git a/examples/with-sightmap-webmcp/.sightmap/tasks.yaml b/examples/with-sightmap-webmcp/.sightmap/tasks.yaml new file mode 100644 index 0000000..ebc1395 --- /dev/null +++ b/examples/with-sightmap-webmcp/.sightmap/tasks.yaml @@ -0,0 +1,44 @@ +version: 1 + +views: + - name: TaskDetail + route: /tasks/:id + source: app/tasks/[id]/page.tsx + dependencies: + - app/layout.tsx + - components/TaskDetail.tsx + - lib/store.ts + description: One task's page — title, status badge, mark-done, back link. + memory: + - Reached by client-side navigation from the board; a full load of /tasks/:id shows the seed state, so ids above 3 render TaskMissing + - The Mark done button is disabled once the task is done, so a second click is a no-op rather than a toggle + components: + - name: TaskDetail + selector: '[data-component="TaskDetail"]' + source: components/TaskDetail.tsx + properties: + - name: title + extract: TaskTitle.text + - name: status + extract: attr=data-status + children: + - name: BackToBoardLink + selector: '[data-component="BackToBoardLink"]' + - name: TaskTitle + selector: '[data-component="TaskTitle"]' + properties: + - name: text + extract: text + - name: StatusBadge + selector: '[data-component="StatusBadge"]' + properties: + - name: label + extract: text + - name: MarkDoneButton + selector: '[data-component="MarkDoneButton"]' + - name: TaskMissing + selector: '[data-component="TaskMissing"]' + description: Shown for an id the store does not have + children: + - name: BackToBoardLink + selector: '[data-component="BackToBoardLink"]' diff --git a/examples/with-sightmap-webmcp/AGENTS.md b/examples/with-sightmap-webmcp/AGENTS.md new file mode 100644 index 0000000..00fa302 --- /dev/null +++ b/examples/with-sightmap-webmcp/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.md + +How a coding agent should work in this app. Install the skills first: + +```bash +npx @sightmap/sightkick skills install # sightkick-authoring, sightkick-debug, sightmap-authoring, sightmap-browser +npx agent-browser skills get core # agent-browser's own workflow (also: skills get webmcp-gen) +``` + +## Before touching UI code + +Read `.sightmap/` for the view you are changing — component names, properties, and the +`memory:` notes are the contract every tool and plan depends on. Keep +`data-component="Name"` attributes stable; they are the selectors. + +## After changing UI code + +1. `npm run build && npm run start` +2. `npx sightmap browser start --detach --url http://localhost:3000/` +3. `npx sightmap snapshot --coverage --url ` — fix the corpus until `0 orphaned T3 ✓` +4. `npm run sightmap:validate` — the tool layer must still compile +5. `npm run test:plans` — every plan must pass; if a plan's expectation is now wrong, + update the plan and re-stamp it, never loosen the expectation to get green +6. Commit the `.sightmap/` and `.sightkick/` changes with the code change + +## Adding a tool (human-in-the-loop) + +Draft one tool per real user action on the view, following `sightkick-authoring`. Then +stop and ask which of the drafted tools matter — do not add every possible action. Journeys +are ranked by the product owner from real usage, not guessed. + +## Do not + +- Regenerate `.sightmap/` from source. `sightmap-next seed` is a one-time scaffold. +- Put CSS selectors in `.sightkick/`. Tools reference corpus component names only. +- Edit `public/.well-known/*`, `public/sightkick-runtime.js`, or `webmcp.init.js` by hand; + `sightmap-next build` writes them. diff --git a/examples/with-sightmap-webmcp/README.md b/examples/with-sightmap-webmcp/README.md new file mode 100644 index 0000000..bd0476c --- /dev/null +++ b/examples/with-sightmap-webmcp/README.md @@ -0,0 +1,45 @@ +# with-sightmap-webmcp + +A Next.js 16 task board whose `.sightmap/` corpus and `.sightkick/` tool layer compile into +WebMCP tools that agent-browser, ChatGPT's browser, and Chrome can call by name. Written to +be lifted into `vercel/next.js/examples/` as-is. + +``` +app/ the app — three routes, data-component hooks, in the layout +.sightmap/ the corpus: seeded by `sightmap-next seed`, curated to 100% direct coverage +.sightkick/ the tool layer (9 tools) and two journeys +features/ three scenarios in plain Gherkin +plans/ each scenario resolved once to tool calls; stamped against the feature + IR +public/.well-known/ generated by `sightmap-next build`: sightmap.json, sightkick.json +public/sightkick-runtime.js, webmcp.init.js generated: the runtime, and runtime + IR for --init-script +``` + +## Run + +```bash +npm install +npx agent-browser install # Chrome for Testing 152, which has native WebMCP +npm run build # prebuild → sightmap-next build +npm run start +``` + +```bash +npx agent-browser open localhost:3000 +npx agent-browser webmcp list +npx agent-browser webmcp invoke add_task --params '{"title":"Hello"}' +npx agent-browser webmcp invoke list_tasks +npm run test:plans # replays features/ via plans/, no model in the loop +``` + +State is in-memory per tab on purpose: every fresh `open` starts from the same three tasks. + +## Change something + +- **UI changed?** `npx sightmap browser start --detach --url http://localhost:3000/`, then + `npx sightmap snapshot --coverage --url http://localhost:3000/` and edit `.sightmap/` + until it reads `0 orphaned T3 ✓`. `npm run sightmap:validate` compiles the tool layer + against the corpus and fails on any name it no longer has. +- **Want a new action?** Add a tool to `.sightkick/tools.yaml`, rebuild, re-stamp the plans + that changed: `npx sightmap-next run-plan plans/x.plan.json --stamp`. +- **New scenario?** Write the `.feature`, have an agent (or yourself) resolve it into a + `plans/*.plan.json`, stamp it, add it to `test:plans`. diff --git a/examples/with-sightmap-webmcp/app/about/page.tsx b/examples/with-sightmap-webmcp/app/about/page.tsx new file mode 100644 index 0000000..3336635 --- /dev/null +++ b/examples/with-sightmap-webmcp/app/about/page.tsx @@ -0,0 +1,22 @@ +export default function AboutPage() { + return ( +
+

About

+

+ This app ships a .sightmap/ corpus and a{" "} + .sightkick/ tool layer. At build time they become{" "} + /.well-known/sightmap.json and{" "} + /.well-known/sightkick.json, and on every page the + Sightkick runtime registers the tools on{" "} + document.modelContext — the WebMCP surface. +

+

+ Try it:{" "} + + agent-browser open localhost:3000 && agent-browser webmcp list + + . +

+
+ ); +} diff --git a/examples/with-sightmap-webmcp/app/globals.css b/examples/with-sightmap-webmcp/app/globals.css new file mode 100644 index 0000000..256c3d9 --- /dev/null +++ b/examples/with-sightmap-webmcp/app/globals.css @@ -0,0 +1,59 @@ +:root { + color-scheme: light dark; + font-family: ui-sans-serif, system-ui, sans-serif; +} +body { + margin: 0; + max-width: 40rem; + padding: 1.5rem; + margin-inline: auto; + line-height: 1.5; +} +nav { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid #8884; + padding-bottom: 0.75rem; +} +form { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; +} +input { + flex: 1; + padding: 0.5rem; + font: inherit; +} +button { + font: inherit; + padding: 0.4rem 0.7rem; + cursor: pointer; +} +button[aria-pressed="true"] { + font-weight: 700; + text-decoration: underline; +} +ul { + list-style: none; + padding: 0; +} +li { + display: flex; + gap: 0.75rem; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #8882; +} +li[data-done="true"] a { + text-decoration: line-through; + opacity: 0.6; +} +[data-component="StatusBadge"] { + display: inline-block; + padding: 0.1rem 0.5rem; + border-radius: 1rem; + border: 1px solid #8886; + margin-right: 0.75rem; +} diff --git a/examples/with-sightmap-webmcp/app/layout.tsx b/examples/with-sightmap-webmcp/app/layout.tsx new file mode 100644 index 0000000..b17922e --- /dev/null +++ b/examples/with-sightmap-webmcp/app/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { SightkickTools } from "@sightmap/next"; +import ir from "../public/.well-known/sightkick.json"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Tasks — Sightmap + WebMCP on Next.js", + description: + "A Next.js app whose .sightmap/ corpus compiles into WebMCP tools.", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + {/* Registers the compiled tool layer on document.modelContext (WebMCP) + on every page. The IR is inlined at build time from + public/.well-known/sightkick.json, which is also served as-is for + external readers; pass a URL string instead to fetch it lazily. */} + + + + ); +} diff --git a/examples/with-sightmap-webmcp/app/page.tsx b/examples/with-sightmap-webmcp/app/page.tsx new file mode 100644 index 0000000..5274451 --- /dev/null +++ b/examples/with-sightmap-webmcp/app/page.tsx @@ -0,0 +1,5 @@ +import { TaskBoard } from "@/components/TaskBoard"; + +export default function HomePage() { + return ; +} diff --git a/examples/with-sightmap-webmcp/app/tasks/[id]/page.tsx b/examples/with-sightmap-webmcp/app/tasks/[id]/page.tsx new file mode 100644 index 0000000..e6e8ba8 --- /dev/null +++ b/examples/with-sightmap-webmcp/app/tasks/[id]/page.tsx @@ -0,0 +1,10 @@ +import { TaskDetail } from "@/components/TaskDetail"; + +export default async function TaskPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + return ; +} diff --git a/examples/with-sightmap-webmcp/components/TaskBoard.tsx b/examples/with-sightmap-webmcp/components/TaskBoard.tsx new file mode 100644 index 0000000..f10b2cd --- /dev/null +++ b/examples/with-sightmap-webmcp/components/TaskBoard.tsx @@ -0,0 +1,88 @@ +"use client"; +import Link from "next/link"; +import { useState } from "react"; +import { store, useTasks } from "@/lib/store"; + +const FILTERS = ["All", "Active", "Done"] as const; +type Filter = (typeof FILTERS)[number]; + +export function TaskBoard() { + const tasks = useTasks(); + const [filter, setFilter] = useState("All"); + const [draft, setDraft] = useState(""); + const visible = tasks.filter((t) => + filter === "All" ? true : filter === "Done" ? t.done : !t.done, + ); + + return ( +
+

Tasks

+
{ + e.preventDefault(); + store.add(draft); + setDraft(""); + }} + > + setDraft(e.target.value)} + /> + +
+ +
+ {FILTERS.map((f) => ( + + ))} +
+ +
    + {visible.map((t) => ( +
  • + + + {t.title} + + +
  • + ))} +
+ {visible.length === 0 &&

Nothing here.

} +
+ ); +} diff --git a/examples/with-sightmap-webmcp/components/TaskDetail.tsx b/examples/with-sightmap-webmcp/components/TaskDetail.tsx new file mode 100644 index 0000000..17f5794 --- /dev/null +++ b/examples/with-sightmap-webmcp/components/TaskDetail.tsx @@ -0,0 +1,37 @@ +"use client"; +import Link from "next/link"; +import { store, useTasks } from "@/lib/store"; + +export function TaskDetail({ id }: { id: number }) { + const task = useTasks().find((t) => t.id === id); + if (!task) { + return ( +
+

No task #{id}.

+ + Back to the board + +
+ ); + } + const status = task.done ? "Done" : "Active"; + return ( +
+ + ← Back to the board + +

{task.title}

+

+ {status} + +

+
+ ); +} diff --git a/examples/with-sightmap-webmcp/features/add-task.feature b/examples/with-sightmap-webmcp/features/add-task.feature new file mode 100644 index 0000000..c7660e7 --- /dev/null +++ b/examples/with-sightmap-webmcp/features/add-task.feature @@ -0,0 +1,9 @@ +Feature: Add a task + As someone tracking work + I want to add a task by title + So that it shows up on the board + + Scenario: A new task appears on the board, not yet done + When I add a task "Ship the proof of concept" + Then the board lists "Ship the proof of concept" + And it is not done diff --git a/examples/with-sightmap-webmcp/features/filter.feature b/examples/with-sightmap-webmcp/features/filter.feature new file mode 100644 index 0000000..f4881da --- /dev/null +++ b/examples/with-sightmap-webmcp/features/filter.feature @@ -0,0 +1,12 @@ +Feature: Filter the board + As someone tracking work + I want to see only active or only done tasks + So that I can focus + + Scenario: Only done tasks under the Done filter + When I show only Done tasks + Then "Ship to Vercel" is listed + And "Write the sightmap" is not listed + When I show only Active tasks + Then "Write the sightmap" is listed + And "Ship to Vercel" is not listed diff --git a/examples/with-sightmap-webmcp/features/triage.feature b/examples/with-sightmap-webmcp/features/triage.feature new file mode 100644 index 0000000..91b825d --- /dev/null +++ b/examples/with-sightmap-webmcp/features/triage.feature @@ -0,0 +1,13 @@ +Feature: Triage from the detail page + As someone tracking work + I want to finish a task from its own page + So that the board reflects it when I come back + + Scenario: Complete a task from its detail page + When I open "Write the sightmap" + Then the task page shows it as "Active" + When I mark it done + Then the task page shows it as "Done" + When I go back to the board + And I show only Done tasks + Then "Write the sightmap" is listed as done diff --git a/examples/with-sightmap-webmcp/lib/store.ts b/examples/with-sightmap-webmcp/lib/store.ts new file mode 100644 index 0000000..339b2db --- /dev/null +++ b/examples/with-sightmap-webmcp/lib/store.ts @@ -0,0 +1,52 @@ +"use client"; +// A deliberately tiny in-memory store. State lives for the life of the tab +// and resets on every full page load, so every `agent-browser open` — and +// every replayed plan — starts from the same three tasks. Client-side +// navigation (next/link) keeps it, which is what the detail page relies on. +import { useSyncExternalStore } from "react"; + +export type Task = { id: number; title: string; done: boolean }; + +const SEED: Task[] = [ + { id: 1, title: "Write the sightmap", done: false }, + { id: 2, title: "Compile the tool layer", done: false }, + { id: 3, title: "Ship to Vercel", done: true }, +]; + +let tasks: Task[] = SEED; +let nextId = 4; +const listeners = new Set<() => void>(); + +function subscribe(fn: () => void) { + listeners.add(fn); + return () => listeners.delete(fn); +} +function commit(next: Task[]) { + tasks = next; + for (const fn of listeners) fn(); +} + +export function useTasks(): Task[] { + return useSyncExternalStore( + subscribe, + () => tasks, + () => SEED, + ); +} + +export const store = { + add(title: string) { + const t = title.trim(); + if (!t) return; + commit([...tasks, { id: nextId++, title: t, done: false }]); + }, + toggle(id: number) { + commit(tasks.map((t) => (t.id === id ? { ...t, done: !t.done } : t))); + }, + complete(id: number) { + commit(tasks.map((t) => (t.id === id ? { ...t, done: true } : t))); + }, + remove(id: number) { + commit(tasks.filter((t) => t.id !== id)); + }, +}; diff --git a/examples/with-sightmap-webmcp/next.config.mjs b/examples/with-sightmap-webmcp/next.config.mjs new file mode 100644 index 0000000..5837ba9 --- /dev/null +++ b/examples/with-sightmap-webmcp/next.config.mjs @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // Nothing Sightmap-specific is needed here. The corpus and the compiled tool + // layer are plain static files under public/.well-known/, written by + // `sightmap-next build` (wired as the `prebuild` script). +}; +export default nextConfig; diff --git a/examples/with-sightmap-webmcp/package.json b/examples/with-sightmap-webmcp/package.json new file mode 100644 index 0000000..0f8656f --- /dev/null +++ b/examples/with-sightmap-webmcp/package.json @@ -0,0 +1,31 @@ +{ + "name": "with-sightmap-webmcp", + "version": "0.0.0", + "private": true, + "description": "A Next.js app whose .sightmap/ corpus compiles into WebMCP tools that agent-browser, ChatGPT's browser, and Chrome can call by name.", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "sightmap:seed": "sightmap-next seed", + "sightmap:validate": "sightmap validate && sightkick build . -o /dev/null", + "sightmap:build": "sightmap-next build", + "prebuild": "sightmap-next build", + "test:plans": "sightmap-next run-plan plans/add-task.plan.json plans/triage.plan.json plans/filter.plan.json" + }, + "dependencies": { + "@sightmap/next": "file:../../packages/next", + "next": "16.3.4", + "react": "19.2.0", + "react-dom": "19.2.0" + }, + "devDependencies": { + "@sightmap/sightkick": "0.5.0", + "@sightmap/sightmap": "0.31.1", + "@types/node": "^24", + "@types/react": "^19", + "@types/react-dom": "^19", + "agent-browser": "0.36.0", + "typescript": "^5" + } +} diff --git a/examples/with-sightmap-webmcp/plans/add-task.plan.json b/examples/with-sightmap-webmcp/plans/add-task.plan.json new file mode 100644 index 0000000..aea1448 --- /dev/null +++ b/examples/with-sightmap-webmcp/plans/add-task.plan.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "app": "..", + "start": "/", + "scenario": { + "feature": "features/add-task.feature", + "name": "A new task appears on the board, not yet done", + "hash": "sha256:7b2be2807bdf5b8dd2c021a9389adaf0dd9544f9ed9ec0124329c2a488157dd4" + }, + "steps": [ + { + "gherkin": "When I add a task \"Ship the proof of concept\"", + "tool": "add_task", + "params": { + "title": "Ship the proof of concept" + }, + "expect": { + "ok": true + } + }, + { + "gherkin": "Then the board lists \"Ship the proof of concept\" / And it is not done", + "tool": "list_tasks", + "expect": { + "ok": true, + "list": { + "contains": { + "title": "Ship the proof of concept", + "done": "false" + } + } + } + } + ], + "irHash": "sha256:ba78eddf61adb6c70b63a9638c2ed91cc659fac8e4bd84d92d22223df9ec1ebb", + "generatedAt": "2026-09-05T13:23:22.079Z" +} diff --git a/examples/with-sightmap-webmcp/plans/filter.plan.json b/examples/with-sightmap-webmcp/plans/filter.plan.json new file mode 100644 index 0000000..ad17ae1 --- /dev/null +++ b/examples/with-sightmap-webmcp/plans/filter.plan.json @@ -0,0 +1,52 @@ +{ + "version": 1, + "app": "..", + "start": "/", + "scenario": { + "feature": "features/filter.feature", + "name": "Only done tasks under the Done filter", + "hash": "sha256:5010331843e77346d6460960535721287ebcd5cc08be4a21e76565774bba3dbe" + }, + "steps": [ + { + "gherkin": "When I show only Done tasks / Then \"Ship to Vercel\" is listed / And \"Write the sightmap\" is not listed", + "tool": "set_filter", + "params": { + "filter": "Done" + }, + "expect": { + "ok": true, + "list": { + "length": 1, + "contains": { + "title": "Ship to Vercel" + }, + "excludes": { + "title": "Write the sightmap" + } + } + } + }, + { + "gherkin": "When I show only Active tasks / Then \"Write the sightmap\" is listed / And \"Ship to Vercel\" is not listed", + "tool": "set_filter", + "params": { + "filter": "Active" + }, + "expect": { + "ok": true, + "list": { + "length": 2, + "contains": { + "title": "Write the sightmap" + }, + "excludes": { + "title": "Ship to Vercel" + } + } + } + } + ], + "irHash": "sha256:ba78eddf61adb6c70b63a9638c2ed91cc659fac8e4bd84d92d22223df9ec1ebb", + "generatedAt": "2026-09-05T13:23:22.081Z" +} diff --git a/examples/with-sightmap-webmcp/plans/triage.plan.json b/examples/with-sightmap-webmcp/plans/triage.plan.json new file mode 100644 index 0000000..fef4226 --- /dev/null +++ b/examples/with-sightmap-webmcp/plans/triage.plan.json @@ -0,0 +1,74 @@ +{ + "version": 1, + "app": "..", + "start": "/", + "scenario": { + "feature": "features/triage.feature", + "name": "Complete a task from its detail page", + "hash": "sha256:5641ab00fd588f8680d8dcb23e6adfdb388248273369a1fdded45274c42dd2e2" + }, + "steps": [ + { + "gherkin": "When I open \"Write the sightmap\"", + "tool": "open_task", + "params": { + "title": "Write the sightmap" + }, + "expect": { + "ok": true + } + }, + { + "gherkin": "Then the task page shows it as \"Active\"", + "tool": "read_task", + "expect": { + "ok": true, + "value": { + "equals": "Active" + } + } + }, + { + "gherkin": "When I mark it done", + "tool": "mark_done", + "expect": { + "ok": true + } + }, + { + "gherkin": "Then the task page shows it as \"Done\"", + "tool": "read_task", + "expect": { + "ok": true, + "value": { + "equals": "Done" + } + } + }, + { + "gherkin": "When I go back to the board", + "tool": "back_to_board", + "expect": { + "ok": true + } + }, + { + "gherkin": "And I show only Done tasks / Then \"Write the sightmap\" is listed as done", + "tool": "set_filter", + "params": { + "filter": "Done" + }, + "expect": { + "ok": true, + "list": { + "contains": { + "title": "Write the sightmap", + "done": "true" + } + } + } + } + ], + "irHash": "sha256:ba78eddf61adb6c70b63a9638c2ed91cc659fac8e4bd84d92d22223df9ec1ebb", + "generatedAt": "2026-09-05T13:23:22.081Z" +} diff --git a/examples/with-sightmap-webmcp/public/.well-known/sightkick.json b/examples/with-sightmap-webmcp/public/.well-known/sightkick.json new file mode 100644 index 0000000..46b4ef4 --- /dev/null +++ b/examples/with-sightmap-webmcp/public/.well-known/sightkick.json @@ -0,0 +1,768 @@ +{ + "version": 1, + "name": "tasks", + "views": [ + { + "name": "About", + "route": "/about" + }, + { + "name": "TaskBoard", + "route": "/" + }, + { + "name": "TaskDetail", + "route": "/tasks/:id" + } + ], + "tools": [ + { + "name": "list_tasks", + "description": "List the tasks currently shown on the board, with their done state. Returns `items`: a list of objects with keys {done, title}. One row per visible task.", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": {} + }, + "ensureView": { + "view": "TaskBoard", + "route": "/" + }, + "steps": [], + "returns": { + "description": "One row per visible task.", + "kind": "list", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ] + } + ] + }, + "fields": { + "done": { + "property": "done", + "extractor": { + "kind": "attr", + "attr": "data-done" + } + }, + "title": { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + } + } + } + } + }, + { + "name": "add_task", + "description": "Add a task to the board by title. Idempotent — an existing title is left alone. Returns `value` (string): The done state of the row just added (\"false\").", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The task title." + } + }, + "required": [ + "title" + ] + }, + "ensureView": { + "view": "TaskBoard", + "route": "/" + }, + "guard": { + "kind": "present", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + } + ] + } + ] + } + }, + "steps": [ + { + "op": "fill", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"NewTaskInput\"]" + ] + } + ] + }, + "value": "{{title}}" + }, + { + "op": "click", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"AddTaskButton\"]" + ] + } + ] + } + }, + { + "op": "waitFor", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + } + ] + } + ] + }, + "timeoutMs": 5000 + } + ], + "returns": { + "description": "The done state of the row just added (\"false\").", + "kind": "value", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + } + ] + } + ] + }, + "extractor": { + "kind": "attr", + "attr": "data-done" + } + }, + "guidance": [ + { + "tool": "list_tasks", + "reason": "confirm the task you just added is listed", + "when": "now" + } + ] + }, + { + "name": "complete_task", + "description": "Mark a task done from the board by clicking its toggle. Idempotent. Returns `value`: a single string.", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ] + }, + "ensureView": { + "view": "TaskBoard", + "route": "/" + }, + "guard": { + "kind": "present", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + }, + { + "property": "done", + "extractor": { + "kind": "attr", + "attr": "data-done" + }, + "op": "=", + "value": "true" + } + ] + } + ] + } + }, + "steps": [ + { + "op": "click", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + } + ] + }, + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"TaskToggle\"]" + ] + } + ] + } + }, + { + "op": "waitFor", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + }, + { + "property": "done", + "extractor": { + "kind": "attr", + "attr": "data-done" + }, + "op": "=", + "value": "true" + } + ] + } + ] + }, + "timeoutMs": 5000 + } + ], + "returns": { + "kind": "value", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + } + ] + } + ] + }, + "extractor": { + "kind": "attr", + "attr": "data-done" + } + } + }, + { + "name": "delete_task", + "description": "Delete a task from the board by title, then return the remaining rows. Returns `items`: a list of objects with keys {done, title}. The rows left on the board.", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ] + }, + "ensureView": { + "view": "TaskBoard", + "route": "/" + }, + "steps": [ + { + "op": "click", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + } + ] + }, + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"DeleteTaskButton\"]" + ] + } + ] + } + } + ], + "returns": { + "description": "The rows left on the board.", + "kind": "list", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ] + } + ] + }, + "fields": { + "done": { + "property": "done", + "extractor": { + "kind": "attr", + "attr": "data-done" + } + }, + "title": { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + } + } + } + } + }, + { + "name": "set_filter", + "description": "Show All, Active, or Done tasks, and return the rows now visible. Returns `items`: a list of objects with keys {done, title}.", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": { + "filter": { + "type": "string", + "enum": [ + "All", + "Active", + "Done" + ] + } + }, + "required": [ + "filter" + ] + }, + "ensureView": { + "view": "TaskBoard", + "route": "/" + }, + "steps": [ + { + "op": "click", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"FilterBar\"] [data-component=\"FilterButton\"]" + ], + "preds": [ + { + "property": "label", + "extractor": { + "kind": "text" + }, + "op": "=", + "value": "{{filter}}" + } + ] + } + ] + } + }, + { + "op": "waitFor", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"FilterBar\"] [data-component=\"FilterButton\"]" + ], + "preds": [ + { + "property": "label", + "extractor": { + "kind": "text" + }, + "op": "=", + "value": "{{filter}}" + }, + { + "property": "active", + "extractor": { + "kind": "attr", + "attr": "aria-pressed" + }, + "op": "=", + "value": "true" + } + ] + } + ] + }, + "timeoutMs": 5000 + } + ], + "returns": { + "kind": "list", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ] + } + ] + }, + "fields": { + "done": { + "property": "done", + "extractor": { + "kind": "attr", + "attr": "data-done" + } + }, + "title": { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + } + } + } + } + }, + { + "name": "open_task", + "description": "Open a task's detail page from the board. Navigates to /tasks/:id.", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ] + }, + "ensureView": { + "view": "TaskBoard", + "route": "/" + }, + "steps": [ + { + "op": "click", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "preds": [ + { + "property": "title", + "extractor": { + "kind": "text", + "within": "[data-component=\"TaskLink\"]" + }, + "op": "=", + "value": "{{title}}" + } + ] + }, + { + "locators": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"TaskLink\"]" + ] + } + ] + } + }, + { + "op": "waitFor", + "view": "TaskDetail", + "route": "/tasks/:id", + "timeoutMs": 5000 + } + ], + "guidance": [ + { + "tool": "read_task", + "reason": "confirm you opened the right task", + "when": "after_navigation", + "view": "TaskDetail" + } + ] + }, + { + "name": "read_task", + "description": "Read the open task's title and status from its detail page. Returns `value` (string): The status shown on the page (\"Active\" or \"Done\").", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": {} + }, + "ensureView": { + "view": "TaskDetail", + "route": "/tasks/:id" + }, + "steps": [], + "returns": { + "description": "The status shown on the page (\"Active\" or \"Done\").", + "kind": "value", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskDetail\"]" + ] + } + ] + }, + "extractor": { + "kind": "attr", + "attr": "data-status" + } + }, + "guidance": [ + { + "tool": "mark_done", + "reason": "finish it", + "when": "now" + } + ] + }, + { + "name": "mark_done", + "description": "Mark the open task done from its detail page. Idempotent. Returns `value`: a single string.", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": {} + }, + "ensureView": { + "view": "TaskDetail", + "route": "/tasks/:id" + }, + "guard": { + "kind": "present", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskDetail\"]" + ], + "preds": [ + { + "property": "status", + "extractor": { + "kind": "attr", + "attr": "data-status" + }, + "op": "=", + "value": "Done" + } + ] + } + ] + } + }, + "steps": [ + { + "op": "click", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskDetail\"] [data-component=\"MarkDoneButton\"]" + ] + } + ] + } + }, + { + "op": "waitFor", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskDetail\"]" + ], + "preds": [ + { + "property": "status", + "extractor": { + "kind": "attr", + "attr": "data-status" + }, + "op": "=", + "value": "Done" + } + ] + } + ] + }, + "timeoutMs": 5000 + } + ], + "returns": { + "kind": "value", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskDetail\"]" + ] + } + ] + }, + "extractor": { + "kind": "attr", + "attr": "data-status" + } + }, + "guidance": [ + { + "tool": "back_to_board", + "reason": "return to the board", + "when": "now" + } + ] + }, + { + "name": "back_to_board", + "description": "Return from a task's detail page to the board.", + "mode": "live", + "inputSchema": { + "type": "object", + "properties": {} + }, + "ensureView": { + "view": "TaskDetail", + "route": "/tasks/:id" + }, + "steps": [ + { + "op": "click", + "query": { + "parts": [ + { + "locators": [ + "[data-component=\"TaskDetail\"]" + ] + }, + { + "locators": [ + "[data-component=\"TaskDetail\"] [data-component=\"BackToBoardLink\"]" + ] + } + ] + } + }, + { + "op": "waitFor", + "view": "TaskBoard", + "route": "/", + "timeoutMs": 5000 + } + ], + "guidance": [ + { + "tool": "set_filter", + "reason": "switch to Done to see it moved", + "when": "after_navigation", + "view": "TaskBoard" + } + ] + } + ] +} diff --git a/examples/with-sightmap-webmcp/public/.well-known/sightmap.json b/examples/with-sightmap-webmcp/public/.well-known/sightmap.json new file mode 100644 index 0000000..4a10649 --- /dev/null +++ b/examples/with-sightmap-webmcp/public/.well-known/sightmap.json @@ -0,0 +1,284 @@ +{ + "globals": [ + { + "name": "SiteNav", + "selectors": [ + "[data-component=\"SiteNav\"]" + ], + "source": "app/layout.tsx" + }, + { + "name": "NavLink", + "selectors": [ + "[data-component=\"SiteNav\"] [data-component=\"NavLink\"]" + ], + "properties": [ + { + "name": "label", + "extract": "text" + } + ], + "parentChain": [ + "SiteNav" + ] + }, + { + "name": "RouteAnnouncer", + "selectors": [ + "#__next-route-announcer__" + ] + } + ], + "views": [ + { + "name": "About", + "route": "/about", + "components": [ + { + "name": "AboutText", + "selectors": [ + "[data-component=\"AboutText\"]" + ] + } + ] + }, + { + "name": "TaskBoard", + "route": "/", + "memory": [ + "State is in-memory per tab and resets on a full page load; every fresh open shows the same three seed tasks", + "Adding submits a form, so Enter in the input works as well as the button", + "Filter labels are All / Active / Done (not \"Completed\")" + ], + "components": [ + { + "name": "TaskBoard", + "selectors": [ + "[data-component=\"TaskBoard\"]" + ], + "source": "components/TaskBoard.tsx" + }, + { + "name": "NewTaskInput", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"NewTaskInput\"]" + ], + "parentChain": [ + "TaskBoard" + ] + }, + { + "name": "AddTaskButton", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"AddTaskButton\"]" + ], + "parentChain": [ + "TaskBoard" + ] + }, + { + "name": "FilterBar", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"FilterBar\"]" + ], + "parentChain": [ + "TaskBoard" + ] + }, + { + "name": "FilterButton", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"FilterBar\"] [data-component=\"FilterButton\"]" + ], + "properties": [ + { + "name": "label", + "extract": "text" + }, + { + "name": "active", + "extract": "attr=aria-pressed" + } + ], + "parentChain": [ + "TaskBoard", + "FilterBar" + ] + }, + { + "name": "TaskList", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskList\"]" + ], + "parentChain": [ + "TaskBoard" + ] + }, + { + "name": "TaskRow", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]" + ], + "properties": [ + { + "name": "title", + "extract": "TaskLink.text" + }, + { + "name": "done", + "extract": "attr=data-done" + }, + { + "name": "id", + "extract": "attr=data-id" + } + ], + "parentChain": [ + "TaskBoard" + ] + }, + { + "name": "TaskToggle", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"TaskToggle\"]" + ], + "properties": [ + { + "name": "label", + "extract": "text" + } + ], + "parentChain": [ + "TaskBoard", + "TaskRow" + ] + }, + { + "name": "TaskLink", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"TaskLink\"]" + ], + "properties": [ + { + "name": "text", + "extract": "text" + } + ], + "parentChain": [ + "TaskBoard", + "TaskRow" + ] + }, + { + "name": "DeleteTaskButton", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"DeleteTaskButton\"]" + ], + "parentChain": [ + "TaskBoard", + "TaskRow" + ] + }, + { + "name": "EmptyState", + "selectors": [ + "[data-component=\"TaskBoard\"] [data-component=\"EmptyState\"]" + ], + "parentChain": [ + "TaskBoard" + ] + } + ] + }, + { + "name": "TaskDetail", + "route": "/tasks/:id", + "memory": [ + "Reached by client-side navigation from the board; a full load of /tasks/:id shows the seed state, so ids above 3 render TaskMissing", + "The Mark done button is disabled once the task is done, so a second click is a no-op rather than a toggle" + ], + "components": [ + { + "name": "TaskDetail", + "selectors": [ + "[data-component=\"TaskDetail\"]" + ], + "source": "components/TaskDetail.tsx", + "properties": [ + { + "name": "title", + "extract": "TaskTitle.text" + }, + { + "name": "status", + "extract": "attr=data-status" + } + ] + }, + { + "name": "BackToBoardLink", + "selectors": [ + "[data-component=\"TaskDetail\"] [data-component=\"BackToBoardLink\"]" + ], + "parentChain": [ + "TaskDetail" + ] + }, + { + "name": "TaskTitle", + "selectors": [ + "[data-component=\"TaskDetail\"] [data-component=\"TaskTitle\"]" + ], + "properties": [ + { + "name": "text", + "extract": "text" + } + ], + "parentChain": [ + "TaskDetail" + ] + }, + { + "name": "StatusBadge", + "selectors": [ + "[data-component=\"TaskDetail\"] [data-component=\"StatusBadge\"]" + ], + "properties": [ + { + "name": "label", + "extract": "text" + } + ], + "parentChain": [ + "TaskDetail" + ] + }, + { + "name": "MarkDoneButton", + "selectors": [ + "[data-component=\"TaskDetail\"] [data-component=\"MarkDoneButton\"]" + ], + "parentChain": [ + "TaskDetail" + ] + }, + { + "name": "TaskMissing", + "selectors": [ + "[data-component=\"TaskMissing\"]" + ] + }, + { + "name": "BackToBoardLink", + "selectors": [ + "[data-component=\"TaskMissing\"] [data-component=\"BackToBoardLink\"]" + ], + "parentChain": [ + "TaskMissing" + ] + } + ] + } + ] +} diff --git a/examples/with-sightmap-webmcp/public/sightkick-runtime.js b/examples/with-sightmap-webmcp/public/sightkick-runtime.js new file mode 100644 index 0000000..f089695 --- /dev/null +++ b/examples/with-sightmap-webmcp/public/sightkick-runtime.js @@ -0,0 +1,571 @@ +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/dom.ts + function deepQueryAll(selector, root = document) { + const out = []; + const seen = /* @__PURE__ */ new Set(); + const collect = (r) => { + let matches = []; + try { + matches = Array.from(r.querySelectorAll(selector)); + } catch { + return; + } + for (const el of matches) { + if (!seen.has(el)) { + seen.add(el); + out.push(el); + } + } + const all = r.querySelectorAll("*"); + for (const el of all) { + const sr = el.shadowRoot; + if (sr) collect(sr); + } + }; + collect(root); + return out; + } + function accessibleText(el) { + const labelledby = el.getAttribute?.("aria-labelledby"); + if (labelledby) { + const doc = el.ownerDocument; + const text = labelledby.split(/\s+/).map((id) => doc?.getElementById(id)?.textContent?.trim() ?? "").filter(Boolean).join(" "); + if (text) return text; + } + const aria = el.getAttribute?.("aria-label"); + if (aria != null && aria.trim() !== "") return aria.trim(); + const labels = el.labels; + if (labels && labels.length) { + const text = Array.from(labels).map((l) => l.textContent?.trim() ?? "").filter(Boolean).join(" "); + if (text) return text; + } + const alt = el.getAttribute?.("alt"); + if (alt != null && alt.trim() !== "") return alt.trim(); + const inner = el.innerText; + if (typeof inner === "string" && inner.trim() !== "") return inner.trim(); + return (el.textContent ?? "").trim(); + } + function extract(el, ex) { + const target = ex.within ? el.querySelector(ex.within) : el; + if (ex.kind === "exists") { + return el.querySelector(ex.within ?? "*") ? "true" : "false"; + } + if (!target) return ""; + switch (ex.kind) { + case "attr": + return ex.attr ? target.getAttribute(ex.attr) ?? "" : ""; + case "text": + default: + return accessibleText(target); + } + } + function matchPred(el, pred, args) { + let a = extract(el, pred.extractor); + let b = interpolate(pred.value, args); + if (pred.ci) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + switch (pred.op) { + case "^=": + return a.startsWith(b); + case "*=": + return a.includes(b); + default: + return a === b; + } + } + function resolvePath(path, args) { + let scopes = [document]; + let matched = []; + for (const part of path) { + const found = []; + const seen = /* @__PURE__ */ new Set(); + for (const root of scopes) { + for (const loc of part.locators) { + for (const el of deepQueryAll(loc, root)) { + if (seen.has(el)) continue; + if ((part.preds ?? []).every((p) => matchPred(el, p, args))) { + seen.add(el); + found.push(el); + } + } + } + } + matched = found; + scopes = found; + } + return matched; + } + function resolveQuery(query, args) { + const all = resolvePath(query.parts, args); + if (query.index == null) return all; + const el = all[query.index]; + return el ? [el] : []; + } + function setElementValue(el, value) { + const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const desc = Object.getOwnPropertyDescriptor(proto, "value"); + if (desc?.set) { + desc.set.call(el, value); + } else { + el.value = value; + } + } + function makeInputEvent(inputType, data) { + if (typeof InputEvent !== "undefined") { + return new InputEvent("input", { bubbles: true, inputType, data: data ?? void 0 }); + } + return new Event("input", { bubbles: true }); + } + function typeInto(el, value) { + const target = el; + target.focus?.(); + setElementValue(el, ""); + target.dispatchEvent(makeInputEvent("deleteContentBackward", null)); + let acc = ""; + for (const ch of value) { + target.dispatchEvent(new KeyboardEvent("keydown", { key: ch, bubbles: true, cancelable: true })); + acc += ch; + setElementValue(el, acc); + target.dispatchEvent(makeInputEvent("insertText", ch)); + target.dispatchEvent(new KeyboardEvent("keyup", { key: ch, bubbles: true })); + } + target.dispatchEvent(new Event("change", { bubbles: true })); + if (el.getAttribute("role") === "combobox") { + target.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true })); + target.dispatchEvent(new KeyboardEvent("keyup", { key: "ArrowDown", bubbles: true })); + } + } + function clickElement(el) { + const t = el; + const r = t.getBoundingClientRect?.(); + const clientX = r ? Math.round(r.left + r.width / 2) : 0; + const clientY = r ? Math.round(r.top + r.height / 2) : 0; + const init = (buttons) => ({ + bubbles: true, + cancelable: true, + composed: true, + clientX, + clientY, + button: 0, + buttons + }); + const hasPE = typeof PointerEvent !== "undefined"; + const emit = (type, buttons, pointer) => { + if (pointer && hasPE) { + t.dispatchEvent( + new PointerEvent(type, { ...init(buttons), pointerId: 1, pointerType: "mouse", isPrimary: true }) + ); + } else { + t.dispatchEvent(new MouseEvent(type, init(buttons))); + } + }; + emit("pointerdown", 1, true); + emit("mousedown", 1, false); + emit("pointerup", 0, true); + emit("mouseup", 0, false); + t.click(); + } + function interpolate(template, args) { + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => { + const v = args[key]; + return v == null ? "" : String(v); + }); + } + + // src/executor.ts + var sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + function resolveOptions(options = {}) { + return { + pollMs: options.pollMs ?? 100, + currentPath: options.currentPath ?? (typeof window !== "undefined" ? window.location.pathname : "/"), + log: options.log ?? ((m) => console.warn(`[sightkick] ${m}`)), + signal: options.signal + }; + } + function routeMatches(pattern, path) { + const norm = (p) => { + const bare = p.split("#")[0].split("?")[0]; + const trimmed = bare.length > 1 && bare.endsWith("/") ? bare.slice(0, -1) : bare; + return trimmed || "/"; + }; + const pat = norm(pattern); + const pth = norm(path); + if (pat === "/") return pth === "/"; + const segs = pat.split("/").filter((s) => s.length > 0); + const rx = "^" + segs.map((seg) => { + if (seg === "**") return "(?:/.+)?"; + if (seg === "*" || seg.startsWith(":")) return "/[^/]+"; + return "/" + seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }).join("") + "$"; + return new RegExp(rx).test(pth); + } + function guardHolds(guard, args) { + const matches = resolveQuery(guard.query, args).length; + return guard.kind === "present" ? matches > 0 : matches === 0; + } + function describeTarget(step) { + const parts = step.query?.parts ?? []; + return `query ${JSON.stringify(parts.map((p) => p.locators.join("|")))}`; + } + function isActionable(el) { + const h = el; + if (typeof h.getBoundingClientRect !== "function") return false; + const style = typeof getComputedStyle === "function" ? getComputedStyle(h) : null; + if (h.offsetParent === null && style?.position !== "fixed") return false; + const r = h.getBoundingClientRect(); + return r.width > 0 && r.height > 0; + } + async function runStep(step, args, opts) { + const target = () => { + if (!step.query) return void 0; + const matches = resolveQuery(step.query, args); + return matches.find(isActionable) ?? matches[0]; + }; + switch (step.op) { + case "navigate": { + const path = opts.currentPath; + if (step.route && !routeMatches(step.route, path)) { + opts.log(`navigate: single-page slice cannot leave ${path} for ${step.route} (deferred to journey work)`); + } + return; + } + case "goto": { + const url = interpolate(step.url ?? "", args); + if (url && typeof window !== "undefined") { + setTimeout(() => window.location.assign(url), 0); + } + return; + } + case "fill": { + const el = target(); + if (!el) throw new Error(`fill: no element for ${describeTarget(step)}`); + typeInto(el, interpolate(step.value ?? "", args)); + return; + } + case "click": { + const el = target(); + if (!el) throw new Error(`click: no element for ${describeTarget(step)}`); + clickElement(el); + return; + } + case "keypress": { + const key = step.key ?? ""; + if (!key) throw new Error("keypress: no key given"); + const el = document.activeElement ?? document.body; + el.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true })); + el.dispatchEvent(new KeyboardEvent("keyup", { key, bubbles: true })); + return; + } + case "waitFor": { + const routeSatisfied = () => !!step.route && routeMatches(step.route, typeof window !== "undefined" ? window.location.pathname : opts.currentPath); + const satisfied = step.query ? () => !!target() : routeSatisfied; + const deadline = Date.now() + (step.timeoutMs ?? 5e3); + for (; ; ) { + if (opts.signal?.aborted) throw new Error("aborted"); + if (satisfied()) return; + if (Date.now() >= deadline) { + const what = step.query ? describeTarget(step) : `route ${step.route}`; + throw new Error(`waitFor: timed out after ${step.timeoutMs ?? 5e3}ms for ${what}`); + } + await sleep(opts.pollMs); + } + } + default: + throw new Error(`unknown step op ${step.op}`); + } + } + function extractFields(el, fields) { + const obj = {}; + for (const [name, f] of Object.entries(fields)) { + obj[name] = extract(el, f.extractor); + } + return obj; + } + function computeReturn(ret, args) { + if (ret.kind === "list") { + const rows = ret.query ? resolveQuery(ret.query, args) : []; + const fields = ret.fields ?? {}; + return { ok: true, items: rows.map((el2) => extractFields(el2, fields)) }; + } + const el = ret.query ? resolveQuery(ret.query, args)[0] : void 0; + const value = el && ret.extractor ? extract(el, ret.extractor) : void 0; + const out = { ok: true }; + if (value !== void 0) out.value = value; + return out; + } + async function runTool(tool, args = {}, options = {}) { + const opts = resolveOptions(options); + if (tool.ensureView && !routeMatches(tool.ensureView.route, opts.currentPath)) { + opts.log(`ensure_view: "${tool.name}" expects ${tool.ensureView.view} (${tool.ensureView.route}) but path is ${opts.currentPath}; proceeding best-effort`); + } + if (tool.guard && guardHolds(tool.guard, args)) { + const skipped = tool.returns ? { ...computeReturn(tool.returns, args), skipped: true } : { ok: true, skipped: true }; + skipped.message = "guard satisfied; steps skipped (already applied)"; + if (tool.guidance && tool.guidance.length) skipped.guidance = tool.guidance; + return skipped; + } + try { + for (const step of tool.steps) { + await runStep(step, args, opts); + } + } catch (err) { + return { ok: false, message: err.message }; + } + const result = tool.returns ? computeReturn(tool.returns, args) : { ok: true }; + if (tool.guidance && tool.guidance.length) result.guidance = tool.guidance; + return result; + } + + // src/webmcp.ts + var POLYFILL_FLAG = "__sightkickPolyfill"; + var _a, _b; + var ModelContextPolyfill = class extends (_b = EventTarget, _a = POLYFILL_FLAG, _b) { + constructor() { + super(...arguments); + __publicField(this, _a, true); + __publicField(this, "tools", /* @__PURE__ */ new Map()); + } + registerTool(def, options) { + this.tools.set(def.name, def); + if (options?.signal) { + options.signal.addEventListener( + "abort", + () => { + if (this.tools.get(def.name) === def) { + this.tools.delete(def.name); + this.dispatchEvent(new Event("toolchange")); + } + }, + { once: true } + ); + } + this.dispatchEvent(new Event("toolchange")); + return Promise.resolve(); + } + getTools() { + const origin = typeof location !== "undefined" ? location.origin : "null"; + return Promise.resolve( + [...this.tools.values()].map((t) => ({ + name: t.name, + description: t.description, + inputSchema: t.inputSchema, + origin + })) + ); + } + executeTool(tool, args = {}, options) { + const def = this.tools.get(tool.name); + if (!def) { + return Promise.reject(new Error(`unknown tool "${tool.name}"`)); + } + return Promise.resolve(def.execute(args, options)); + } + }; + function ensureModelContext() { + if (typeof document === "undefined") return void 0; + const doc = document; + if (doc.modelContext) return doc.modelContext; + const poly = new ModelContextPolyfill(); + Object.defineProperty(doc, "modelContext", { value: poly, configurable: true, writable: true }); + return poly; + } + function isPolyfilled(ctx) { + return !!ctx && ctx[POLYFILL_FLAG] === true; + } + + // src/errors.ts + function describeError(e) { + if (e instanceof Error) return `${e.name}: ${e.message}`; + if (typeof e === "object" && e !== null) { + const anyE = e; + if (anyE.message != null || anyE.name != null) { + return `${String(anyE.name ?? "Error")}: ${String(anyE.message ?? "")}`.trim(); + } + try { + const s = JSON.stringify(e); + if (s && s !== "{}") return s; + } catch { + } + return Object.prototype.toString.call(e); + } + return String(e); + } + + // src/boot.ts + function detectMode() { + return typeof window !== "undefined" && window.__sightkick_host != null ? "injected" : "direct"; + } + function findTool(ir, name) { + return ir?.tools.find((t) => t.name === name); + } + function toEnvelope(result) { + return { content: [{ type: "text", text: JSON.stringify(result) }], isError: !result.ok }; + } + var historyPatched = false; + function patchHistory() { + if (historyPatched || typeof history === "undefined" || typeof window === "undefined") return; + historyPatched = true; + const wrap = (orig) => function(data, unused, url) { + const r = orig.call(this, data, unused, url); + window.dispatchEvent(new Event("sightkick:navigate")); + return r; + }; + history.pushState = wrap(history.pushState.bind(history)); + history.replaceState = wrap(history.replaceState.bind(history)); + } + function boot(initial, opts = {}) { + const ctx = ensureModelContext(); + const currentPath = () => opts.currentPath ?? (typeof window !== "undefined" ? window.location.pathname : "/"); + let registrations = []; + let registered = []; + const unregisterAll = () => { + for (const c of registrations) c.abort(); + registrations = []; + registered = []; + }; + const refresh = () => { + unregisterAll(); + const ir = api.ir; + if (!ir || !ctx) return; + const path = currentPath(); + for (const tool of ir.tools) { + if (tool.ensureView && !routeMatches(tool.ensureView.route, path)) continue; + const controller = new AbortController(); + registrations.push(controller); + registered.push({ name: tool.name, description: tool.description }); + Promise.resolve( + ctx.registerTool( + { + name: tool.name, + description: tool.description ?? "", + inputSchema: tool.inputSchema, + execute: async (args, options) => toEnvelope(await runTool(tool, args, { signal: options?.signal, currentPath: path })) + }, + { signal: controller.signal } + ) + ).catch((e) => console.warn(`[sightkick] registerTool "${tool.name}" rejected: ${describeError(e)}`)); + } + }; + const api = { + mode: detectMode(), + ir: null, + modelContext: ctx, + polyfilled: isPolyfilled(ctx), + load(ir) { + this.ir = ir; + refresh(); + console.info( + `[sightkick] loaded IR "${ir.name}" (${ir.tools.length} tools, ${this.mode}, ${this.polyfilled ? "polyfilled" : "native"} modelContext)` + ); + }, + tools() { + return registered.slice(); + }, + refresh, + call(name, args = {}, options) { + const tool = findTool(this.ir, name); + if (!tool) return Promise.resolve({ ok: false, message: `unknown tool "${name}"` }); + return runTool(tool, args, options); + } + }; + if (typeof window !== "undefined" && opts.currentPath === void 0) { + let lastPath = currentPath(); + const onNav = () => { + const p = currentPath(); + if (p !== lastPath) { + lastPath = p; + refresh(); + } + }; + window.addEventListener("popstate", onNav); + window.addEventListener("sightkick:navigate", onNav); + patchHistory(); + } + if (initial) api.load(initial); + return api; + } + + // src/channel.ts + var IR_ATTR = "data-sightkick-ir"; + var HOST_ATTR = "data-sightkick-host"; + var IR_EVENT = "sightkick:ir"; + function loadFromDom(api) { + if (typeof document === "undefined" || !document.documentElement) return false; + const de = document.documentElement; + const raw = de.getAttribute(IR_ATTR); + if (!raw) return false; + const host = de.getAttribute(HOST_ATTR); + de.removeAttribute(IR_ATTR); + de.removeAttribute(HOST_ATTR); + try { + const ir = JSON.parse(raw); + if (host) { + try { + window.__sightkick_host = JSON.parse(host); + api.mode = "injected"; + } catch { + } + } + api.load(ir); + return true; + } catch (e) { + console.warn("[sightkick] IR channel: bad payload", e); + return false; + } + } + function installIrChannel(api) { + if (typeof document === "undefined") return; + if (loadFromDom(api)) return; + let tries = 0; + const poll = setInterval(() => { + if (loadFromDom(api) || ++tries > 40) clearInterval(poll); + }, 50); + document.addEventListener( + IR_EVENT, + () => { + if (loadFromDom(api)) clearInterval(poll); + }, + { once: true } + ); + } + + // src/client.ts + function createClient(ctx = ensureModelContext()) { + if (!ctx) throw new Error("createClient: no document.modelContext available"); + return { + async listTools() { + try { + return await ctx.getTools(); + } catch (e) { + throw new Error(`getTools failed: ${describeError(e)}`); + } + }, + async callTool(name, args = {}, options) { + const tools = await ctx.getTools(); + const tool = tools.find((t) => t.name === name); + if (!tool) throw new Error(`unknown tool "${name}"`); + let raw; + try { + raw = await ctx.executeTool(tool, args, options); + } catch (e) { + throw new Error(`executeTool "${name}" failed: ${describeError(e)}`); + } + return typeof raw === "string" ? JSON.parse(raw) : raw; + } + }; + } + + // src/index.ts + if (typeof window !== "undefined") { + const api = boot(window.__sightkick_ir); + window.__sightkick = api; + if (!window.__sightkick_ir) installIrChannel(api); + } +})(); diff --git a/examples/with-sightmap-webmcp/tsconfig.json b/examples/with-sightmap-webmcp/tsconfig.json new file mode 100644 index 0000000..705f5ce --- /dev/null +++ b/examples/with-sightmap-webmcp/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/examples/with-sightmap-webmcp/webmcp.init.js b/examples/with-sightmap-webmcp/webmcp.init.js new file mode 100644 index 0000000..680836d --- /dev/null +++ b/examples/with-sightmap-webmcp/webmcp.init.js @@ -0,0 +1,577 @@ +// Generated by sightmap-next build — do not edit. Sightkick runtime + compiled IR. +// Load before navigation: agent-browser --init-script ./webmcp.init.js open +// Then: agent-browser webmcp list +window.__sightkick_ir = {"version":1,"name":"tasks","views":[{"name":"About","route":"/about"},{"name":"TaskBoard","route":"/"},{"name":"TaskDetail","route":"/tasks/:id"}],"tools":[{"name":"list_tasks","description":"List the tasks currently shown on the board, with their done state. Returns `items`: a list of objects with keys {done, title}. One row per visible task.","mode":"live","inputSchema":{"type":"object","properties":{}},"ensureView":{"view":"TaskBoard","route":"/"},"steps":[],"returns":{"description":"One row per visible task.","kind":"list","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"]}]},"fields":{"done":{"property":"done","extractor":{"kind":"attr","attr":"data-done"}},"title":{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"}}}}},{"name":"add_task","description":"Add a task to the board by title. Idempotent — an existing title is left alone. Returns `value` (string): The done state of the row just added (\"false\").","mode":"live","inputSchema":{"type":"object","properties":{"title":{"type":"string","description":"The task title."}},"required":["title"]},"ensureView":{"view":"TaskBoard","route":"/"},"guard":{"kind":"present","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"}]}]}},"steps":[{"op":"fill","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"NewTaskInput\"]"]}]},"value":"{{title}}"},{"op":"click","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"AddTaskButton\"]"]}]}},{"op":"waitFor","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"}]}]},"timeoutMs":5000}],"returns":{"description":"The done state of the row just added (\"false\").","kind":"value","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"}]}]},"extractor":{"kind":"attr","attr":"data-done"}},"guidance":[{"tool":"list_tasks","reason":"confirm the task you just added is listed","when":"now"}]},{"name":"complete_task","description":"Mark a task done from the board by clicking its toggle. Idempotent. Returns `value`: a single string.","mode":"live","inputSchema":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"]},"ensureView":{"view":"TaskBoard","route":"/"},"guard":{"kind":"present","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"},{"property":"done","extractor":{"kind":"attr","attr":"data-done"},"op":"=","value":"true"}]}]}},"steps":[{"op":"click","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"}]},{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"TaskToggle\"]"]}]}},{"op":"waitFor","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"},{"property":"done","extractor":{"kind":"attr","attr":"data-done"},"op":"=","value":"true"}]}]},"timeoutMs":5000}],"returns":{"kind":"value","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"}]}]},"extractor":{"kind":"attr","attr":"data-done"}}},{"name":"delete_task","description":"Delete a task from the board by title, then return the remaining rows. Returns `items`: a list of objects with keys {done, title}. The rows left on the board.","mode":"live","inputSchema":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"]},"ensureView":{"view":"TaskBoard","route":"/"},"steps":[{"op":"click","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"}]},{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"DeleteTaskButton\"]"]}]}}],"returns":{"description":"The rows left on the board.","kind":"list","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"]}]},"fields":{"done":{"property":"done","extractor":{"kind":"attr","attr":"data-done"}},"title":{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"}}}}},{"name":"set_filter","description":"Show All, Active, or Done tasks, and return the rows now visible. Returns `items`: a list of objects with keys {done, title}.","mode":"live","inputSchema":{"type":"object","properties":{"filter":{"type":"string","enum":["All","Active","Done"]}},"required":["filter"]},"ensureView":{"view":"TaskBoard","route":"/"},"steps":[{"op":"click","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"FilterBar\"] [data-component=\"FilterButton\"]"],"preds":[{"property":"label","extractor":{"kind":"text"},"op":"=","value":"{{filter}}"}]}]}},{"op":"waitFor","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"FilterBar\"] [data-component=\"FilterButton\"]"],"preds":[{"property":"label","extractor":{"kind":"text"},"op":"=","value":"{{filter}}"},{"property":"active","extractor":{"kind":"attr","attr":"aria-pressed"},"op":"=","value":"true"}]}]},"timeoutMs":5000}],"returns":{"kind":"list","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"]}]},"fields":{"done":{"property":"done","extractor":{"kind":"attr","attr":"data-done"}},"title":{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"}}}}},{"name":"open_task","description":"Open a task's detail page from the board. Navigates to /tasks/:id.","mode":"live","inputSchema":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"]},"ensureView":{"view":"TaskBoard","route":"/"},"steps":[{"op":"click","query":{"parts":[{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"]"],"preds":[{"property":"title","extractor":{"kind":"text","within":"[data-component=\"TaskLink\"]"},"op":"=","value":"{{title}}"}]},{"locators":["[data-component=\"TaskBoard\"] [data-component=\"TaskRow\"] [data-component=\"TaskLink\"]"]}]}},{"op":"waitFor","view":"TaskDetail","route":"/tasks/:id","timeoutMs":5000}],"guidance":[{"tool":"read_task","reason":"confirm you opened the right task","when":"after_navigation","view":"TaskDetail"}]},{"name":"read_task","description":"Read the open task's title and status from its detail page. Returns `value` (string): The status shown on the page (\"Active\" or \"Done\").","mode":"live","inputSchema":{"type":"object","properties":{}},"ensureView":{"view":"TaskDetail","route":"/tasks/:id"},"steps":[],"returns":{"description":"The status shown on the page (\"Active\" or \"Done\").","kind":"value","query":{"parts":[{"locators":["[data-component=\"TaskDetail\"]"]}]},"extractor":{"kind":"attr","attr":"data-status"}},"guidance":[{"tool":"mark_done","reason":"finish it","when":"now"}]},{"name":"mark_done","description":"Mark the open task done from its detail page. Idempotent. Returns `value`: a single string.","mode":"live","inputSchema":{"type":"object","properties":{}},"ensureView":{"view":"TaskDetail","route":"/tasks/:id"},"guard":{"kind":"present","query":{"parts":[{"locators":["[data-component=\"TaskDetail\"]"],"preds":[{"property":"status","extractor":{"kind":"attr","attr":"data-status"},"op":"=","value":"Done"}]}]}},"steps":[{"op":"click","query":{"parts":[{"locators":["[data-component=\"TaskDetail\"] [data-component=\"MarkDoneButton\"]"]}]}},{"op":"waitFor","query":{"parts":[{"locators":["[data-component=\"TaskDetail\"]"],"preds":[{"property":"status","extractor":{"kind":"attr","attr":"data-status"},"op":"=","value":"Done"}]}]},"timeoutMs":5000}],"returns":{"kind":"value","query":{"parts":[{"locators":["[data-component=\"TaskDetail\"]"]}]},"extractor":{"kind":"attr","attr":"data-status"}},"guidance":[{"tool":"back_to_board","reason":"return to the board","when":"now"}]},{"name":"back_to_board","description":"Return from a task's detail page to the board.","mode":"live","inputSchema":{"type":"object","properties":{}},"ensureView":{"view":"TaskDetail","route":"/tasks/:id"},"steps":[{"op":"click","query":{"parts":[{"locators":["[data-component=\"TaskDetail\"]"]},{"locators":["[data-component=\"TaskDetail\"] [data-component=\"BackToBoardLink\"]"]}]}},{"op":"waitFor","view":"TaskBoard","route":"/","timeoutMs":5000}],"guidance":[{"tool":"set_filter","reason":"switch to Done to see it moved","when":"after_navigation","view":"TaskBoard"}]}]}; +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/dom.ts + function deepQueryAll(selector, root = document) { + const out = []; + const seen = /* @__PURE__ */ new Set(); + const collect = (r) => { + let matches = []; + try { + matches = Array.from(r.querySelectorAll(selector)); + } catch { + return; + } + for (const el of matches) { + if (!seen.has(el)) { + seen.add(el); + out.push(el); + } + } + const all = r.querySelectorAll("*"); + for (const el of all) { + const sr = el.shadowRoot; + if (sr) collect(sr); + } + }; + collect(root); + return out; + } + function accessibleText(el) { + const labelledby = el.getAttribute?.("aria-labelledby"); + if (labelledby) { + const doc = el.ownerDocument; + const text = labelledby.split(/\s+/).map((id) => doc?.getElementById(id)?.textContent?.trim() ?? "").filter(Boolean).join(" "); + if (text) return text; + } + const aria = el.getAttribute?.("aria-label"); + if (aria != null && aria.trim() !== "") return aria.trim(); + const labels = el.labels; + if (labels && labels.length) { + const text = Array.from(labels).map((l) => l.textContent?.trim() ?? "").filter(Boolean).join(" "); + if (text) return text; + } + const alt = el.getAttribute?.("alt"); + if (alt != null && alt.trim() !== "") return alt.trim(); + const inner = el.innerText; + if (typeof inner === "string" && inner.trim() !== "") return inner.trim(); + return (el.textContent ?? "").trim(); + } + function extract(el, ex) { + const target = ex.within ? el.querySelector(ex.within) : el; + if (ex.kind === "exists") { + return el.querySelector(ex.within ?? "*") ? "true" : "false"; + } + if (!target) return ""; + switch (ex.kind) { + case "attr": + return ex.attr ? target.getAttribute(ex.attr) ?? "" : ""; + case "text": + default: + return accessibleText(target); + } + } + function matchPred(el, pred, args) { + let a = extract(el, pred.extractor); + let b = interpolate(pred.value, args); + if (pred.ci) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + switch (pred.op) { + case "^=": + return a.startsWith(b); + case "*=": + return a.includes(b); + default: + return a === b; + } + } + function resolvePath(path, args) { + let scopes = [document]; + let matched = []; + for (const part of path) { + const found = []; + const seen = /* @__PURE__ */ new Set(); + for (const root of scopes) { + for (const loc of part.locators) { + for (const el of deepQueryAll(loc, root)) { + if (seen.has(el)) continue; + if ((part.preds ?? []).every((p) => matchPred(el, p, args))) { + seen.add(el); + found.push(el); + } + } + } + } + matched = found; + scopes = found; + } + return matched; + } + function resolveQuery(query, args) { + const all = resolvePath(query.parts, args); + if (query.index == null) return all; + const el = all[query.index]; + return el ? [el] : []; + } + function setElementValue(el, value) { + const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const desc = Object.getOwnPropertyDescriptor(proto, "value"); + if (desc?.set) { + desc.set.call(el, value); + } else { + el.value = value; + } + } + function makeInputEvent(inputType, data) { + if (typeof InputEvent !== "undefined") { + return new InputEvent("input", { bubbles: true, inputType, data: data ?? void 0 }); + } + return new Event("input", { bubbles: true }); + } + function typeInto(el, value) { + const target = el; + target.focus?.(); + setElementValue(el, ""); + target.dispatchEvent(makeInputEvent("deleteContentBackward", null)); + let acc = ""; + for (const ch of value) { + target.dispatchEvent(new KeyboardEvent("keydown", { key: ch, bubbles: true, cancelable: true })); + acc += ch; + setElementValue(el, acc); + target.dispatchEvent(makeInputEvent("insertText", ch)); + target.dispatchEvent(new KeyboardEvent("keyup", { key: ch, bubbles: true })); + } + target.dispatchEvent(new Event("change", { bubbles: true })); + if (el.getAttribute("role") === "combobox") { + target.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true })); + target.dispatchEvent(new KeyboardEvent("keyup", { key: "ArrowDown", bubbles: true })); + } + } + function clickElement(el) { + const t = el; + const r = t.getBoundingClientRect?.(); + const clientX = r ? Math.round(r.left + r.width / 2) : 0; + const clientY = r ? Math.round(r.top + r.height / 2) : 0; + const init = (buttons) => ({ + bubbles: true, + cancelable: true, + composed: true, + clientX, + clientY, + button: 0, + buttons + }); + const hasPE = typeof PointerEvent !== "undefined"; + const emit = (type, buttons, pointer) => { + if (pointer && hasPE) { + t.dispatchEvent( + new PointerEvent(type, { ...init(buttons), pointerId: 1, pointerType: "mouse", isPrimary: true }) + ); + } else { + t.dispatchEvent(new MouseEvent(type, init(buttons))); + } + }; + emit("pointerdown", 1, true); + emit("mousedown", 1, false); + emit("pointerup", 0, true); + emit("mouseup", 0, false); + t.click(); + } + function interpolate(template, args) { + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => { + const v = args[key]; + return v == null ? "" : String(v); + }); + } + + // src/executor.ts + var sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + function resolveOptions(options = {}) { + return { + pollMs: options.pollMs ?? 100, + currentPath: options.currentPath ?? (typeof window !== "undefined" ? window.location.pathname : "/"), + log: options.log ?? ((m) => console.warn(`[sightkick] ${m}`)), + signal: options.signal + }; + } + function routeMatches(pattern, path) { + const norm = (p) => { + const bare = p.split("#")[0].split("?")[0]; + const trimmed = bare.length > 1 && bare.endsWith("/") ? bare.slice(0, -1) : bare; + return trimmed || "/"; + }; + const pat = norm(pattern); + const pth = norm(path); + if (pat === "/") return pth === "/"; + const segs = pat.split("/").filter((s) => s.length > 0); + const rx = "^" + segs.map((seg) => { + if (seg === "**") return "(?:/.+)?"; + if (seg === "*" || seg.startsWith(":")) return "/[^/]+"; + return "/" + seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }).join("") + "$"; + return new RegExp(rx).test(pth); + } + function guardHolds(guard, args) { + const matches = resolveQuery(guard.query, args).length; + return guard.kind === "present" ? matches > 0 : matches === 0; + } + function describeTarget(step) { + const parts = step.query?.parts ?? []; + return `query ${JSON.stringify(parts.map((p) => p.locators.join("|")))}`; + } + function isActionable(el) { + const h = el; + if (typeof h.getBoundingClientRect !== "function") return false; + const style = typeof getComputedStyle === "function" ? getComputedStyle(h) : null; + if (h.offsetParent === null && style?.position !== "fixed") return false; + const r = h.getBoundingClientRect(); + return r.width > 0 && r.height > 0; + } + async function runStep(step, args, opts) { + const target = () => { + if (!step.query) return void 0; + const matches = resolveQuery(step.query, args); + return matches.find(isActionable) ?? matches[0]; + }; + switch (step.op) { + case "navigate": { + const path = opts.currentPath; + if (step.route && !routeMatches(step.route, path)) { + opts.log(`navigate: single-page slice cannot leave ${path} for ${step.route} (deferred to journey work)`); + } + return; + } + case "goto": { + const url = interpolate(step.url ?? "", args); + if (url && typeof window !== "undefined") { + setTimeout(() => window.location.assign(url), 0); + } + return; + } + case "fill": { + const el = target(); + if (!el) throw new Error(`fill: no element for ${describeTarget(step)}`); + typeInto(el, interpolate(step.value ?? "", args)); + return; + } + case "click": { + const el = target(); + if (!el) throw new Error(`click: no element for ${describeTarget(step)}`); + clickElement(el); + return; + } + case "keypress": { + const key = step.key ?? ""; + if (!key) throw new Error("keypress: no key given"); + const el = document.activeElement ?? document.body; + el.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true })); + el.dispatchEvent(new KeyboardEvent("keyup", { key, bubbles: true })); + return; + } + case "waitFor": { + const routeSatisfied = () => !!step.route && routeMatches(step.route, typeof window !== "undefined" ? window.location.pathname : opts.currentPath); + const satisfied = step.query ? () => !!target() : routeSatisfied; + const deadline = Date.now() + (step.timeoutMs ?? 5e3); + for (; ; ) { + if (opts.signal?.aborted) throw new Error("aborted"); + if (satisfied()) return; + if (Date.now() >= deadline) { + const what = step.query ? describeTarget(step) : `route ${step.route}`; + throw new Error(`waitFor: timed out after ${step.timeoutMs ?? 5e3}ms for ${what}`); + } + await sleep(opts.pollMs); + } + } + default: + throw new Error(`unknown step op ${step.op}`); + } + } + function extractFields(el, fields) { + const obj = {}; + for (const [name, f] of Object.entries(fields)) { + obj[name] = extract(el, f.extractor); + } + return obj; + } + function computeReturn(ret, args) { + if (ret.kind === "list") { + const rows = ret.query ? resolveQuery(ret.query, args) : []; + const fields = ret.fields ?? {}; + return { ok: true, items: rows.map((el2) => extractFields(el2, fields)) }; + } + const el = ret.query ? resolveQuery(ret.query, args)[0] : void 0; + const value = el && ret.extractor ? extract(el, ret.extractor) : void 0; + const out = { ok: true }; + if (value !== void 0) out.value = value; + return out; + } + async function runTool(tool, args = {}, options = {}) { + const opts = resolveOptions(options); + if (tool.ensureView && !routeMatches(tool.ensureView.route, opts.currentPath)) { + opts.log(`ensure_view: "${tool.name}" expects ${tool.ensureView.view} (${tool.ensureView.route}) but path is ${opts.currentPath}; proceeding best-effort`); + } + if (tool.guard && guardHolds(tool.guard, args)) { + const skipped = tool.returns ? { ...computeReturn(tool.returns, args), skipped: true } : { ok: true, skipped: true }; + skipped.message = "guard satisfied; steps skipped (already applied)"; + if (tool.guidance && tool.guidance.length) skipped.guidance = tool.guidance; + return skipped; + } + try { + for (const step of tool.steps) { + await runStep(step, args, opts); + } + } catch (err) { + return { ok: false, message: err.message }; + } + const result = tool.returns ? computeReturn(tool.returns, args) : { ok: true }; + if (tool.guidance && tool.guidance.length) result.guidance = tool.guidance; + return result; + } + + // src/webmcp.ts + var POLYFILL_FLAG = "__sightkickPolyfill"; + var _a, _b; + var ModelContextPolyfill = class extends (_b = EventTarget, _a = POLYFILL_FLAG, _b) { + constructor() { + super(...arguments); + __publicField(this, _a, true); + __publicField(this, "tools", /* @__PURE__ */ new Map()); + } + registerTool(def, options) { + this.tools.set(def.name, def); + if (options?.signal) { + options.signal.addEventListener( + "abort", + () => { + if (this.tools.get(def.name) === def) { + this.tools.delete(def.name); + this.dispatchEvent(new Event("toolchange")); + } + }, + { once: true } + ); + } + this.dispatchEvent(new Event("toolchange")); + return Promise.resolve(); + } + getTools() { + const origin = typeof location !== "undefined" ? location.origin : "null"; + return Promise.resolve( + [...this.tools.values()].map((t) => ({ + name: t.name, + description: t.description, + inputSchema: t.inputSchema, + origin + })) + ); + } + executeTool(tool, args = {}, options) { + const def = this.tools.get(tool.name); + if (!def) { + return Promise.reject(new Error(`unknown tool "${tool.name}"`)); + } + return Promise.resolve(def.execute(args, options)); + } + }; + function ensureModelContext() { + if (typeof document === "undefined") return void 0; + const doc = document; + if (doc.modelContext) return doc.modelContext; + const poly = new ModelContextPolyfill(); + Object.defineProperty(doc, "modelContext", { value: poly, configurable: true, writable: true }); + return poly; + } + function isPolyfilled(ctx) { + return !!ctx && ctx[POLYFILL_FLAG] === true; + } + + // src/errors.ts + function describeError(e) { + if (e instanceof Error) return `${e.name}: ${e.message}`; + if (typeof e === "object" && e !== null) { + const anyE = e; + if (anyE.message != null || anyE.name != null) { + return `${String(anyE.name ?? "Error")}: ${String(anyE.message ?? "")}`.trim(); + } + try { + const s = JSON.stringify(e); + if (s && s !== "{}") return s; + } catch { + } + return Object.prototype.toString.call(e); + } + return String(e); + } + + // src/boot.ts + function detectMode() { + return typeof window !== "undefined" && window.__sightkick_host != null ? "injected" : "direct"; + } + function findTool(ir, name) { + return ir?.tools.find((t) => t.name === name); + } + function toEnvelope(result) { + return { content: [{ type: "text", text: JSON.stringify(result) }], isError: !result.ok }; + } + var historyPatched = false; + function patchHistory() { + if (historyPatched || typeof history === "undefined" || typeof window === "undefined") return; + historyPatched = true; + const wrap = (orig) => function(data, unused, url) { + const r = orig.call(this, data, unused, url); + window.dispatchEvent(new Event("sightkick:navigate")); + return r; + }; + history.pushState = wrap(history.pushState.bind(history)); + history.replaceState = wrap(history.replaceState.bind(history)); + } + function boot(initial, opts = {}) { + const ctx = ensureModelContext(); + const currentPath = () => opts.currentPath ?? (typeof window !== "undefined" ? window.location.pathname : "/"); + let registrations = []; + let registered = []; + const unregisterAll = () => { + for (const c of registrations) c.abort(); + registrations = []; + registered = []; + }; + const refresh = () => { + unregisterAll(); + const ir = api.ir; + if (!ir || !ctx) return; + const path = currentPath(); + for (const tool of ir.tools) { + if (tool.ensureView && !routeMatches(tool.ensureView.route, path)) continue; + const controller = new AbortController(); + registrations.push(controller); + registered.push({ name: tool.name, description: tool.description }); + Promise.resolve( + ctx.registerTool( + { + name: tool.name, + description: tool.description ?? "", + inputSchema: tool.inputSchema, + execute: async (args, options) => toEnvelope(await runTool(tool, args, { signal: options?.signal, currentPath: path })) + }, + { signal: controller.signal } + ) + ).catch((e) => console.warn(`[sightkick] registerTool "${tool.name}" rejected: ${describeError(e)}`)); + } + }; + const api = { + mode: detectMode(), + ir: null, + modelContext: ctx, + polyfilled: isPolyfilled(ctx), + load(ir) { + this.ir = ir; + refresh(); + console.info( + `[sightkick] loaded IR "${ir.name}" (${ir.tools.length} tools, ${this.mode}, ${this.polyfilled ? "polyfilled" : "native"} modelContext)` + ); + }, + tools() { + return registered.slice(); + }, + refresh, + call(name, args = {}, options) { + const tool = findTool(this.ir, name); + if (!tool) return Promise.resolve({ ok: false, message: `unknown tool "${name}"` }); + return runTool(tool, args, options); + } + }; + if (typeof window !== "undefined" && opts.currentPath === void 0) { + let lastPath = currentPath(); + const onNav = () => { + const p = currentPath(); + if (p !== lastPath) { + lastPath = p; + refresh(); + } + }; + window.addEventListener("popstate", onNav); + window.addEventListener("sightkick:navigate", onNav); + patchHistory(); + } + if (initial) api.load(initial); + return api; + } + + // src/channel.ts + var IR_ATTR = "data-sightkick-ir"; + var HOST_ATTR = "data-sightkick-host"; + var IR_EVENT = "sightkick:ir"; + function loadFromDom(api) { + if (typeof document === "undefined" || !document.documentElement) return false; + const de = document.documentElement; + const raw = de.getAttribute(IR_ATTR); + if (!raw) return false; + const host = de.getAttribute(HOST_ATTR); + de.removeAttribute(IR_ATTR); + de.removeAttribute(HOST_ATTR); + try { + const ir = JSON.parse(raw); + if (host) { + try { + window.__sightkick_host = JSON.parse(host); + api.mode = "injected"; + } catch { + } + } + api.load(ir); + return true; + } catch (e) { + console.warn("[sightkick] IR channel: bad payload", e); + return false; + } + } + function installIrChannel(api) { + if (typeof document === "undefined") return; + if (loadFromDom(api)) return; + let tries = 0; + const poll = setInterval(() => { + if (loadFromDom(api) || ++tries > 40) clearInterval(poll); + }, 50); + document.addEventListener( + IR_EVENT, + () => { + if (loadFromDom(api)) clearInterval(poll); + }, + { once: true } + ); + } + + // src/client.ts + function createClient(ctx = ensureModelContext()) { + if (!ctx) throw new Error("createClient: no document.modelContext available"); + return { + async listTools() { + try { + return await ctx.getTools(); + } catch (e) { + throw new Error(`getTools failed: ${describeError(e)}`); + } + }, + async callTool(name, args = {}, options) { + const tools = await ctx.getTools(); + const tool = tools.find((t) => t.name === name); + if (!tool) throw new Error(`unknown tool "${name}"`); + let raw; + try { + raw = await ctx.executeTool(tool, args, options); + } catch (e) { + throw new Error(`executeTool "${name}" failed: ${describeError(e)}`); + } + return typeof raw === "string" ? JSON.parse(raw) : raw; + } + }; + } + + // src/index.ts + if (typeof window !== "undefined") { + const api = boot(window.__sightkick_ir); + window.__sightkick = api; + if (!window.__sightkick_ir) installIrChannel(api); + } +})(); + +;(function(){ if (window.__sightkick && !window.__sightkick.ir) window.__sightkick.load(window.__sightkick_ir); })(); diff --git a/package-lock.json b/package-lock.json index 10874ea..e296e33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ }, "examples/with-sightmap-webmcp": { "version": "0.0.0", - "extraneous": true, "dependencies": { "@sightmap/next": "file:../../packages/next", "next": "16.3.4", @@ -34,13 +33,33 @@ "typescript": "^5" } }, + "examples/with-sightmap-webmcp/node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "examples/with-sightmap-webmcp/node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -51,7 +70,6 @@ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -68,7 +86,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -91,7 +108,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -111,7 +127,6 @@ "os": [ "freebsd" ], - "peer": true, "dependencies": { "@img/sharp-wasm32": "0.35.4" }, @@ -134,7 +149,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -151,7 +165,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -168,7 +181,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -185,7 +197,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -202,7 +213,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -219,7 +229,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -236,7 +245,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -253,7 +261,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -270,7 +277,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -287,7 +293,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -304,7 +309,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -327,7 +331,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -350,7 +353,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -373,7 +375,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -396,7 +397,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -419,7 +419,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -442,7 +441,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -465,7 +463,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -482,7 +479,6 @@ "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.11.3" }, @@ -502,7 +498,6 @@ ], "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/sharp-wasm32": "0.35.4" }, @@ -525,7 +520,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -545,7 +539,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.9.0" }, @@ -565,7 +558,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=20.9.0" }, @@ -577,8 +569,7 @@ "version": "16.3.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.4.tgz", "integrity": "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "16.3.4", @@ -592,7 +583,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -609,7 +599,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -626,7 +615,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -643,7 +631,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -660,7 +647,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -677,7 +663,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -694,7 +679,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -711,7 +695,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -720,22 +703,275 @@ "resolved": "packages/next", "link": true }, + "node_modules/@sightmap/sightkick": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sightmap/sightkick/-/sightkick-0.5.0.tgz", + "integrity": "sha512-NzTG+bcPGRaCgjuoMu9/g1c8tQfHmA6/ZarrpuKvqw56cDd3x+5mLA/vHirLmW+Os4VJfwT2sz/zTyRErXFOBQ==", + "dev": true, + "license": "MIT", + "bin": { + "sightkick": "bin/sightkick.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@sightmap/sightkick-darwin-arm64": "0.5.0", + "@sightmap/sightkick-darwin-x64": "0.5.0", + "@sightmap/sightkick-linux-arm64": "0.5.0", + "@sightmap/sightkick-linux-x64": "0.5.0", + "@sightmap/sightkick-win32-arm64": "0.5.0", + "@sightmap/sightkick-win32-x64": "0.5.0" + } + }, + "node_modules/@sightmap/sightkick-darwin-arm64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sightmap/sightkick-darwin-arm64/-/sightkick-darwin-arm64-0.5.0.tgz", + "integrity": "sha512-HPMpbfduNenjpdApvJEOhhm9bu0abbrK4s2alAivJAYFVG4deUo4TZLgQfjJ2Wkzg4q0XGjq0DA35joEXGKc9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@sightmap/sightkick-darwin-x64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sightmap/sightkick-darwin-x64/-/sightkick-darwin-x64-0.5.0.tgz", + "integrity": "sha512-rjMF8AqOLrtLGKGGYo2rLqHBZiPOFHNncc7czCOPQY5ikz3WQjS+xi71CqVx3bMrQJrQ0fDzsIcG+1/MtbNKQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@sightmap/sightkick-linux-arm64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sightmap/sightkick-linux-arm64/-/sightkick-linux-arm64-0.5.0.tgz", + "integrity": "sha512-/va7bfjI0Z0SeGUAMZM4YXAwdegRmxxdPWlE7c2N+8W7hs7w7FzUC9rFGqBrCUyjhUHnwBmZWOetHnLSwH7CTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@sightmap/sightkick-linux-x64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sightmap/sightkick-linux-x64/-/sightkick-linux-x64-0.5.0.tgz", + "integrity": "sha512-H8frHkCfIPqRdxjcfK+c7X2yYcv2UslNY9x2OQS47Gn5aVQADX6Ygq9eY3dvrC8HaGwY9iI7glzuhO/9C6ep2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@sightmap/sightkick-win32-arm64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sightmap/sightkick-win32-arm64/-/sightkick-win32-arm64-0.5.0.tgz", + "integrity": "sha512-94BGuwUd/f2rD2DixB65gfF9j2sZmYlsgZYKEDEkmCM340s0xixDqsDw8EWR4mEwXzqXFQJoorqkaOcXHO9npA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sightmap/sightkick-win32-x64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sightmap/sightkick-win32-x64/-/sightkick-win32-x64-0.5.0.tgz", + "integrity": "sha512-uarfPu95JmNmzKwZYwvOKyR2kE3NZ6CuQkD63/ExvojSmTxOjbfus1g99nt3dhvM7W1Q5e7BxWU2c1OVPSaJZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sightmap/sightmap": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@sightmap/sightmap/-/sightmap-0.31.1.tgz", + "integrity": "sha512-pYD3Jpdtv1LzKhSfU6WTo9/OtH/D6SwTiwi7eZL/I0ydtc9LgsrrOSOzrXhWwa8HYzlzbV8+/dOsaCRkUdW1jg==", + "dev": true, + "license": "MIT", + "bin": { + "sightmap": "bin/sightmap.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@sightmap/sightmap-darwin-arm64": "0.31.1", + "@sightmap/sightmap-darwin-x64": "0.31.1", + "@sightmap/sightmap-linux-arm64": "0.31.1", + "@sightmap/sightmap-linux-x64": "0.31.1", + "@sightmap/sightmap-win32-arm64": "0.31.1", + "@sightmap/sightmap-win32-x64": "0.31.1" + } + }, + "node_modules/@sightmap/sightmap-darwin-arm64": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@sightmap/sightmap-darwin-arm64/-/sightmap-darwin-arm64-0.31.1.tgz", + "integrity": "sha512-+uQ/dKVFIr2X4VV4INYprc+92j1IoauLLfdinB+vbFTSYWgQuQ7sD3E5D0+PucaSaalXx6Oum/0TVb8Jxvi4BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@sightmap/sightmap-darwin-x64": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@sightmap/sightmap-darwin-x64/-/sightmap-darwin-x64-0.31.1.tgz", + "integrity": "sha512-GxbX/bUzLVbVOySASB9GYYqL14hcOSDWkJSYDNhxgQ2zyLxcsnhXd3ggCB4cVJigxm1YsfoaAAB9U0Ty0PxwfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@sightmap/sightmap-linux-arm64": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@sightmap/sightmap-linux-arm64/-/sightmap-linux-arm64-0.31.1.tgz", + "integrity": "sha512-VDiZ0s5b2swzP14Z+b+6ZWxReGlm6adnvWCdhY5AB553tXEaCPCrWUPJe3YQvLhApHQ9I+v4H15bpXiKoKM+FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@sightmap/sightmap-linux-x64": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@sightmap/sightmap-linux-x64/-/sightmap-linux-x64-0.31.1.tgz", + "integrity": "sha512-wh4q0IogteaGXbJz1JJR+0XXOMSpFzUgkNSi4iWpgLwvE1tHL2Cu7wEBHPRuAnSFgldo6lNO8WpJTdAb7h/x8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@sightmap/sightmap-win32-arm64": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@sightmap/sightmap-win32-arm64/-/sightmap-win32-arm64-0.31.1.tgz", + "integrity": "sha512-KZ/EBje6mxq0Lgrzl6g5PJk5XOanf7qZGT2Pecwna/7WYihRattRBQvZsxhzAgbzXQMSLOWLnBqCLbtnsvKWPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sightmap/sightmap-win32-x64": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@sightmap/sightmap-win32-x64/-/sightmap-win32-x64-0.31.1.tgz", + "integrity": "sha512-BPh+reI1jzdGYmG4Volbu+0NfM9i4zJL8bPMeMlG0JXXKSJH1scvGToniOG5f26+dPdgST1Mq5kjkNT8YLErWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@swc/helpers": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/agent-browser": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.36.0.tgz", + "integrity": "sha512-Ljjj4nRKUEqtrFF0pgev8lxTfC79tNgPj67sNi6BLnUAWIG8y9Cu2VQIeZ0MY3N2fTQagoBlfQ/pUY/4NWPD3w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "agent-browser": "bin/agent-browser.js" + }, + "engines": { + "node": ">=24.0.0", + "pnpm": ">=11.0.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.21", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "license": "Apache-2.0", - "peer": true, "bin": { "baseline-browser-mapping": "dist/cli.cjs" }, @@ -761,15 +997,20 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "CC-BY-4.0", - "peer": true + "license": "CC-BY-4.0" }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" }, "node_modules/detect-libc": { "version": "2.1.2", @@ -777,7 +1018,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -793,7 +1033,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -806,7 +1045,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.3.4.tgz", "integrity": "sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.3.4", "@swc/helpers": "0.5.23", @@ -859,8 +1097,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/postcss": { "version": "8.5.23", @@ -881,7 +1118,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", @@ -918,8 +1154,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "7.8.5", @@ -927,7 +1162,6 @@ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "optional": true, - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -941,7 +1175,6 @@ "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", @@ -991,7 +1224,6 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1001,7 +1233,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -1024,8 +1255,32 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/with-sightmap-webmcp": { + "resolved": "examples/with-sightmap-webmcp", + "link": true }, "packages/next": { "name": "@sightmap/next", diff --git a/package.json b/package.json index ee9e657..93c425e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,10 @@ "examples/*" ], "scripts": { - "test": "npm test -w packages/next" + "test": "npm test -w packages/next", + "build:example": "npm run build -w examples/with-sightmap-webmcp", + "start:example": "npm run start -w examples/with-sightmap-webmcp", + "test:plans": "npm run test:plans -w examples/with-sightmap-webmcp" }, "engines": { "node": ">=20"