npm.io
0.1.27 • Published 4d agoCLI

restaurant-cli

Licence
MIT
Version
0.1.27
Deps
5
Size
916 kB
Vulns
0
Weekly
500

restaurant-cli

Pluggable CLI for booking restaurant reservations across Resy, OpenTable, Tock, and SevenRooms. Every provider is an independent module that plugs into the same interface; the CLI, OpenClaw plugin, and Claude Code plugin all read from the provider registry, not from any one provider.

Agent-friendly. Every command supports --agent (= --json --compact --no-color --no-input --yes), --select for field projection, --csv for table output, --dry-run for preview, and typed exit codes. restaurant agent-context emits a structured JSON manifest of every command, flag, provider, and exit code for autonomous callers.

Install

npm i -g restaurant-cli
# or
npx restaurant-cli --help

OpenTable browser-automation support is optional (the API path doesn't need it):

npm i -g patchright
npx playwright install chromium

From a clone (for development)

git clone https://github.com/omarshahine/restaurant-cli.git
cd restaurant-cli
npm install        # `prepare` runs `npm run build` automatically
npm link           # puts `restaurant` on your PATH
From GitHub directly (no npm)
npm i -g github:omarshahine/restaurant-cli

The package's prepare script builds dist/ automatically during install, so the restaurant binary is on your PATH right away.

Quick start

# one-time credential setup per provider (email + password; token persisted)
restaurant setup resy

# sanity check — config, auth, scheduler health
restaurant doctor

# search
restaurant search "le bernardin"
restaurant search "carbone" --provider opentable

# availability + book (Resy)
restaurant availability --venue 1387 --date 2026-05-15 --party 2
restaurant book --venue 1387 --date 2026-05-15 --time 19:30 --party 2

# list + cancel
restaurant list --upcoming
restaurant cancel <reservation-id>

# snipe — queue a booking for when the reservation window opens
restaurant snipe --venue 1387 --date 2026-05-15 --time 19:30 --party 2 \
                 --release-at 2026-04-30T10:00-07:00
restaurant jobs list
restaurant jobs cancel <job-id>
restaurant jobs logs <job-id>

# OpenTable: slug → numeric ID, then fast availability via the GraphQL API path
restaurant lookup --slug carbone-new-york
restaurant availability --venue 8033 --date 2026-05-15 --party 2 --provider opentable

# OpenTable hand-off (no API booking; deep link → user confirms in browser)
restaurant book --venue 1046758 --date 2026-05-15 --time 19:00 --party 2 \
                --provider opentable

# multi-provider — search every registered provider and merge
restaurant search "alinea"

# soonest open slot per venue across all providers
restaurant earliest alinea,le-bernardin,smyth --within 14d --party 2

# Tock authenticated reads — import cookies from your logged-in Chrome session
# 1) open exploretock.com → DevTools → Application → Cookies, copy as JSON, save to ~/tock-cookies.json
# 2) restaurant auth login tock --from-file ~/tock-cookies.json
restaurant list --provider tock

# agent self-discovery
restaurant agent-context | jq '.commands[].name'

All destructive commands (book, cancel, jobs cancel, snipe) prompt for y/N confirmation. Pass --yes to skip — useful for scripts and the snipe fire-time self-invocation. Pass --agent for the full agent default set.

Agent mode

Every command honors a consolidated agent flag set:

Flag Effect
--agent All of the below: --json --compact --no-color --no-input --yes
--json JSON output
--csv CSV output (table/array results)
--compact Drop verbose fields from rows; keep id, name, status, time, date, etc.
--select id,name,time Project named fields (dotted paths) from the JSON result
--no-color Disable ANSI colors
--no-input Fail closed instead of prompting (paired with --yes for destructive commands)
--yes Skip y/N confirmation prompts
--dry-run Build and print the request envelope without firing

Env-var floors (override flags):

Var Effect
RESTAURANT_CLI_AGENT=1 Every command behaves as if --agent were passed
RESTAURANT_CLI_DRY_RUN=1 Every destructive command runs as --dry-run
RESTAURANT_CLI_ENABLE_SNIPE=1 Required to queue a snipe. Off by default — scheduled sniping is an unattended booking that fires later, loading your token at run time with no further confirmation. (snipe --dry-run previews without it.)
RESTAURANT_CLI_ENABLE_SITE_AUTOMATION=1 Required for OpenTable + Tock search/availability. Off by default — those providers have no official API and drive the live site (scraping / anti-bot bypass), which may violate the site's ToS. Resy and OpenTable bookUrl hand-off are unaffected.
RESTAURANT_CLI_OT_MODE=api|browser|auto OpenTable transport selector (when site automation is enabled)
RESTAURANT_CLI_TOCK_MODE=api|browser|auto Tock transport selector (when site automation is enabled)
RESTAURANT_CLI_TOCK_ALLOW_BOOK=1 Required to ever fire a real Tock book (default-off safety floor)

book also supports --idempotent — pre-flights list for an existing matching (venue, date, time, party) reservation and returns it instead of double-booking on retry. Used automatically by the snipe fire-time wrapper.

Exit codes

Code Meaning
0 success
2 usage error (bad flags, missing required arg)
3 not found (venue, reservation, slot, slug)
5 api error (provider 5xx, malformed response, capability miss)
6 auth error (missing/invalid credentials)
7 rate limited (HTTP 429)
10 config error (bad/missing config file)

restaurant doctor --fail-on stale|error returns non-zero when the corresponding health level is reached — useful for CI.

Self-describing manifest

restaurant agent-context emits a single JSON document describing every command, subcommand, flag (including type, default, required), every registered provider's capabilities, every documented env-var floor, and the exit-code table. An agent can run this once and learn the entire surface without scanning --help for each subcommand.

Commands

Command Status
setup <provider>
search <query>
doctor
version
availability ✓ (Resy + OpenTable API)
lookup --slug ✓ (OpenTable)
book ✓ (Resy)
list ✓ (Resy)
cancel ✓ (Resy)
snipe ✓ (Resy)
jobs list/cancel/logs
config get/set/path

Providers

Provider search availability book cancel list snipe bookUrl
Resy
OpenTable
Tock

Automation & Terms of Service. Resy uses a documented token. OpenTable and Tock have no official public API, so reading their availability means automating the live site — a stealth-patched browser (OpenTable) or a TLS-fingerprint-impersonating binary (Tock) to get past anti-bot protections. This may be against those sites' Terms of Service; use the OpenTable/Tock providers at your own risk. The CLI prints a one-time runtime notice when a browser-automation path runs. Resy-only use involves none of this.

Tock specifics

Tock anonymous reads (search, availability) shell out to table-reservation-goat-pp-cli — a Go binary by Matt Van Horn / Pejman Pour-Moezzi (Apache-2.0) that handles two things Node can't do cleanly: (1) Chrome TLS fingerprint impersonation to clear Cloudflare, and (2) sourcing the page-issued x-tock-session token Tock's React bundle mints client-side. Install it once:

npx -y @mvanhorn/printing-press install table-reservation-goat
# writes ~/go/bin/table-reservation-goat-pp-cli

Add ~/go/bin to your PATH or set RESTAURANT_CLI_TRG_BIN to the binary path. Then:

restaurant search "canlis" --provider tock --agent
restaurant availability --venue canlis --date 2026-05-12 --party 2 --provider tock --agent

restaurant doctor checks the binary is on disk and surfaces an install hint when missing.

What's not wired: list, cancel, book. Tock's authenticated calls require imported session cookies (path scaffolded via restaurant auth login tock --from-file <path>); the calls themselves aren't built yet. Tock book is form-submit page navigation (not XHR) and needs a chromedp-style flow with CVC prompting — upstream trg defers this too. The capability flags are honest-false for these.

OpenTable specifics

OpenTable has no public consumer API and Akamai Bot Manager blocks raw HTTP. There are two live paths:

  • API path (default, fast) — direct /dapi/fe/gql POSTs with a CSRF token scraped from the homepage and a persisted-query SHA256 hash for the RestaurantsAvailability operation. No browser. Approach ported (clean reimplementation, no code copied) from Jeff Steinbok's openclaw-hub OpenTable plugin. Jeff's Python uses curl_cffi for Chrome TLS fingerprinting; Node's undici uses a different fingerprint, so this path may 403 on Akamai depending on IP/region.
  • Browser path (slow, reliable)patchright (stealth-patched Playwright fork) + persistent Chrome profile + channel: "chrome" + ~4.5s mouse jitter defeats Akamai reliably. First run opens a headed Chrome window for ~5-10s; headless trips Akamai.

Mode is picked at call time via RESTAURANT_CLI_OT_MODE:

  • auto (default): try API first, fall back to browser on 403.
  • api: API-only, errors if blocked.
  • browser: skip API, go straight to patchright.

When OpenTable rotates the persisted-query hash (rare), set OPENTABLE_AVAILABILITY_HASH=<sha256> to override without a code change. Extract a fresh hash from the network tab on opentable.com.

Search is browser-only — OpenTable doesn't expose a public text-search GraphQL operation, only an autocomplete that needs DOM driving. Use restaurant lookup --slug <slug> if you already know the URL slug; that path is API-only and skips the browser.

Booking completion is intentionally not available through the API — OpenTable confirmation requires a logged-in session + real user interaction, and automated confirmation has historically tripped bot-detection and accidentally completed real reservations (see mikehe123/opentable-reservations). The bookUrl capability generates a /restref/client hand-off URL (verified live 2026-04-18) that OpenTable redirects into its own booking flow with the time-slot picker rendered — you complete the reservation yourself.

Architecture

Four consumers of the same core:

  1. Plain CLIrestaurant <subcommand>
  2. Libraryimport { providers, Scheduler } from "restaurant-cli"
  3. OpenClaw plugin — registers 6 provider-agnostic tools (restaurant_search, restaurant_availability, restaurant_book, restaurant_schedule_snipe, restaurant_list, restaurant_cancel) via the host
  4. Claude Code plugin — skill + router agent + provider-specific agents (resy-agent, opentable-agent) + slash commands (/restaurant, /restaurant-setup, /restaurant-book, /restaurant-snipe, /restaurant-jobs), all shelling out to the CLI

The pluggable seam:

src/providers/
  types.ts         ← Provider interface + ProviderCapabilities
  registry.ts      ← runtime dispatcher
  bootstrap.ts     ← the ONLY file that knows every provider
  resy/            ← first provider; future modules are peer directories
  opentable/       ← second provider; proves the seam
  # tock/, sevenrooms/ — added the same way

Adding a new provider is a two-file change: create src/providers/<name>/ implementing Provider, add one line to bootstrap.ts. No core-code changes.

OpenClaw plugin

npm i -g restaurant-cli
openclaw plugins install restaurant-cli
restaurant setup resy-openclaw    # auth + mirror creds into OpenClaw config
# Restart the OpenClaw gateway

The -openclaw suffix on setup is the bridge: restaurant setup resy persists credentials to ~/.secrets.env + ~/.config/restaurant-cli/config.yaml (CLI store); appending -openclaw additionally registers them for the gateway-side tools. Works for any provider — opentable-openclaw, tock-openclaw, etc.

How the mirror stores secrets. Sensitive values (auth tokens, session cookies) are never written to disk by the mirror. plugins.entries.restaurant-cli.config in ~/.openclaw/openclaw.json holds only an environment SecretRef — {source:"env", id:"RESY_AUTH_TOKEN"} — that resolves at runtime from the gateway environment. Those are exactly the env vars the plugin manifest declares under metadata.openclaw.requires.env, so the gateway supplies them (for this setup they come from the chezmoi+age encrypted ~/.secrets.env). The plugin therefore keeps no plaintext credential store of its own — there is no ~/.openclaw/secrets.json written. Non-secret fields (the public apiKey, email) stay inline, and the installer leaves no secret-bearing .bak files (it purges any left by older versions). The standalone CLI is unaffected — it keeps reading ~/.secrets.env + config.yaml. (Legacy installs that still carry a {source:"file", provider:"secrets"} ref keep resolving for backward compat.)

From a clone
git clone https://github.com/omarshahine/restaurant-cli.git
cd restaurant-cli
./scripts/install-openclaw.sh     # deps + build + link bin + openclaw plugin register
restaurant setup resy-openclaw

Claude Code plugin

Installs from Omar's private marketplace:

/plugin install restaurant-cli@omarshahine-plugins

See the skills/, agents/, and commands/ directories for the plugin surface.

Config & credential storage

  • ~/.config/restaurant-cli/config.yaml — non-secret config (default provider, timezone, logging) plus a tokenRef SecretRef pointer, never the token value.
  • The tool persists no secret of its own — it is env-first. restaurant setup / restaurant auth login print an export KEY=… line; you place it in your own environment (e.g. ~/.secrets.env, or a chezmoi/age-encrypted source). The token is resolved from the environment at runtime via config.yaml's {source:env} ref.
  • OpenClaw plugin mode — the gateway config (~/.openclaw/openclaw.json) likewise stores only an environment SecretRef per token; the value is read from the gateway env (the manifest-required RESY_AUTH_TOKEN, etc.).

Security note. The tool never uses the macOS Keychain and never writes a secret file of its own — neither ~/.secrets.env nor ~/.openclaw/secrets.json. You provide the token through your environment; whatever file you choose to keep it in (commonly ~/.secrets.env, 0600) holds a bearer credential in plaintext, so treat it as sensitive, keep it out of backups/syncs you don't control, and rotate by editing it. Scheduled at jobs read the token from that env file at fire time. Tokens are bearer credentials scoped to reservation actions on your own account.

How tokens are obtained. These providers don't issue partner API keys for this kind of use, so the credential is your own session material:

  • Resyrestaurant setup resy exchanges your email + password for a durable token via Resy's login endpoint (password is used once, never stored).
  • OpenTable / Tock — you export your own logged-in session cookies from your browser (DevTools → Application → Cookies → copy as JSON) and pass them with auth login <provider> --from-file. Nothing is harvested without you doing this deliberately for your own account.

restaurant config masks secret-looking values by default; pass --show-secrets to print them in full. restaurant setup, auth login, and snipe print a one-time notice about what they store and (for snipe) that the job books unattended at fire time.

Snipe how it works

restaurant snipe queues a booking to fire at a specific release time via POSIX at:

  • --release-at takes ISO8601 with timezone offset (e.g. 2026-04-30T10:00-07:00). Must be in the future.
  • The job is wrapped in a bash script that sources ~/.secrets.env at fire time so the auth token is present. Never written to the at-spool in plaintext.
  • Fire-time output goes to ~/.local/state/restaurant-cli/logs/<job-id>.log — JSONL snipe.start/snipe.end events plus the restaurant book --yes --json output.
  • Inspect via restaurant jobs list / logs <id>; cancel via jobs cancel <id> (calls atrm + removes the local metadata row).

Resolution is per-minute (POSIX at limit). Sub-minute sniping requires a daemon backend, which is not implemented.

Attribution

The Resy provider module is a clean TypeScript reimplementation inspired by the design of lgrees/resy-cli (MIT). No code was copied; endpoint-level citations are inline in src/providers/resy/client.ts. See NOTICE.

License

MIT — see LICENSE.

Keywords