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 --helpOpenTable browser-automation support is optional (the API path doesn't need it):
npm i -g patchright
npx playwright install chromiumFrom 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 PATHFrom GitHub directly (no npm)
npm i -g github:omarshahine/restaurant-cliThe 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-cliAdd ~/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 --agentrestaurant 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/gqlPOSTs with a CSRF token scraped from the homepage and a persisted-query SHA256 hash for theRestaurantsAvailabilityoperation. No browser. Approach ported (clean reimplementation, no code copied) from Jeff Steinbok's openclaw-hub OpenTable plugin. Jeff's Python usescurl_cffifor Chrome TLS fingerprinting; Node'sundiciuses 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:
- Plain CLI —
restaurant <subcommand> - Library —
import { providers, Scheduler } from "restaurant-cli" - OpenClaw plugin — registers 6 provider-agnostic tools (
restaurant_search,restaurant_availability,restaurant_book,restaurant_schedule_snipe,restaurant_list,restaurant_cancel) via the host - 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 gatewayThe -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-openclawClaude 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 atokenRefSecretRef pointer, never the token value.- The tool persists no secret of its own — it is env-first.
restaurant setup/restaurant auth loginprint anexport 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 viaconfig.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-requiredRESY_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:
- Resy —
restaurant setup resyexchanges 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-attakes 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.envat 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— JSONLsnipe.start/snipe.endevents plus therestaurant book --yes --jsonoutput. - Inspect via
restaurant jobs list/logs <id>; cancel viajobs cancel <id>(callsatrm+ 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.