Implement BookingSync API v3 command-line client#1
Merged
Conversation
Build the `smily` CLI on top of the official bookingsync-api client: per-resource list/get/create/update/delete for all documented v3 resources, a raw `api` escape hatch, OAuth helpers, config profiles, account scoping (--account-id for client-credentials tokens), and table/json/yaml/csv/ndjson output with filtering, sparse fieldsets and pagination. Add RSpec (WebMock) coverage, RuboCop, and a GitHub Actions CI running tests (Ruby 3.2-3.4), RuboCop and Brakeman. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
Speak the Streamable-HTTP JSON-RPC 2.0 transport to a BookingSync MCP server as a separate mode with its own credential (mcp_token) and endpoint (mcp_url, default <base-url>/mcp). Adds `mcp info/tools/call`, `list/get/schema/resources` convenience wrappers over the generic dispatcher tools, and `mcp login`. Account scoping reuses --account-id as the account_id tool argument. Zero new dependencies; covered by WebMock specs. Renames the internal `info` helper to `notice` to avoid shadowing the `mcp info` command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
Verbose mode dumped `Authorization: Bearer <token>` (REST + MCP) and the `client_secret` / tokens (OAuth) to stderr in the clear. Route all three debug streams through a Redaction scrubber so secrets become [REDACTED]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
Create ~/.config/smily as 0700 (only when we create it, so a shared parent is left alone) and open the config file with 0600 from creation instead of chmod-after-write, closing the brief world-readable window. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
Like the GitHub CLI, warn (to stderr) rather than hard-refuse when the resolved REST/OAuth/MCP endpoint is not HTTPS and a credential is about to be sent, so local test servers still work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
--all / --limit now stop after a configurable number of pages (--max-pages / SMILY_MAX_PAGES / profile max_pages, default 1000) and warn about possible truncation, so a server that keeps advertising a next link can't loop forever. Extract the follow-pages loop into a helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
… size Avoids paging a small --limit at the default 25/page: ask the API for exactly the number of rows needed (capped at the API max of 100). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
--retry N (SMILY_RETRY / profile) retries 429 Too Many Requests up to N times, waiting for Retry-After or X-RateLimit-Reset (capped). Also route pagination follow-up requests through the same error handling so an error on page 2+ maps to ApiError instead of escaping unhandled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
Thor doesn't pass a parent's class options to registered subcommands when they appear before the subcommand word, so `smily -o json rentals list` dropped -o. Normalize argv in CLI.start by hoisting leading single-value and boolean global flags to the end, where every command re-declares them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
`smily bookings create --path rentals/42/bookings --data ...` posts to a nested collection while keeping the resource envelope key, so nested creates no longer force a drop to `smily api`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
Add specs for the previously untested auth command layer (client-credentials /refresh/exchange/authorize-url/status/token, incl. --save persistence), the config subcommands (path/get/set/unset/use/remove/list masking) and the MCP get/schema/resources convenience wrappers. Render MCP get/schema as a single member (single:) rather than a one-element list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
New CI job runs `bundler-audit check --update` against the ruby-advisory-db to catch known CVEs in transitive dependencies. Currently: no vulnerabilities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
Cover --max-pages, --retry, create --path, global-flags-before-subcommand, and the verbose-redaction / HTTPS-warning / 0600 security notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
resources only special-cased -o json (ignoring -o yaml/csv/ndjson and SMILY_OUTPUT), and api read options["output"] directly (ignoring the env var). Route both through the shared output resolver: resources renders the grouped human view for table and Formatters for any other format; api uses the explicitly-requested format (flag or env), defaulting to the raw JSON envelope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
A well-formed-YAML-but-wrong-shape config (e.g. a hand edit leaving `profiles: []`) previously blew up with a raw NoMethodError deep in a later command, which the executable doesn't rescue as a CLI error. Validate that `profiles` (and each profile) is a mapping on load, raising ConfigurationError so it surfaces as a clean, exit-1 message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
An arbitrary MCP tool (`smily mcp call`) can return a scalar/array payload; table and CSV output assumed each record was a Hash and crashed with a raw NoMethodError. Coerce non-hash records into a single `value` column so any caller is safe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1zLtLLaFLKErBdmh18feY
…e tests - Advanced filtering: `smily mcp list` now speaks the API v3 filter DSL — `attribute[op]=value` operators (eq/not_eq/gt/gteq/lt/lteq/in/matches/ does_not_match), with a repeated attribute combining conditions as AND (numeric/date ranges). Values coerce to numbers/booleans; ISO 8601 stays a string. Reachable structurally over the real api_v3_list tool. - Compatibility fix: booking_discount_codes / booking_discount_code_usages now use their flat top-level v3 paths (matching the API and MCP), not a nested `booking/discount_codes` path (which was also the wrong write-envelope key). - Zeitwerk autoloading replaces the hand-maintained require_relative chains (errors.rb/version.rb required eagerly; CLI/OAuth/CLIOptions inflections). - VCR-cassette acceptance tests drive the CLI end-to-end: REST (list, Link pagination, 404 -> exit 4) and MCP (full JSON-RPC handshake, api_v3_-prefixed tool-name resolution, operator/range filter). - Avoid a double rate-limit warning in `smily api` verbose output. - README: dedicated first-run Setup section + advanced-filtering docs; CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- auth: only dump the full token payload (incl. refresh_token) on an explicitly requested `-o json` / SMILY_OUTPUT=json, not the resolved format — non-TTY defaults to json, which leaked the refresh token when piped/captured. Default stays access-token-only. - client: warn "results may be truncated" only when a further page is actually being dropped, not when a collection ends exactly on --max-pages. - config: rescue any Psych::Exception (e.g. a bare date -> DisallowedClass) as a clean ConfigurationError instead of an uncaught YAML backtrace. - cli: a forgotten value for a leading global flag (`smily --token rentals list`) now raises a clear "Missing value" error instead of swallowing the subcommand; use --flag=value when a value collides with a command name. - Regression specs for all four; CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Build the
smilyCLI on top of the official bookingsync-api client: per-resource list/get/create/update/delete for all documented v3 resources, a rawapiescape hatch, OAuth helpers, config profiles, account scoping (--account-id for client-credentials tokens), and table/json/yaml/csv/ndjson output with filtering, sparse fieldsets and pagination. Add RSpec (WebMock) coverage, RuboCop, and a GitHub Actions CI running tests (Ruby 3.2-3.4), RuboCop and Brakeman.