Skill: ivannikolovbg/vanio (version 0.1.0)

--- name: vanio description: Connect your agent to Airbnb, Booking.com & VRBO via Vanio AI — the only way to manage vacation rentals from OpenClaw. 140 tools for reservations, guests, messaging, smart locks, payments, workflows, and more. version: 0.1.0 homepage: https://www.vanio.ai metadata: openclaw: emoji: 🏠 requires: bins: [vanio] env: [VANIO_API_KEY] primaryEnv: VANIO_API_KEY install: - id: npm kind: node package: '@vanio-ai/cli' bins: [vanio] label: Install Vanio CLI (npm) --- # Vanio AI — Connect Your Agent to Airbnb, Booking.com & VRBO **[Vanio AI](https://www.vanio.ai)** is an AI property manager that lets you run your vacation rental portfolio fully autonomously. Connect to Airbnb, Booking.com, and VRBO, create your own direct booking website, and manage everything — guest communication, smart locks, payments, cleaning teams, and more — all powered by AI. This skill is the **only way to connect your OpenClaw agent to Airbnb and Booking.com**. It gives your agent access to Vanio's full AI system — 140 tools across every aspect of property management. ## Setup 1. Sign up at [vanio.ai](https://www.vanio.ai) and connect your Airbnb/Booking.com/VRBO accounts 2. Login via browser: ```bash vanio login ``` Or with an API key (from Settings → API Keys): ```bash vanio login <your-api-key> ``` ## Usage ### Ask a question (single query) ```bash vanio ask "What are today's check-ins?" vanio ask "What's the WiFi password at Alpine Lodge?" --listing 42 vanio ask "Why was this guest charged extra?" --reservation 214303 vanio ask "Send a message to the guest in reservation 214303 asking about their ETA" vanio ask "Create a maintenance task for unit 5 - broken shower head" vanio ask "What are my occupancy rates for April?" vanio ask "Unlock the front door at Beach House" --listing 15 ``` ### Interactive chat ```bash vanio chat ``` Maintains conversation context across turns — useful for multi-step operations. ### Check connection ```bash vanio status ``` ## What the agent can do **Reservations** — Search, view details, create direct bookings, modify dates, cancel, move between listings **Guest CRM** — Search guests, view profiles, add contacts, flag guests, view booking history **Messaging** — Send guest messages (auto-routes to Airbnb/Booking.com/WhatsApp/SMS/email), internal notes, team chat with @mentions **Tasks** — Create, update, complete, cancel operational tasks (cleaning, maintenance, inspections) **Smart Locks & IoT** — List devices, create/delete access codes, lock/unlock remotely, sync devices **Payments** — View payment history, charge guests, process refunds **Knowledge Base** — Search, create, edit articles (check-in guides, WiFi info, house rules, parking) **Workflows & Automations** — Create, update, toggle automated workflows **Analytics** — Revenue forecasts, occupancy rates, listing performance, training scores **Listings** — View properties, update amenities, rooms, content, pricing, push to platforms **Calendars** — Check availability, block/unblock dates, update pricing **Websites** — Manage direct booking websites **Service Providers** — Manage cleaning/maintenance teams, schedules, marketplace **Emails** — Search email history, send status updates **AI Settings** — Configure AI modes, escalation policies, notification preferences ## Tips - The agent automatically determines which tools to use based on your natural language query - For reservation-specific questions, use `--reservation <id>` for best context - For property-specific questions, use `--listing <id>` - The agent remembers conversation context in `chat` mode - Complex operations (cancellations, charges, lock changes) will ask for confirmation before executing ## Configuration ```bash vanio config # Show current config vanio config set baseUrl <url> # Custom API URL (self-hosted) vanio config set apiKey <key> # Update API key ``` Config stored at `~/.config/vanio/config.json`.

path: build.mjs
content: import { build } from 'esbuild'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'

const __dirname = dirname(fileURLToPath(import.meta.url))

await build({
  entryPoints: [resolve(__dirname, 'src/index.ts')],
  bundle: true,
  platform: 'node',
  target: 'node20',
  format: 'cjs',
  outfile: resolve(__dirname, 'dist/index.cjs'),
  external: [],
  resolveExtensions: ['.ts', '.js', '.json'],
  banner: { js: '#!/usr/bin/env node' },
  sourcemap: false,
  minify: true,
  logLevel: 'warning',
})

console.log('Built dist/index.cjs')


path: dist/index.cjs
content: #!/usr/bin/env node
"use strict";var K=Object.create;var A=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var O=Object.getPrototypeOf,E=Object.prototype.hasOwnProperty;var q=(e,o,n,i)=>{if(o&&typeof o=="object"||typeof o=="function")for(let s of L(o))!E.call(e,s)&&s!==n&&A(e,s,{get:()=>o[s],enumerable:!(i=_(o,s))||i.enumerable});return e};var $=(e,o,n)=>(n=e!=null?K(O(e)):{},q(o||!e||!e.__esModule?A(n,"default",{value:e,enumerable:!0}):n,e));var p=require("fs"),h=require("path"),S=require("os"),C=require("http"),b=require("crypto"),x=(0,h.join)((0,S.homedir)(),".config","vanio"),m=(0,h.join)(x,"config.json"),v="https://api.vanio.ai",y="/v1";function u(){try{return JSON.parse((0,p.readFileSync)(m,"utf-8"))}catch{return{}}}function f(e){(0,p.mkdirSync)(x,{recursive:!0}),(0,p.writeFileSync)(m,JSON.stringify(e,null,2))}function V(){let e=process.env.VANIO_API_KEY;if(e)return e;let o=u();if(o.apiKey)return o.apiKey;console.error("Error: No API key configured."),console.error("Run: vanio login <api-key>"),console.error("Or set VANIO_API_KEY environment variable."),process.exit(1)}function N(){return process.env.VANIO_API_URL||u().baseUrl||v}async function k(e,o="GET",n){let i=`${N()}${e}`,s=V(),t=await fetch(i,{method:o,headers:{"Content-Type":"application/json","x-api-key":s},...n?{body:JSON.stringify(n)}:{}});if(!t.ok){let r=await t.json().catch(()=>({message:t.statusText}));console.error(`Error ${t.status}: ${r.message||r.error||t.statusText}`),process.exit(1)}return t.json()}async function R(e){if(e[0]&&!e[0].startsWith("--")){let a=u();a.apiKey=e[0],f(a),console.log("API key saved to ~/.config/vanio/config.json");return}let o=process.env.VANIO_API_URL||u().baseUrl||v,n=(0,b.randomBytes)(16).toString("hex"),i=(0,C.createServer)((a,c)=>{let g=new URL(a.url||"/","http://127.0.0.1");if(g.pathname==="/callback"){let I=g.searchParams.get("key"),U=g.searchParams.get("state"),T=g.searchParams.get("user")||"";if(U!==n){c.writeHead(400,{"Content-Type":"text/html"}),c.end("<h1>State mismatch \u2014 login aborted</h1><p>Close this tab and try again.</p>"),i.close(),console.error("Error: state mismatch. Possible CSRF attack. Try again."),process.exit(1);return}if(!I){c.writeHead(400,{"Content-Type":"text/html"}),c.end("<h1>No API key received</h1><p>Close this tab and try again.</p>"),i.close(),console.error("Error: no API key received from server."),process.exit(1);return}let d=u();d.apiKey=I,d.baseUrl=o,f(d),c.writeHead(200,{"Content-Type":"text/html"}),c.end(`
        <html>
          <head><title>Vanio CLI</title></head>
          <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff;">
            <div style="text-align: center;">
              <h1 style="font-size: 2rem;">\u2713 Logged in to Vanio</h1>
              <p style="color: #888;">You can close this tab and return to your terminal.</p>
            </div>
          </body>
          <script>history.replaceState(null, '', '/')</script>
        </html>
      `),console.log(`
Logged in as ${T||"unknown"}`),console.log("API key saved to ~/.config/vanio/config.json"),setTimeout(()=>{i.close(),process.exit(0)},1e3);return}c.writeHead(404),c.end()});await new Promise(a=>{i.listen(0,"127.0.0.1",()=>a())});let s=i.address().port,t=`${o}${y}/auth/cli?port=${s}&state=${n}`;console.log("Opening browser to authenticate..."),console.log(`If it doesn't open, visit: ${t}`);let{exec:r}=await import("child_process"),l=process.platform==="darwin"?`open "${t}"`:process.platform==="win32"?`start "${t}"`:`xdg-open "${t}"`;r(l),console.log("Waiting for authentication..."),setTimeout(()=>{console.error(`
Login timed out. Try again.`),i.close(),process.exit(1)},12e4)}async function w(e){let o=e.join(" ");o||(console.error("Usage: vanio ask <question>"),console.error('       vanio ask "What are my check-ins today?"'),process.exit(1));let n={query:o};for(let t=0;t<e.length;t++)e[t]==="--reservation"&&e[t+1]?n.reservationId=parseInt(e[++t],10):e[t]==="--listing"&&e[t+1]?n.listingId=parseInt(e[++t],10):e[t]==="--thread"&&e[t+1]&&(n.threadId=e[++t]);let i=e.filter((t,r)=>!(t.startsWith("--")||r>0&&e[r-1].startsWith("--")));n.query=i.join(" "),n.query||(console.error("No query provided."),process.exit(1));let s=await k(`${y}/chat`,"POST",n);if(console.log(s.answer),s.sources?.length){console.log(""),console.log("Sources:");for(let t of s.sources)console.log(`  - ${t.label} (${t.type})`)}s.usage&&console.error(`
[${s.usage.model} | ${s.usage.inputTokens+s.usage.outputTokens} tokens | $${s.usage.cost?.toFixed(4)||"?"}]`)}async function j(e){let n=(await import("readline")).createInterface({input:process.stdin,output:process.stdout}),i=`cli-${Date.now()}`,s=[];console.log('Vanio AI \u2014 interactive chat (type "exit" to quit)'),console.log("\u2500".repeat(50));let t=()=>{n.question(`
You: `,async r=>{let l=r.trim();if(!l||l==="exit"||l==="quit"){n.close();return}s.push({role:"user",content:l});let a=await k(`${y}/chat`,"POST",{query:l,threadId:i,conversationHistory:s.slice(-20)});console.log(`
Vanio: ${a.answer}`),s.push({role:"assistant",content:a.answer}),t()})};t()}async function F(){let e=await k(`${y}/chat`,"GET");console.log(`Service: ${e.service}`),console.log(`Version: ${e.version}`),console.log(`Tools:   ${e.tools}`),console.log(`Status:  ${e.status}`)}async function W(e){let o=u();if(e[0]==="set"&&e[1]&&e[2]){let n=e[1];n==="apiKey"||n==="baseUrl"?(o[n]=e[2],f(o),console.log(`Set ${n} = ${e[2]}`)):console.error(`Unknown config key: ${n}. Valid keys: apiKey, baseUrl`);return}if(e[0]==="get"&&e[1]){console.log(o[e[1]]||"(not set)");return}console.log(`API Key:  ${o.apiKey?o.apiKey.slice(0,8)+"...":"(not set)"}`),console.log(`Base URL: ${o.baseUrl||v}`),console.log(`Config:   ${m}`)}function P(){console.log(`
Vanio AI CLI \u2014 manage your vacation rental portfolio via AI

Usage:
  vanio <command> [options]

Commands:
  login                    Authenticate via browser (opens Vanio dashboard)
  login <api-key>          Authenticate with an API key directly
  logout                   Remove saved credentials
  ask <question>           Ask Vanio AI a question (single query)
  chat                     Start interactive conversation
  status                   Check API connection
  config                   Show current configuration
  config set <key> <val>   Set config value (apiKey, baseUrl)
  help                     Show this help

Ask Options:
  --reservation <id>       Scope question to a specific reservation
  --listing <id>           Scope question to a specific listing
  --thread <id>            Continue a conversation thread

Examples:
  vanio ask "What are today's check-ins?"
  vanio ask "What's the WiFi password at Alpine Lodge?" --listing 42
  vanio ask "Why was this guest charged extra?" --reservation 214303
  vanio chat
  vanio config set baseUrl https://api.vanio.ai

Environment:
  VANIO_API_KEY            API key (overrides config file)
  VANIO_API_URL            Base URL (overrides config file)
`.trim())}async function H(){let[e,...o]=process.argv.slice(2);switch(e){case"login":return R(o);case"logout":{let n=u();delete n.apiKey,f(n),console.log("Logged out. API key removed.");return}case"ask":return w(o);case"chat":return j(o);case"status":return F();case"config":return W(o);case"help":case"--help":case"-h":return P();case void 0:P();break;default:return w([e,...o])}}H().catch(e=>{console.error(e.message||e),process.exit(1)});


path: package.json
content: {
  "name": "@vanio-ai/cli",
  "version": "0.1.0",
  "description": "Vanio AI CLI — manage your vacation rental portfolio from the command line",
  "main": "dist/index.cjs",
  "bin": {
    "vanio": "dist/index.cjs"
  },
  "scripts": {
    "build": "node build.mjs",
    "dev": "node build.mjs && node dist/index.cjs"
  },
  "dependencies": {},
  "keywords": ["vanio", "property-management", "vacation-rental", "ai-agent", "openclaw", "mcp", "cli"],
  "license": "MIT",
  "files": ["dist", "SKILL.md", "README.md"]
}


path: src/index.ts
content: import { readFileSync, writeFileSync, mkdirSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
import { createServer } from 'http'
import { randomBytes } from 'crypto'

// ─── Config ──────────────────────────────────────────────────────────────────

const CONFIG_DIR = join(homedir(), '.config', 'vanio')
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
const DEFAULT_BASE_URL = 'https://api.vanio.ai'
// Production rewrites /:path* → /api/:path* so we don't include /api prefix
const API_PREFIX = '/v1'

interface Config {
  apiKey?: string
  baseUrl?: string
}

function loadConfig(): Config {
  try {
    return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
  } catch {
    return {}
  }
}

function saveConfig(config: Config) {
  mkdirSync(CONFIG_DIR, { recursive: true })
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
}

function getApiKey(): string {
  const envKey = process.env.VANIO_API_KEY
  if (envKey) return envKey
  const config = loadConfig()
  if (config.apiKey) return config.apiKey
  console.error('Error: No API key configured.')
  console.error('Run: vanio login <api-key>')
  console.error('Or set VANIO_API_KEY environment variable.')
  process.exit(1)
}

function getBaseUrl(): string {
  return process.env.VANIO_API_URL || loadConfig().baseUrl || DEFAULT_BASE_URL
}

// ─── HTTP Client ─────────────────────────────────────────────────────────────

async function apiCall(
  path: string,
  method: 'GET' | 'POST' = 'GET',
  body?: any,
): Promise<any> {
  const url = `${getBaseUrl()}${path}`
  const [REDACTED_SECRET])

  const res = await fetch(url, {
    method,
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': apiKey,
    },
    ...(body ? { body: JSON.stringify(body) } : {}),
  })

  if (!res.ok) {
    const err = await res.json().catch(() => ({ message: res.statusText }))
    console.error(`Error ${res.status}: ${err.message || err.error || res.statusText}`)
    process.exit(1)
  }

  return res.json()
}

// ─── Commands ────────────────────────────────────────────────────────────────

async function cmdLogin(args: string[]) {
  // If a key is passed directly, use it (for scripts/CI)
  if (args[0] && !args[0].startsWith('--')) {
    const config = loadConfig()
    config.[REDACTED_SECRET]
    saveConfig(config)
    console.log('API key saved to ~/.config/vanio/config.json')
    return
  }

  const baseUrl = process.env.VANIO_API_URL || loadConfig().baseUrl || DEFAULT_BASE_URL
  const state = randomBytes(16).toString('hex')

  // Start a temporary local server to receive the callback
  const server = createServer((req, res) => {
    const url = new URL(req.url || '/', `http://127.0.0.1`)

    if (url.pathname === '/callback') {
      const key = url.searchParams.get('key')
      const returnedState = url.searchParams.get('state')
      const user = url.searchParams.get('user') || ''

      if (returnedState !== state) {
        res.writeHead(400, { 'Content-Type': 'text/html' })
        res.end('<h1>State mismatch — login aborted</h1><p>Close this tab and try again.</p>')
        server.close()
        console.error('Error: state mismatch. Possible CSRF attack. Try again.')
        process.exit(1)
        return
      }

      if (!key) {
        res.writeHead(400, { 'Content-Type': 'text/html' })
        res.end('<h1>No API key received</h1><p>Close this tab and try again.</p>')
        server.close()
        console.error('Error: no API key received from server.')
        process.exit(1)
        return
      }

      // Save the key
      const config = loadConfig()
      config.apiKey = key
      config.baseUrl = baseUrl
      saveConfig(config)

      // Immediately redirect to strip the key from the URL bar, then show success
      res.writeHead(200, { 'Content-Type': 'text/html' })
      res.end(`
        <html>
          <head><title>Vanio CLI</title></head>
          <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff;">
            <div style="text-align: center;">
              <h1 style="font-size: 2rem;">✓ Logged in to Vanio</h1>
              <p style="color: #888;">You can close this tab and return to your terminal.</p>
            </div>
          </body>
          <script>history.replaceState(null, '', '/')</script>
        </html>
      `)

      console.log(`\nLogged in as ${user || 'unknown'}`)
      console.log('API key saved to ~/.config/vanio/config.json')

      setTimeout(() => { server.close(); process.exit(0) }, 1000)
      return
    }

    res.writeHead(404)
    res.end()
  })

  // Find a free port
  await new Promise<void>((resolve) => {
    server.listen(0, '127.0.0.1', () => resolve())
  })

  const port = (server.address() as any).port
  const authUrl = `${baseUrl}${API_PREFIX}/auth/cli?port=${port}&state=${state}`

  console.log('Opening browser to authenticate...')
  console.log(`If it doesn't open, visit: ${authUrl}`)

  // Open browser
  const { exec } = await import('child_process')
  const openCmd = process.platform === 'darwin'
    ? `open "${authUrl}"`
    : process.platform === 'win32'
      ? `start "${authUrl}"`
      : `xdg-open "${authUrl}"`

  exec(openCmd)

  console.log('Waiting for authentication...')

  // Timeout after 2 minutes
  setTimeout(() => {
    console.error('\nLogin timed out. Try again.')
    server.close()
    process.exit(1)
  }, 120_000)
}

async function cmdAsk(args: string[]) {
  const query = args.join(' ')
  if (!query) {
    console.error('Usage: vanio ask <question>')
    console.error('       vanio ask "What are my check-ins today?"')
    process.exit(1)
  }

  const opts: any = { query }

  // Parse flags
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--reservation' && args[i + 1]) {
      opts.reservationId = parseInt(args[++i], 10)
    } else if (args[i] === '--listing' && args[i + 1]) {
      opts.listingId = parseInt(args[++i], 10)
    } else if (args[i] === '--thread' && args[i + 1]) {
      opts.threadId = args[++i]
    }
  }

  // Re-join non-flag args as query
  const queryParts = args.filter((a, i) => {
    if (a.startsWith('--')) return false
    if (i > 0 && args[i - 1].startsWith('--')) return false
    return true
  })
  opts.query = queryParts.join(' ')

  if (!opts.query) {
    console.error('No query provided.')
    process.exit(1)
  }

  const result = await apiCall(`${API_PREFIX}/chat`, 'POST', opts)

  console.log(result.answer)

  if (result.sources?.length) {
    console.log('')
    console.log('Sources:')
    for (const s of result.sources) {
      console.log(`  - ${s.label} (${s.type})`)
    }
  }

  if (result.usage) {
    console.error(`\n[${result.usage.model} | ${result.usage.inputTokens + result.usage.outputTokens} tokens | $${result.usage.cost?.toFixed(4) || '?'}]`)
  }
}

async function cmdChat(args: string[]) {
  const readline = await import('readline')
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })

  const threadId = `cli-${Date.now()}`
  const history: Array<{ role: 'user' | 'assistant'; content: string }> = []

  console.log('Vanio AI — interactive chat (type "exit" to quit)')
  console.log('─'.repeat(50))

  const prompt = () => {
    rl.question('\nYou: ', async (input) => {
      const trimmed = input.trim()
      if (!trimmed || trimmed === 'exit' || trimmed === 'quit') {
        rl.close()
        return
      }

      history.push({ role: 'user', content: trimmed })

      const result = await apiCall(`${API_PREFIX}/chat`, 'POST', {
        query: trimmed,
        threadId,
        conversationHistory: history.slice(-20),
      })

      console.log(`\nVanio: ${result.answer}`)
      history.push({ role: 'assistant', content: result.answer })

      prompt()
    })
  }

  prompt()
}

async function cmdStatus() {
  const result = await apiCall(`${API_PREFIX}/chat`, 'GET')
  console.log(`Service: ${result.service}`)
  console.log(`Version: ${result.version}`)
  console.log(`Tools:   ${result.tools}`)
  console.log(`Status:  ${result.status}`)
}

async function cmdConfig(args: string[]) {
  const config = loadConfig()

  if (args[0] === 'set' && args[1] && args[2]) {
    const key = args[1] as keyof Config
    if (key === 'apiKey' || key === 'baseUrl') {
      config[key] = args[2]
      saveConfig(config)
      console.log(`Set ${key} = ${args[2]}`)
    } else {
      console.error(`Unknown config key: ${key}. Valid keys: apiKey, baseUrl`)
    }
    return
  }

  if (args[0] === 'get' && args[1]) {
    console.log(config[args[1] as keyof Config] || '(not set)')
    return
  }

  // Show all config
  console.log(`API Key:  ${config.apiKey ? config.apiKey.slice(0, 8) + '...' : '(not set)'}`)
  console.log(`Base URL: ${config.baseUrl || DEFAULT_BASE_URL}`)
  console.log(`Config:   ${CONFIG_FILE}`)
}

function showHelp() {
  console.log(`
Vanio AI CLI — manage your vacation rental portfolio via AI

Usage:
  vanio <command> [options]

Commands:
  login                    Authenticate via browser (opens Vanio dashboard)
  login <api-key>          Authenticate with an API key directly
  logout                   Remove saved credentials
  ask <question>           Ask Vanio AI a question (single query)
  chat                     Start interactive conversation
  status                   Check API connection
  config                   Show current configuration
  config set <key> <val>   Set config value (apiKey, baseUrl)
  help                     Show this help

Ask Options:
  --reservation <id>       Scope question to a specific reservation
  --listing <id>           Scope question to a specific listing
  --thread <id>            Continue a conversation thread

Examples:
  vanio ask "What are today's check-ins?"
  vanio ask "What's the WiFi password at Alpine Lodge?" --listing 42
  vanio ask "Why was this guest charged extra?" --reservation 214303
  vanio chat
  vanio config set baseUrl https://api.vanio.ai

Environment:
  VANIO_API_KEY            API key (overrides config file)
  VANIO_API_URL            Base URL (overrides config file)
`.trim())
}

// ─── Main ────────────────────────────────────────────────────────────────────

async function main() {
  const [command, ...args] = process.argv.slice(2)

  switch (command) {
    case 'login':
      return cmdLogin(args)
    case 'logout': {
      const config = loadConfig()
      delete config.apiKey
      saveConfig(config)
      console.log('Logged out. API key removed.')
      return
    }
    case 'ask':
      return cmdAsk(args)
    case 'chat':
      return cmdChat(args)
    case 'status':
      return cmdStatus()
    case 'config':
      return cmdConfig(args)
    case 'help':
    case '--help':
    case '-h':
      return showHelp()
    case undefined:
      showHelp()
      break
    default:
      // If no known command, treat the entire args as a query
      return cmdAsk([command, ...args])
  }
}

main().catch((err) => {
  console.error(err.message || err)
  process.exit(1)
})


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)