R8 293 lines; ~3391 tokens; deep reference ../snappy-infra/auth-reference.md; deep reference ../snappy-database/SKILL.md; ../snappy-database/SKILL.md >100 lines without ToC; deep reference ../snappy-browse/SKILL.md; ../snappy-browse/SKILL.md >100 lines without ToC
snappy-xano-dashboard drives the Xano admin UI for the Snappy backend instance (xnwv-v1z6-dvnr.n7c.xano.io, vanity xano.snappy.ai). Uses agent-browser (snappy-browse) exclusively -- never Charlotte MCP browser tools. The REST API handles runtime calls; this skill handles everything the API cannot: editing endpoint logic, viewing request logs, inspecting schema, managing API groups, deploying XanoScript, configuring auth, viewing background tasks.
Take an interactive snapshotDashboard of the current page
navigateTo(section)
Navigate to a specific dashboard section
runApiTest(endpoint)
Run a browser-based API test via the dashboard
getRequestLogs()
Extract request logs (dashboard-only feature)
closeDashboard()
Close the browser session
If this loader doesn't cover your case:
bashecho "[$(date -u +%FT%TZ)] snappy-xano-dashboard: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-xano-dashboard Index]|root: ~/.claude/skills/snappy-xano-dashboard|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-xano-dashboard
role: Browser-driven admin operations on the Xano dashboard (xano.snappy.ai) via agent-browser for tasks the REST API cannot do.
loaded-by: PreToolUse hook (auto-injected when "snappy-xano-dashboard" is mentioned)
---
## Context
snappy-xano-dashboard drives the Xano admin UI for the Snappy backend instance (`xnwv-v1z6-dvnr.n7c.xano.io`, vanity `xano.snappy.ai`). Uses `agent-browser` (snappy-browse) exclusively -- never Charlotte MCP browser tools. The REST API handles runtime calls; this skill handles everything the API cannot: editing endpoint logic, viewing request logs, inspecting schema, managing API groups, deploying XanoScript, configuring auth, viewing background tasks.
## Key Capabilities
- **Request logs**: Dashboard-only. Navigate to Request History, screenshot or extract via `eval`.
- **Schema inspection**: Database section -> table -> view columns, foreign keys, references.
- **API group management**: Create/edit API groups, open endpoints, view the visual function stack.
- **XanoScript deploy**: Author via `xanoscript-builder`, paste into dashboard Monaco editor using clipboard pattern (`navigator.clipboard.writeText` + `Meta+v`), click Deploy.
- **Background tasks**: View and manage via Tasks section.
- **Auth config**: JWT settings, OAuth providers, API keys via Settings.
## Rules
- **API-first**: If the REST API can do it, use `snappy-infra` instead. Dashboard is slow and brittle by comparison.
- **Always use `agent-browser`**, never Charlotte MCP browser tools.
- **Navigate like a human**: Click through workspace nav, never construct deep links (workspace IDs change).
- **Screenshot before destructive actions**: Xano edits are immediate, no undo.
- **Clipboard paste for Monaco**: Individual keystrokes get swallowed. Use `navigator.clipboard.writeText(...)` then `Meta+v`.
- **Auth via cookie state**: `--state ~/.openclaw/workspace/xano-auth.json` on first `open` call. Cookies last days.
## API vs Dashboard Decision
| Task | Where |
|------|-------|
| Send Slack message, list records, run SQL | **REST API** (snappy-infra) |
| Request logs, edit endpoint logic, schema inspect | **Dashboard** (this skill) |
| Create API group, configure JWT, background tasks | **Dashboard** (this skill) |
| Deploy XanoScript | **Dashboard** (xanoscript-builder authors, this deploys) |
## Cross-Skill Chains
- `snappy-browse` -- underlying agent-browser CLI primitive
- `snappy-infra` -- REST API surface for the same Xano instance (use first)
- `xanoscript-builder` -- authors raw XanoScript; this skill deploys it
- `snappy-database` -- sister table catalog for cross-referencing schema
- `snappy-xano-mcp` -- regenerate MCP registry after deploying new endpoints
- `snappy-deploy` -- triggers this skill when shipping Xano changes in a meta-deploy
## Directory Layout
```
snappy-xano-dashboard/
SKILL.md <- Full reference (auth setup, common operations, anti-patterns)
AGENTS.md <- This file
```
## API module
```typescript
import { openDashboard, snapshotDashboard, navigateTo, runApiTest, getRequestLogs, closeDashboard } from "../snappy-xano-dashboard/api.ts";
```
Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-xano-dashboard/api.ts open
npx tsx ~/.claude/skills/snappy-xano-dashboard/api.ts test /api:PB9UH7b9/contacts
npx tsx ~/.claude/skills/snappy-xano-dashboard/api.ts logs
npx tsx ~/.claude/skills/snappy-xano-dashboard/api.ts closeDashboard
```
## API functions
| Function | Purpose |
|----------|---------|
| `openDashboard()` | Open Xano dashboard with saved auth cookies |
| `snapshotDashboard()` | Take an interactive snapshotDashboard of the current page |
| `navigateTo(section)` | Navigate to a specific dashboard section |
| `runApiTest(endpoint)` | Run a browser-based API test via the dashboard |
| `getRequestLogs()` | Extract request logs (dashboard-only feature) |
| `closeDashboard()` | Close the browser session |
---
If this loader doesn't cover your case:
```bash
echo "[$(date -u +%FT%TZ)] snappy-xano-dashboard: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```
Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-xano-dashboard Index]|root: ~/.claude/skills/snappy-xano-dashboard|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
## Used by
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `logs` | — | `read` | `npx tsx ~/.claude/skills/snappy-xano-dashboard/api.ts logs` |
| `open` | — | `write` | `npx tsx ~/.claude/skills/snappy-xano-dashboard/api.ts open` |
| `test` | `endpoint` | `write` | `npx tsx ~/.claude/skills/snappy-xano-dashboard/api.ts test <endpoint>` |
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
Browser-driven operations on the Xano admin dashboard for Robert's Snappy backend instance. The Xano REST API exposes the runtime endpoints; this skill drives the admin UI for everything the runtime API doesn't expose: editing endpoint logic, viewing request logs, inspecting the schema, managing API groups, deploying XanoScript, and configuring auth.
bash# 0. Kill zombie browser sessions
pkill -9 -f "daemon.js" 2>/dev/null
pkill -9 -f "Chrome for Testing" 2>/dev/null
sleep 1
# 1. Launch with saved Xano auth state
agent-browser --state ~/.openclaw/workspace/xano-auth.json open https://xano.snappy.ai
# 2. Verify logged in (look for workspace nav, NOT a login form)
agent-browser snapshot -i | head -20
# 3. Navigate to the workspace section you need (database / api / functions / tasks / addons)
agent-browser find text "API" click
# 4. Take screenshot to verify state before any destructive action
agent-browser screenshot /tmp/xano-state.png
snappy-xano-dashboard is a browser-automation consumer. It does not produce data -- it drives the Xano admin UI to perform admin tasks that the REST API can't.
Inputs (skills that feed this one):
snappy-browse -- the underlying agent-browser CLI primitive. This skill is a focused consumer of snappy-browse with Xano-specific recipes.
snappy-infra -- provides the auth pattern (cookie state file path), the canonical Xano instance URL, and the API group inventory you'll be editing through the dashboard.
xanoscript-builder -- when authoring raw XanoScript, that skill produces the script; this skill deploys it via the dashboard's XanoScript editor.
snappy-database -- sister catalog of the table schema. When inspecting/modifying tables, cross-reference with snappy-database.
Outputs (consumers downstream):
snappy-infra -- once endpoint changes are deployed via the dashboard, snappy-infra (and its REST API curl patterns) start hitting the new behavior immediately.
snappy-xano-mcp -- when new Xano endpoints are deployed via this dashboard, the MCP registry must be regenerated to expose them.
snappy-pipeline -- operates on a separate Xano instance (xh2o-yths-38lt.n7c.xano.io); follows the same pattern but uses a different auth state file.
Channels (where output is delivered):
The dashboard surfaces results inline (Slack/email notifications happen via snappy-infra, not this skill).
Orchestrator:
snappy-deploy triggers this skill when shipping a Xano endpoint change as part of a meta-deploy
Used ad-hoc by Robert when he needs to inspect the dashboard directly
Robert logs into Xano with email/password. The cookie state file is the cached login.
First-time setup: add Xano login credentials to snappy-settings/.env.cache (see snappy-settings/SKILL.md). Expected keys: XANO_LOGIN_USER and XANO_LOGIN_PASS. Then:
bash# Load from .env.cachesource ~/.claude/skills/snappy-settings/scripts/load-env.sh
# One-time auth save (run once, cookies refresh themselves on subsequent use)
bash ~/.openclaw/workspace/scripts/browser-connect.sh xano \
"https://xano.snappy.ai""Workspace"# (the script logs in, waits for "Workspace" text in the page, then saves state)
After the first save, every subsequent invocation just uses --state ~/.openclaw/workspace/xano-auth.json.
bashagent-browser find text "Database" click
agent-browser wait 1500
agent-browser snapshot -i | head -40 # see the table list
agent-browser find text "users" click # open the users table
agent-browser screenshot /tmp/xano-users-schema.png
bashagent-browser find text "API" click
agent-browser wait 1500
agent-browser find text "PB9UH7b9" click # the Main group
agent-browser snapshot -i | head -60 # list endpoints
bash# After opening the API group:
agent-browser find text "calendar/events" click
agent-browser wait 2000
agent-browser screenshot /tmp/xano-endpoint-stack.png
# Then use click/fill on the visual editor as needed
bash# 1. Author the script via xanoscript-builder skill# 2. Open the target endpoint in the dashboard
agent-browser find text "API" click
agent-browser find text "PB9UH7b9" click
agent-browser find text "your-endpoint" click
# 3. Switch to XanoScript view (button in the function stack toolbar)
agent-browser find text "XanoScript" click
agent-browser wait 1500
# 4. Clear the editor and paste new script# (use Cmd+A then paste pattern)
agent-browser eval "navigator.clipboard.writeText(`<your script here>`)"
agent-browser press "Meta+a"
agent-browser press "Meta+v"# 5. Click Deploy / Save
agent-browser find text "Deploy" click
agent-browser wait 3000
agent-browser screenshot /tmp/xano-deploy-result.png
The agent-browser CLI primitive this skill consumes. Read its SKILL.md for the full command cheat sheet, auth state file conventions, and zombie process cleanup.
snappy-infra
The REST API surface for the same Xano instance. Use it FIRST -- only fall back to this dashboard skill when the API can't do what you need.
xanoscript-builder
Authoring raw XanoScript that this skill deploys via the dashboard's XanoScript editor. Pair these two: write with xanoscript-builder, deploy with snappy-xano-dashboard.
snappy-database
Sister table catalog. When editing schema in the dashboard, cross-reference table definitions in snappy-database first.
snappy-xano-mcp
The MCP wrapper around the runtime API. After deploying a new endpoint via this dashboard, regenerate the MCP registry so the new tool is exposed.
snappy-pipeline
Sister skill for the Orbiter Xano instance (xh2o-yths-38lt.n7c.xano.io). Same dashboard patterns, different auth state file.
snappy-deploy
Meta-deploy orchestrator that may invoke this skill when shipping Xano changes as part of a larger deploy.
Snappy project maintenance -- keeping all client and internal systems healthy across Vercel…
---
name: snappy-xano-dashboard
reports_to: plumbing
head: false
description: >
Browser-driven operations on the Xano admin dashboard for the Snappy backend instance
(`xnwv-v1z6-dvnr.n7c.xano.io`, vanity `xano.snappy.ai`). Wraps `agent-browser` (snappy-browse)
to navigate the Xano UI for tasks the public REST API doesn't expose: editing endpoints,
inspecting request logs, viewing the database schema, deploying functions, managing API
groups, configuring auth, viewing background tasks, looking at the function stack, and
working with the visual XanoScript editor. Pairs with xanoscript-builder (raw script
authoring) and snappy-infra (REST API surface).
Triggers on: xano dashboard, xano admin, xano.snappy.ai, edit xano endpoint, xano logs,
xano request log, xano database, xano table, xano api group, xano auth, xano function,
xano background task, xanoscript editor, xano deploy, browse xano, xano workspace.
---
# Snappy Xano Dashboard
## Purpose
Browser-driven operations on the Xano admin dashboard for Robert's Snappy backend instance. The Xano REST API exposes the runtime endpoints; this skill drives the **admin UI** for everything the runtime API doesn't expose: editing endpoint logic, viewing request logs, inspecting the schema, managing API groups, deploying XanoScript, and configuring auth.
## When to Use This Skill
Auto-activates when:
- Editing or inspecting a Xano endpoint that you can't fix via the REST API alone
- Viewing the Xano request log to debug a failing endpoint (logs are not exposed via REST)
- Managing the Xano database schema (creating/editing tables, viewing columns and references)
- Creating or editing API groups, function groups, or background tasks
- Deploying a XanoScript file authored via `xanoscript-builder`
- Looking at the visual function stack to understand how an endpoint flows
- Configuring auth (JWT, OAuth, API key) via the dashboard
- Anything that requires the human-only Xano admin UI rather than the public REST API
---
## Core Principles
|target_instance: Snappy Xano (`xnwv-v1z6-dvnr.n7c.xano.io`)
|vanity_url: `xano.snappy.ai` (CNAME to the Xano workspace)
|tool: agent-browser (NOT Charlotte MCP browser tools -- those don't work)
|auth: cookie state file via `--state ~/.openclaw/workspace/xano-auth.json`
|navigation: like a human -- no URL guessing, click through the workspace navigation
|api_first: if the REST API can do it, USE THE REST API. The dashboard is for everything else.
|companion_skill: xanoscript-builder for raw script authoring; snappy-infra for REST API patterns
---
## Quick Start
```bash
# 0. Kill zombie browser sessions
pkill -9 -f "daemon.js" 2>/dev/null
pkill -9 -f "Chrome for Testing" 2>/dev/null
sleep 1
# 1. Launch with saved Xano auth state
agent-browser --state ~/.openclaw/workspace/xano-auth.json open https://xano.snappy.ai
# 2. Verify logged in (look for workspace nav, NOT a login form)
agent-browser snapshot -i | head -20
# 3. Navigate to the workspace section you need (database / api / functions / tasks / addons)
agent-browser find text "API" click
# 4. Take screenshot to verify state before any destructive action
agent-browser screenshot /tmp/xano-state.png
```
---
## Workflow
snappy-xano-dashboard is a **browser-automation consumer**. It does not produce data -- it drives the Xano admin UI to perform admin tasks that the REST API can't.
**Inputs (skills that feed this one):**
- `snappy-browse` -- the underlying agent-browser CLI primitive. This skill is a focused consumer of snappy-browse with Xano-specific recipes.
- `snappy-infra` -- provides the auth pattern (cookie state file path), the canonical Xano instance URL, and the API group inventory you'll be editing through the dashboard.
- `xanoscript-builder` -- when authoring raw XanoScript, that skill produces the script; this skill deploys it via the dashboard's XanoScript editor.
- `snappy-database` -- sister catalog of the table schema. When inspecting/modifying tables, cross-reference with snappy-database.
**Outputs (consumers downstream):**
- `snappy-infra` -- once endpoint changes are deployed via the dashboard, snappy-infra (and its REST API curl patterns) start hitting the new behavior immediately.
- `snappy-xano-mcp` -- when new Xano endpoints are deployed via this dashboard, the MCP registry must be regenerated to expose them.
- `snappy-pipeline` -- operates on a separate Xano instance (`xh2o-yths-38lt.n7c.xano.io`); follows the same pattern but uses a different auth state file.
**Channels (where output is delivered):**
- The dashboard surfaces results inline (Slack/email notifications happen via snappy-infra, not this skill).
**Orchestrator:**
- `snappy-deploy` triggers this skill when shipping a Xano endpoint change as part of a meta-deploy
- Used ad-hoc by Robert when he needs to inspect the dashboard directly
---
## Auth Setup
Robert logs into Xano with email/password. The cookie state file is the cached login.
First-time setup: add Xano login credentials to `snappy-settings/.env.cache` (see `snappy-settings/SKILL.md`). Expected keys: `XANO_LOGIN_USER` and `XANO_LOGIN_PASS`. Then:
```bash
# Load from .env.cache
source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# One-time auth save (run once, cookies refresh themselves on subsequent use)
bash ~/.openclaw/workspace/scripts/browser-connect.sh xano \
"https://xano.snappy.ai" "Workspace"
# (the script logs in, waits for "Workspace" text in the page, then saves state)
```
After the first save, every subsequent invocation just uses `--state ~/.openclaw/workspace/xano-auth.json`.
> **Auth canonical reference**: [snappy-infra/auth-reference.md](../snappy-infra/auth-reference.md) Section 4 (Auth State Files).
---
## Common Operations
### View Request Logs (debug a failing endpoint)
Request logs are NOT in the REST API -- only in the dashboard.
```bash
agent-browser --state ~/.openclaw/workspace/xano-auth.json open https://xano.snappy.ai
agent-browser find text "Request History" click
agent-browser wait 2000
agent-browser screenshot /tmp/xano-logs.png
# Or extract programmatically
agent-browser eval "
Array.from(document.querySelectorAll('[class*=\"request-row\"]'))
.slice(0, 10)
.map(r => ({
time: r.querySelector('[class*=\"time\"]')?.textContent,
endpoint: r.querySelector('[class*=\"endpoint\"]')?.textContent,
status: r.querySelector('[class*=\"status\"]')?.textContent
}))
"
```
### Inspect / Edit a Table Schema
```bash
agent-browser find text "Database" click
agent-browser wait 1500
agent-browser snapshot -i | head -40 # see the table list
agent-browser find text "users" click # open the users table
agent-browser screenshot /tmp/xano-users-schema.png
```
### Open an API Group
```bash
agent-browser find text "API" click
agent-browser wait 1500
agent-browser find text "PB9UH7b9" click # the Main group
agent-browser snapshot -i | head -60 # list endpoints
```
### Edit an Endpoint via the Visual Function Stack
```bash
# After opening the API group:
agent-browser find text "calendar/events" click
agent-browser wait 2000
agent-browser screenshot /tmp/xano-endpoint-stack.png
# Then use click/fill on the visual editor as needed
```
### Deploy XanoScript (raw authoring → dashboard deploy)
```bash
# 1. Author the script via xanoscript-builder skill
# 2. Open the target endpoint in the dashboard
agent-browser find text "API" click
agent-browser find text "PB9UH7b9" click
agent-browser find text "your-endpoint" click
# 3. Switch to XanoScript view (button in the function stack toolbar)
agent-browser find text "XanoScript" click
agent-browser wait 1500
# 4. Clear the editor and paste new script
# (use Cmd+A then paste pattern)
agent-browser eval "navigator.clipboard.writeText(`<your script here>`)"
agent-browser press "Meta+a"
agent-browser press "Meta+v"
# 5. Click Deploy / Save
agent-browser find text "Deploy" click
agent-browser wait 3000
agent-browser screenshot /tmp/xano-deploy-result.png
```
### View Background Tasks
```bash
agent-browser find text "Tasks" click
agent-browser wait 1500
agent-browser snapshot -i | head -40
```
### Inspect Auth Config (JWT settings, OAuth providers, API keys)
```bash
agent-browser find text "Settings" click
# Then navigate to Authentication submenu
```
---
## What AI Agents Get Wrong
| ❌ WRONG | ✅ CORRECT |
|----------|-----------|
| Use Charlotte MCP browser tools to drive Xano | Always use `agent-browser` CLI. CLAUDE.md explicitly forbids Charlotte MCP browser tools -- they don't inject cookies properly. |
| Construct deep links like `https://xano.snappy.ai/workspace/123/api/PB9UH7b9` | Navigate from the workspace home like a human: click "API" → click the group name. Workspace IDs and route segments change between instances. |
| Try to read endpoint logic via REST API | The function stack is dashboard-only. Use the visual editor or switch to XanoScript view. |
| Try to read request logs via REST | Request History is dashboard-only -- use this skill. |
| Edit XanoScript by typing into the editor field by field | Use clipboard paste pattern: `navigator.clipboard.writeText(...)` then `Meta+v`. The Monaco editor swallows individual keystrokes. |
| Skip the screenshot before any destructive action | Always `screenshot /tmp/xano-state.png` first -- Xano edits are immediate, no undo. |
| Re-pass `--state` on every command | `--state` only loads on the first `open` call. Subsequent commands inherit the session. |
| Rebuild auth from scratch every session | Use the saved cookie state. Cookies last days; re-auth only when you actually see a login page. |
---
## API-First Rule
Before browser-driving the dashboard, ask: **Can the REST API do this?**
| Task | API or Dashboard? |
|------|-------------------|
| Send a Slack message via Snappy backend | API (`api:hZB4Dj0c/slack/bot-message`) |
| List records from a table | API -- write a `query` endpoint, then curl it |
| Read request logs | **Dashboard only** |
| Edit endpoint logic | **Dashboard only** (or `xanoscript-builder` + dashboard deploy) |
| Inspect schema (column types, foreign keys) | **Dashboard** OR cross-reference [snappy-database](../snappy-database/SKILL.md) |
| Create a new API group | **Dashboard only** |
| Configure JWT auth settings | **Dashboard only** |
| Run a one-off SQL query | API -- `snappy_query` MCP tool, or a dashboard SQL query in the database section |
| Check background task status | **Dashboard only** |
| Download a database backup | **Dashboard only** |
If a task is API-doable, **use the API** (snappy-infra) -- the dashboard is slow and brittle by comparison.
---
## Related Skills
| Skill | Why |
|-------|-----|
| `snappy-browse` | The agent-browser CLI primitive this skill consumes. Read its [SKILL.md](../snappy-browse/SKILL.md) for the full command cheat sheet, auth state file conventions, and zombie process cleanup. |
| `snappy-infra` | The REST API surface for the same Xano instance. Use it FIRST -- only fall back to this dashboard skill when the API can't do what you need. |
| `xanoscript-builder` | Authoring raw XanoScript that this skill deploys via the dashboard's XanoScript editor. Pair these two: write with xanoscript-builder, deploy with snappy-xano-dashboard. |
| `snappy-database` | Sister table catalog. When editing schema in the dashboard, cross-reference table definitions in snappy-database first. |
| `snappy-xano-mcp` | The MCP wrapper around the runtime API. After deploying a new endpoint via this dashboard, regenerate the MCP registry so the new tool is exposed. |
| `snappy-pipeline` | Sister skill for the Orbiter Xano instance (`xh2o-yths-38lt.n7c.xano.io`). Same dashboard patterns, different auth state file. |
| `snappy-deploy` | Meta-deploy orchestrator that may invoke this skill when shipping Xano changes as part of a larger deploy. |
---
## Anti-Patterns
| Wrong | Right |
|-------|-------|
| Browser-driving for tasks the REST API supports | Use snappy-infra REST API -- it's 10-100x faster |
| Hardcoding workspace UUIDs in scripts | Navigate from the workspace home; let click chains handle IDs |
| Acting blind without snapshot/screenshot | Always `snapshot -i` after navigation, screenshot before edits |
| Building auth flows in the skill code | Use the saved cookie state file (`xano-auth.json`) |
| Editing XanoScript field-by-field with `fill` | Use clipboard paste for the Monaco editor |
| Pasting JWT secrets into the script for testing | Auth tokens stay in `.env.cache` + Xano server-side, never in browser scripts |
---
**Skill Status**: COMPLETE
## Near neighbours
Skills whose description overlaps this one enough that a reader could pick the
wrong door. Each row is that skill's own first sentence about itself, so the
choice is made on its words, not on a summary written here.
| Skill | Reach for it instead when |
|---|---|
| `snappy-dashboard` | Snappy Dashboard — the operating system for your backend infrastructure. |
| `snappy-docs` | THE DEFAULT for writing to Notion -- the Snappy stack's Notion primitive over the REST API (a… |
| `snappy-dom-cartographer` | Master DOM mapping agent for the Snappy swarm. |
| `snappy-github` | Centralized GitHub operations across all Snappy client repos via the `gh` CLI -- pull request… |
| `snappy-linkedin` | LinkedIn operations for Snappy -- posting (text, image, carousel, native video, article, poll… |
| `snappy-maintenance` | Snappy project maintenance -- keeping all client and internal systems healthy across Vercel… |
api.ts
#!/usr/bin/env npx tsx
/**
* snappy-xano-dashboard/api.ts -- Browser-driven Xano admin operations.
*
* Uses agent-browser for dashboard tasks the REST API cannot do:
* request logs, schema inspection, endpoint editing.
*
* Usage:
* npx tsx api.ts open
* npx tsx api.ts test /api:PB9UH7b9/contacts
*
* Or import as module:
* import { openDashboard, runApiTest } from "../snappy-xano-dashboard/api.ts";
*/
import { execSync } from "child_process";
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { annotationsForClass } from "../snappy-settings/tool-annotations.ts";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
const XANO_URL = "https://xano.snappy.ai";
const AUTH_STATE = `${process.env.HOME}/.openclaw/workspace/xano-auth.json`;
const SESSION = process.env.AGENT_BROWSER_SESSION || `xano-${process.pid}-${Date.now()}`;
function browser(command: string): string {
return execSync(`agent-browser --session "${SESSION}" ${command}`, {
encoding: "utf-8",
timeout: 30_000,
}).trim();
}
/** Open the Xano dashboard with saved auth cookies. */
export function openDashboard(): string {
return browser(`--state ${AUTH_STATE} open "${XANO_URL}"`);
}
/** Take an interactive snapshotDashboard of the current dashboard page. */
export function snapshotDashboard(): string {
return browser("snapshotDashboard -i");
}
/** Navigate to a specific section of the dashboard. */
export function navigateTo(section: string): string {
return browser(`find text "${section}" click`);
}
/** Run a browser-based API test by navigating to the endpoint in the dashboard. */
export function runApiTest(endpoint: string): string {
// Open dashboard, navigate to API section, find endpoint
openDashboard();
browser("wait 2000");
// Navigate to the API group
const parts = endpoint.split("/");
const group = parts.find(p => p.startsWith("api:")) || "";
if (group) {
browser(`find text "API" click`);
browser("wait 1000");
browser(`snapshotDashboard -i`);
}
// Take a screenshot for verification
const screenshotPath = `/tmp/xano-test-${Date.now()}.png`;
browser(`screenshot ${screenshotPath}`);
return `Dashboard opened. Endpoint: ${endpoint}. Screenshot: ${screenshotPath}`;
}
/** Extract request logs from the dashboard (dashboard-only feature). */
export function getRequestLogs(): string {
openDashboard();
browser("wait 2000");
browser(`find text "Request History" click`);
browser("wait 2000");
return browser("snapshotDashboard -i");
}
/** Close the browser session. */
export function closeDashboard(): string {
return browser("closeDashboard");
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*
* `backend: "retired"` — this road's backend is BANNED (the ruling of
* 2026-08-30: never read it, write it, or fall back to it). The verbs are
* declared so the census can count the road honestly and Snappy can refuse
* it BY NAME; nothing here is callable until the road is rebuilt. */
/** THE HOST-FACING FACTS ⟨lane CONTRACTS N–Z, 2026-09-09⟩. `class` is the
* closed effect set snappy-tool-design rule 18 grades; `annotations` are
* DERIVED from it by the ONE derivation in
* `snappy-settings/tool-annotations.ts`, never written per verb, so a class
* and its published hints cannot disagree; `refusals` projects the ONE closed
* table in `snappy-settings/refusal-codes.ts`; `requires` is exactly the
* credential keys this file's own executable reads name, and nothing else. */
export const HAND_CONTRACT = {
skill: "snappy-xano-dashboard",
description: "Browser-driven operations on the Xano admin dashboard for the Snappy backend instance (`xnwv-v1z6-dvnr.n7c.xano.io`, vanity `xano.snappy.ai`). Wraps `agent-browser` (snappy-browse) to navigate the Xano UI for tasks the public REST API doesn't expose: editing endpoints, inspecting request logs, viewing the database schema, deploying functions, managing API groups, configuring auth, viewing background tasks, looking at the function stack, and working with the visual XanoScript editor. Pairs with xanoscript-builder (raw script authoring) and snappy-infra (REST API surface). Triggers on: xano dashboard, xano admin, xano.snappy.ai, edit xano endpoint, xano logs, xano request log, xano database, xano table, xano api group, xano auth, xano function, xano background task, xanoscript editor, xano deploy, browse xano, xano workspace.",
managed: true,
requires: [] as string[],
backend: "retired",
refusals: refusalTable("backend_retired", "missing_argument", "unknown_verb"),
verbs: {
logs: {
args: [], effect: "read", class: "read", execution: "call", openWorld: true,
annotations: annotationsForClass("read", { openWorld: true }),
},
open: {
args: [], effect: "write", class: "additive-write", openWorld: true,
annotations: annotationsForClass("additive-write", { openWorld: true }),
},
test: {
args: ["endpoint"], effect: "write", class: "additive-write", openWorld: true,
annotations: annotationsForClass("additive-write", { openWorld: true }),
inputSchema: { properties: { endpoint: { type: "string", description: "Xano endpoint path to call" } } },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "open": {
console.log(openDashboard());
console.log(snapshotDashboard());
break;
}
case "test": {
const [endpoint] = args;
if (!endpoint) { console.error("Usage: api.ts test <endpoint>"); process.exit(1); }
console.log(runApiTest(endpoint));
break;
}
case "logs": {
console.log(getRequestLogs());
break;
}
case "closeDashboard": {
console.log(closeDashboard());
break;
}
default:
console.log("Usage: npx tsx api.ts [open|test|logs|closeDashboard] ...");
}
})();
}
#!/usr/bin/env npx tsx
/**
* snappy-xano-dashboard/api.ts -- Browser-driven Xano admin operations.
*
* Uses agent-browser for dashboard tasks the REST API cannot do:
* request logs, schema inspection, endpoint editing.
*
* Usage:
* npx tsx api.ts open
* npx tsx api.ts test /api:PB9UH7b9/contacts
*
* Or import as module:
* import { openDashboard, runApiTest } from "../snappy-xano-dashboard/api.ts";
*/
import { execSync } from "child_process";
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { annotationsForClass } from "../snappy-settings/tool-annotations.ts";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
const XANO_URL = "https://xano.snappy.ai";
const AUTH_STATE = `${process.env.HOME}/.openclaw/workspace/xano-auth.json`;
const SESSION = process.env.AGENT_BROWSER_SESSION || `xano-${process.pid}-${Date.now()}`;
function browser(command: string): string {
return execSync(`agent-browser --session "${SESSION}" ${command}`, {
encoding: "utf-8",
timeout: 30_000,
}).trim();
}
/** Open the Xano dashboard with saved auth cookies. */
export function openDashboard(): string {
return browser(`--state ${AUTH_STATE} open "${XANO_URL}"`);
}
/** Take an interactive snapshotDashboard of the current dashboard page. */
export function snapshotDashboard(): string {
return browser("snapshotDashboard -i");
}
/** Navigate to a specific section of the dashboard. */
export function navigateTo(section: string): string {
return browser(`find text "${section}" click`);
}
/** Run a browser-based API test by navigating to the endpoint in the dashboard. */
export function runApiTest(endpoint: string): string {
// Open dashboard, navigate to API section, find endpoint
openDashboard();
browser("wait 2000");
// Navigate to the API group
const parts = endpoint.split("/");
const group = parts.find(p => p.startsWith("api:")) || "";
if (group) {
browser(`find text "API" click`);
browser("wait 1000");
browser(`snapshotDashboard -i`);
}
// Take a screenshot for verification
const screenshotPath = `/tmp/xano-test-${Date.now()}.png`;
browser(`screenshot ${screenshotPath}`);
return `Dashboard opened. Endpoint: ${endpoint}. Screenshot: ${screenshotPath}`;
}
/** Extract request logs from the dashboard (dashboard-only feature). */
export function getRequestLogs(): string {
openDashboard();
browser("wait 2000");
browser(`find text "Request History" click`);
browser("wait 2000");
return browser("snapshotDashboard -i");
}
/** Close the browser session. */
export function closeDashboard(): string {
return browser("closeDashboard");
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*
* `backend: "retired"` — this road's backend is BANNED (the ruling of
* 2026-08-30: never read it, write it, or fall back to it). The verbs are
* declared so the census can count the road honestly and Snappy can refuse
* it BY NAME; nothing here is callable until the road is rebuilt. */
/** THE HOST-FACING FACTS ⟨lane CONTRACTS N–Z, 2026-09-09⟩. `class` is the
* closed effect set snappy-tool-design rule 18 grades; `annotations` are
* DERIVED from it by the ONE derivation in
* `snappy-settings/tool-annotations.ts`, never written per verb, so a class
* and its published hints cannot disagree; `refusals` projects the ONE closed
* table in `snappy-settings/refusal-codes.ts`; `requires` is exactly the
* credential keys this file's own executable reads name, and nothing else. */
export const HAND_CONTRACT = {
skill: "snappy-xano-dashboard",
description: "Browser-driven operations on the Xano admin dashboard for the Snappy backend instance (`xnwv-v1z6-dvnr.n7c.xano.io`, vanity `xano.snappy.ai`). Wraps `agent-browser` (snappy-browse) to navigate the Xano UI for tasks the public REST API doesn't expose: editing endpoints, inspecting request logs, viewing the database schema, deploying functions, managing API groups, configuring auth, viewing background tasks, looking at the function stack, and working with the visual XanoScript editor. Pairs with xanoscript-builder (raw script authoring) and snappy-infra (REST API surface). Triggers on: xano dashboard, xano admin, xano.snappy.ai, edit xano endpoint, xano logs, xano request log, xano database, xano table, xano api group, xano auth, xano function, xano background task, xanoscript editor, xano deploy, browse xano, xano workspace.",
managed: true,
requires: [] as string[],
backend: "retired",
refusals: refusalTable("backend_retired", "missing_argument", "unknown_verb"),
verbs: {
logs: {
args: [], effect: "read", class: "read", execution: "call", openWorld: true,
annotations: annotationsForClass("read", { openWorld: true }),
},
open: {
args: [], effect: "write", class: "additive-write", openWorld: true,
annotations: annotationsForClass("additive-write", { openWorld: true }),
},
test: {
args: ["endpoint"], effect: "write", class: "additive-write", openWorld: true,
annotations: annotationsForClass("additive-write", { openWorld: true }),
inputSchema: { properties: { endpoint: { type: "string", description: "Xano endpoint path to call" } } },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "open": {
console.log(openDashboard());
console.log(snapshotDashboard());
break;
}
case "test": {
const [endpoint] = args;
if (!endpoint) { console.error("Usage: api.ts test <endpoint>"); process.exit(1); }
console.log(runApiTest(endpoint));
break;
}
case "logs": {
console.log(getRequestLogs());
break;
}
case "closeDashboard": {
console.log(closeDashboard());
break;
}
default:
console.log("Usage: npx tsx api.ts [open|test|logs|closeDashboard] ...");
}
})();
}
refusals.test.ts
/**
* COVERAGE FOR SNAPPY-XANO-DASHBOARD'S DECLARED REFUSAL CODES
* (snappy-tool-design rule 33: "refusal codes form one closed table and each
* row has coverage").
*
* Two things are graded here, and the second is the one that matters. The
* first is that the hand's table is a PROJECTION of the collection's one
* closed table in snappy-settings/refusal-codes.ts — same object, not a copy
* that can drift. The second is that every declared code is GROUNDED: the
* evidence that justified declaring it is re-checked here, because a refusal
* code with no path that emits it is a branch the reader waits for and never
* sees, and a table of those passes a lint while teaching a lie.
*
* The code list is spelled out rather than read from the contract: a test that
* iterates the thing it grades passes for an empty table.
*/
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES } from "../snappy-settings/refusal-codes.ts";
const SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "api.ts"), "utf8");
/** Every refusal code snappy-xano-dashboard declares. */
const DECLARED = [
"backend_retired",
"missing_argument",
"unknown_verb",
] as const;
test("snappy-xano-dashboard declares exactly these refusal codes", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals).sort(), [...DECLARED].sort());
});
test("every declared code is the SAME row as the one closed table's, never a copy", () => {
const table = HAND_CONTRACT.refusals as Record<string, unknown>;
for (const code of DECLARED) {
assert.equal(table[code], REFUSAL_CODES[code], `${code} is not the shared row`);
}
});
test("backend_retired is grounded: the contract declares the retired road", () => {
assert.equal((HAND_CONTRACT as { backend?: string }).backend, "retired");
});
test("missing_argument is grounded: at least one verb has a required word", () => {
const required = Object.values(HAND_CONTRACT.verbs as Record<string, { args?: readonly string[] }>)
.flatMap((v) => (v.args ?? []).filter((a) => !a.endsWith("?")));
assert.ok(required.length > 0, "no verb has a required argument, so missing_argument can never fire");
});
test("unknown_verb is grounded: the contract closes the verb set, so a word outside it is refusable", () => {
assert.ok(Object.keys(HAND_CONTRACT.verbs).length >= 0);
assert.ok(!Object.keys(HAND_CONTRACT.verbs).includes("no-such-verb"));
});
/**
* COVERAGE FOR SNAPPY-XANO-DASHBOARD'S DECLARED REFUSAL CODES
* (snappy-tool-design rule 33: "refusal codes form one closed table and each
* row has coverage").
*
* Two things are graded here, and the second is the one that matters. The
* first is that the hand's table is a PROJECTION of the collection's one
* closed table in snappy-settings/refusal-codes.ts — same object, not a copy
* that can drift. The second is that every declared code is GROUNDED: the
* evidence that justified declaring it is re-checked here, because a refusal
* code with no path that emits it is a branch the reader waits for and never
* sees, and a table of those passes a lint while teaching a lie.
*
* The code list is spelled out rather than read from the contract: a test that
* iterates the thing it grades passes for an empty table.
*/
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES } from "../snappy-settings/refusal-codes.ts";
const SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "api.ts"), "utf8");
/** Every refusal code snappy-xano-dashboard declares. */
const DECLARED = [
"backend_retired",
"missing_argument",
"unknown_verb",
] as const;
test("snappy-xano-dashboard declares exactly these refusal codes", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals).sort(), [...DECLARED].sort());
});
test("every declared code is the SAME row as the one closed table's, never a copy", () => {
const table = HAND_CONTRACT.refusals as Record<string, unknown>;
for (const code of DECLARED) {
assert.equal(table[code], REFUSAL_CODES[code], `${code} is not the shared row`);
}
});
test("backend_retired is grounded: the contract declares the retired road", () => {
assert.equal((HAND_CONTRACT as { backend?: string }).backend, "retired");
});
test("missing_argument is grounded: at least one verb has a required word", () => {
const required = Object.values(HAND_CONTRACT.verbs as Record<string, { args?: readonly string[] }>)
.flatMap((v) => (v.args ?? []).filter((a) => !a.endsWith("?")));
assert.ok(required.length > 0, "no verb has a required argument, so missing_argument can never fire");
});
test("unknown_verb is grounded: the contract closes the verb set, so a word outside it is refusable", () => {
assert.ok(Object.keys(HAND_CONTRACT.verbs).length >= 0);
assert.ok(!Object.keys(HAND_CONTRACT.verbs).includes("no-such-verb"));
});