Skip to content

Implement BookingSync API v3 command-line client#1

Merged
Azdaroth merged 18 commits into
masterfrom
first-commit
Jul 9, 2026
Merged

Implement BookingSync API v3 command-line client#1
Azdaroth merged 18 commits into
masterfrom
first-commit

Conversation

@Azdaroth

@Azdaroth Azdaroth commented Jul 7, 2026

Copy link
Copy Markdown
Member

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.

Azdaroth and others added 18 commits July 7, 2026 09:44
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>
@Azdaroth Azdaroth merged commit 08f49ec into master Jul 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant