smily_cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f157eb28c398c7038493c5c558f5faec807d8e2b89761046ba137f086049fb6c
4
+ data.tar.gz: ff3e7be3ae92f10ddbd32cb86764ca84b59b3b76ca92fc4fb07e3634890115c1
5
+ SHA512:
6
+ metadata.gz: 0b165be215a87ccba5e4f9411bc94683d3894cf6d96da72646e0589d622f06e3088f43013dbb1a47d7b4802be8b2a188e47c550ff632e2678868a7867c36043e
7
+ data.tar.gz: 4cf8ffdcac8182837f1311949aa2f94902a6661f00467e546c28cd662977815f77e6f4c3801a736e3d610fb72d72df735b843e44d62db821aa5ba43ac7e3fe35
@@ -0,0 +1,68 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: Test (Ruby ${{ matrix.ruby }})
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ ruby: ["3.2", "3.3", "3.4"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby }}
25
+ bundler-cache: true
26
+ - name: Run tests
27
+ run: bundle exec rspec
28
+
29
+ rubocop:
30
+ name: RuboCop
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - name: Set up Ruby
35
+ uses: ruby/setup-ruby@v1
36
+ with:
37
+ ruby-version: "3.4"
38
+ bundler-cache: true
39
+ - name: Run RuboCop
40
+ run: bundle exec rubocop --format github
41
+
42
+ brakeman:
43
+ name: Brakeman
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - name: Set up Ruby
48
+ uses: ruby/setup-ruby@v1
49
+ with:
50
+ ruby-version: "3.4"
51
+ bundler-cache: true
52
+ # This is a plain Ruby gem (not a Rails app), so Brakeman needs --force.
53
+ # -z makes it exit non-zero if any security warning is found.
54
+ - name: Run Brakeman
55
+ run: bundle exec brakeman --force --no-progress --quiet -z
56
+
57
+ bundler-audit:
58
+ name: Bundler Audit
59
+ runs-on: ubuntu-latest
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+ - name: Set up Ruby
63
+ uses: ruby/setup-ruby@v1
64
+ with:
65
+ ruby-version: "3.4"
66
+ bundler-cache: true
67
+ - name: Audit dependencies for known CVEs
68
+ run: bundle exec bundler-audit check --update
data/.rubocop.yml ADDED
@@ -0,0 +1,70 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - "bin/**/*"
7
+ - "vendor/**/*"
8
+ - "tmp/**/*"
9
+ - "pkg/**/*"
10
+
11
+ # We document with YARD; a class comment isn't always warranted.
12
+ Style/Documentation:
13
+ Enabled: false
14
+
15
+ # The CLI leans on descriptive multi-branch case/format methods; keep the
16
+ # metrics as guardrails rather than hard style rules for this codebase.
17
+ Metrics/AbcSize:
18
+ Max: 40
19
+ Metrics/MethodLength:
20
+ Max: 30
21
+ Metrics/ClassLength:
22
+ Max: 250
23
+ Metrics/ModuleLength:
24
+ Max: 250
25
+ Exclude:
26
+ - "spec/**/*"
27
+ Metrics/BlockLength:
28
+ # Thor groups its instance helpers in a `no_commands do ... end` block.
29
+ AllowedMethods:
30
+ - "no_commands"
31
+ Exclude:
32
+ - "spec/**/*"
33
+ - "*.gemspec"
34
+ Metrics/CyclomaticComplexity:
35
+ Max: 13
36
+ Metrics/PerceivedComplexity:
37
+ Max: 13
38
+ Metrics/ParameterLists:
39
+ Max: 6
40
+ # Keyword args are self-documenting at the call site and don't suffer the
41
+ # positional-argument ordering problem this cop guards against.
42
+ CountKeywordArgs: false
43
+
44
+ # Our error classes take keyword args (status:, body:), so `raise Error.new(...)`
45
+ # is intentional and clearer than the exploded form.
46
+ Style/RaiseArgs:
47
+ Enabled: false
48
+
49
+ # Endless methods and long single-purpose lines read fine here.
50
+ Layout/LineLength:
51
+ Max: 120
52
+ AllowedPatterns:
53
+ - '\A\s*#' # long comment/URLs
54
+
55
+ Style/EndlessMethod:
56
+ EnforcedStyle: allow_single_line
57
+
58
+ # Heredocs and message strings frequently mix quote styles intentionally.
59
+ Style/StringLiterals:
60
+ EnforcedStyle: double_quotes
61
+ Style/WordArray:
62
+ Enabled: true
63
+
64
+ # Guard clauses that raise with a message are clearer un-negated at times.
65
+ Style/GuardClause:
66
+ Enabled: false
67
+
68
+ # Specs use `expect { }.to raise_error { |e| ... }` blocks.
69
+ Lint/AmbiguousBlockAssociation:
70
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ ## [Unreleased]
2
+
3
+ - MCP `list` now speaks the API v3 filter DSL: `attribute[op]=value` applies an
4
+ operator (`eq`, `not_eq`, `gt`, `gteq`, `lt`, `lteq`, `in`, `matches`,
5
+ `does_not_match`) and repeating an attribute combines conditions with AND
6
+ (e.g. a numeric or date range). Values are coerced to numbers/booleans; ISO
7
+ 8601 timestamps stay strings.
8
+ - Fixed the `booking_discount_codes` / `booking_discount_code_usages` resources
9
+ to use their flat top-level v3 paths (were pointing at a nested
10
+ `booking/discount_codes` path).
11
+ - Internal: the library now autoloads via Zeitwerk instead of hand-maintained
12
+ `require_relative` chains.
13
+ - Tests: added VCR-cassette acceptance tests exercising the REST and MCP flows
14
+ end-to-end (list, Link-header pagination, 404 handling, operator/range filter).
15
+ - Security: `auth client-credentials` / `refresh` / `exchange` no longer dump the
16
+ full token payload (including the refresh token) when their output is piped or
17
+ captured. The full payload is now opt-in via an explicit `-o json` /
18
+ `SMILY_OUTPUT=json`; the default stays access-token-only.
19
+ - Fixed a spurious "results may be truncated" warning from `--all` / `--limit`
20
+ when a collection finished on exactly the `--max-pages` page; the warning now
21
+ fires only when a further page is actually being left behind.
22
+ - Config: a structurally valid config file carrying a value `safe_load` refuses
23
+ (e.g. an unquoted date) now fails with a clean configuration error instead of
24
+ an uncaught YAML backtrace.
25
+ - Argv parsing: a forgotten value for a global flag (`smily --token rentals
26
+ list`) now fails with a clear "Missing value for --token" error instead of
27
+ swallowing the subcommand into the flag (which produced a baffling "command
28
+ not found" or a request fired with a bogus value). Use `--flag=value` when a
29
+ value legitimately collides with a command name.
30
+
31
+ ## [0.1.0]
32
+
33
+ Initial (unreleased) version: a command-line client for the BookingSync (Smily)
34
+ API v3.
35
+
36
+ - Per-resource commands (`list`, `get`, `create`, `update`, `delete`) for the
37
+ documented API v3 endpoints, generated from a resource registry.
38
+ - Raw `smily api <method> <path>` escape hatch for any endpoint/verb, with
39
+ optional `--paginate`.
40
+ - OAuth helpers: `auth client-credentials`, `auth refresh`, `auth exchange`,
41
+ `auth authorize-url`, `auth status`, `auth token`.
42
+ - Config profiles (`smily config …`) stored at `~/.config/smily/config.yml`
43
+ (0600), with flag > env > profile resolution.
44
+ - Account scoping via `--account-id` / `SMILY_ACCOUNT_ID` for
45
+ Client-Credentials-flow tokens.
46
+ - Output formats: table (default on a TTY), json, yaml, csv, ndjson; sparse
47
+ fieldsets, filtering, sideloading and pagination (`--all`, `--limit`,
48
+ `--page`, `--per-page`).
49
+ - MCP mode (`smily mcp`): talk to a BookingSync MCP server (Model Context
50
+ Protocol) over the Streamable-HTTP JSON-RPC 2.0 transport. Includes `info`,
51
+ `tools`, and a low-level `call`, plus `list`/`get`/`schema`/`resources`
52
+ convenience wrappers over the generic dispatcher tools, and `mcp login` for
53
+ its dedicated setup. Separate credential (`mcp_token`) and endpoint
54
+ (`mcp_url`, default `<base-url>/mcp`); `--account-id` scopes tool calls.
55
+ - `whoami`, `resources`, `completion` (bash/zsh/fish) and `version` commands.
56
+ - Built on the official `bookingsync-api` client.
57
+ - Security: `--verbose` redacts `Authorization`/`client_secret`/tokens; config
58
+ written `0600` in a `0700` directory; warns when credentials would be sent
59
+ over non-HTTPS.
60
+ - Reliability: `--max-pages` backstop on auto-pagination (default 1000);
61
+ `--limit` sizes the page request; `--retry N` retries 429s honoring
62
+ `Retry-After` / `X-RateLimit-Reset`.
63
+ - Ergonomics: single-value global flags may precede the subcommand;
64
+ `create --path` for nested creates.
65
+ - CI: `bundler-audit` dependency CVE scan.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Karol Galanciak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,387 @@
1
+ # smily — BookingSync (Smily) API v3 CLI
2
+
3
+ `smily` is an ergonomic, comprehensive command-line client for the
4
+ [BookingSync (Smily) API v3](https://developers.bookingsync.com/reference).
5
+
6
+ It aims to be *the* tool you reach for when you want to talk to API v3 from a
7
+ terminal or a script:
8
+
9
+ - **Per-resource commands** — `smily rentals list`, `smily bookings get 42`,
10
+ `smily clients create …` — for every documented endpoint.
11
+ - **A raw escape hatch** — `smily api get <path>` reaches *anything* the API
12
+ exposes, with pagination and wrapping out of your way.
13
+ - **OAuth built in** — client-credentials, refresh, and authorization-code
14
+ helpers; no need to hand-roll token requests.
15
+ - **Config profiles** — keep tokens for multiple accounts/environments and
16
+ switch with `--profile`.
17
+ - **Output that fits your workflow** — a readable table on a TTY, clean JSON
18
+ when piped, plus YAML, CSV and NDJSON.
19
+ - **Pagination, filtering, sparse fieldsets, sideloading** — first-class flags.
20
+
21
+ It is built on top of the official
22
+ [`bookingsync-api`](https://github.com/BookingSync/bookingsync-api) gem, so the
23
+ HTTP, JSON:API and error-handling behavior matches the reference client.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ gem install smily_cli
29
+ ```
30
+
31
+ Or with Bundler:
32
+
33
+ ```ruby
34
+ gem "smily_cli"
35
+ ```
36
+
37
+ This installs the `smily` executable.
38
+
39
+ ## Setup (first run)
40
+
41
+ Once the gem is installed, get from zero to your first authenticated call in
42
+ four steps:
43
+
44
+ ```bash
45
+ # 1. Confirm the executable is on your PATH
46
+ smily version
47
+
48
+ # 2. Get an API token. For scripts/automation the Client Credentials flow is
49
+ # simplest: you need your application's Client ID and Secret from the
50
+ # BookingSync developer portal (https://www.bookingsync.com/oauth/applications).
51
+ # --save writes the token to the default profile at
52
+ # ~/.config/smily/config.yml (created 0600, since it holds secrets).
53
+ smily auth client-credentials --client-id "$CLIENT_ID" --client-secret "$CLIENT_SECRET" --save
54
+
55
+ # 3. Verify the token actually works (calls /me)
56
+ smily auth status --check
57
+
58
+ # 4. Make your first real call
59
+ smily rentals list --limit 5
60
+ ```
61
+
62
+ A Client-Credentials token can read across every account that authorized your
63
+ app; pin one account with `--account-id <ID>` per call, or store it once:
64
+
65
+ ```bash
66
+ smily config set account_id <ID>
67
+ ```
68
+
69
+ Don't want to store anything? Every command also reads `--token` / `SMILY_TOKEN`
70
+ directly, so `SMILY_TOKEN=… smily rentals list` works with no config file at
71
+ all. See [Authentication](#authentication) for the Authorization-Code (per-user)
72
+ flow and [Configuration & profiles](#configuration--profiles) for multiple
73
+ environments.
74
+
75
+ ## Quick start
76
+
77
+ ```bash
78
+ # 1. Get a token (Client Credentials flow) and save it to the default profile
79
+ smily auth client-credentials --client-id "$CLIENT_ID" --client-secret "$CLIENT_SECRET" --save
80
+
81
+ # 2. Explore
82
+ smily resources # what can I talk to?
83
+ smily rentals list --limit 5 # a readable table
84
+ smily bookings list --filter status=booked from=2026-01-01
85
+
86
+ # 3. Fetch one, as JSON
87
+ smily rentals get 42 -o json
88
+
89
+ # 4. Anything the resource commands don't cover
90
+ smily api get rentals/42/bookings --paginate -o ndjson
91
+ ```
92
+
93
+ ## Authentication
94
+
95
+ API v3 uses OAuth 2.0 Bearer tokens. A token can be supplied (in order of
96
+ precedence) by:
97
+
98
+ 1. `--token` flag
99
+ 2. `SMILY_TOKEN` (or `BOOKINGSYNC_TOKEN`) environment variable
100
+ 3. the active config profile
101
+
102
+ ### Getting a token
103
+
104
+ ```bash
105
+ # Client Credentials — app-level, public scope, reads across all authorized
106
+ # accounts. Great for scripts. (Tokens last ~2h.)
107
+ smily auth client-credentials --client-id ID --client-secret SECRET --save
108
+
109
+ # Authorization Code — per-account access.
110
+ smily auth authorize-url --client-id ID --redirect-uri https://app.example.com/cb --scope "rentals_read bookings_read"
111
+ # (open the URL, approve, copy the ?code=… back)
112
+ smily auth exchange --client-id ID --client-secret SECRET --code CODE --redirect-uri https://app.example.com/cb --save
113
+
114
+ # Refresh an expiring token
115
+ smily auth refresh --client-id ID --client-secret SECRET --refresh-token RT --save
116
+
117
+ # Check what's configured (optionally validate against /me)
118
+ smily auth status --check
119
+ ```
120
+
121
+ `--save` stores the token (and any refresh token / client credentials) in the
122
+ active profile so subsequent commands just work.
123
+
124
+ ### Scoping a Client-Credentials token to one account
125
+
126
+ A Client-Credentials token spans every account that authorized your app. Pin a
127
+ single account with `--account-id` (sent as the `account_id` query parameter),
128
+ via `SMILY_ACCOUNT_ID`, or by storing it in a profile:
129
+
130
+ ```bash
131
+ smily rentals list --account-id 12345
132
+ smily config set account_id 12345 --profile acme
133
+ ```
134
+
135
+ For Authorization-Code tokens the account is implicit, so this is a no-op.
136
+
137
+ ## Configuration & profiles
138
+
139
+ Configuration lives in `~/.config/smily/config.yml` (override with
140
+ `SMILY_CONFIG`; honors `XDG_CONFIG_HOME`). The file is written with `0600`
141
+ permissions because it holds secrets.
142
+
143
+ ```bash
144
+ smily config init # interactive: store a token + base URL
145
+ smily config set token TOKEN --profile prod
146
+ smily config set account_id 123 --profile prod
147
+ smily config use prod # make "prod" the default profile
148
+ smily config list # profiles + settings (secrets masked)
149
+ smily config path # where's the file?
150
+ ```
151
+
152
+ Use a specific profile per command with `--profile NAME`, or set
153
+ `SMILY_PROFILE`.
154
+
155
+ ## Working with resources
156
+
157
+ Every registered resource supports the same verbs:
158
+
159
+ ```bash
160
+ smily <resource> list [options]
161
+ smily <resource> get ID [options]
162
+ smily <resource> create --data <json> # not on read-only resources
163
+ smily <resource> update ID --data <json>
164
+ smily <resource> delete ID [--yes]
165
+ ```
166
+
167
+ Run `smily resources` for the full catalog, or `smily <resource> help`.
168
+
169
+ ### Listing, filtering, pagination
170
+
171
+ ```bash
172
+ smily bookings list --filter status=booked --filter from=2026-01-01
173
+ smily rentals list --fields id name --limit 10
174
+ smily rentals list --all -o ndjson # every page, one JSON object per line
175
+ smily rentals list --page 2 --per-page 50 # a specific page
176
+ smily rentals list --include photos # sideload associations
177
+ ```
178
+
179
+ - `--filter k=v` / `--query k=v` — API query parameters (repeat or space-separate).
180
+ These are sent verbatim, so a resource's documented filters work directly
181
+ (`--filter status=booked from=2026-01-01`), and operator query params are
182
+ forwarded as-is where the endpoint supports them
183
+ (`--filter 'final_price[gteq]=600'`). For the *validated* operator/range DSL,
184
+ see [MCP mode](#advanced-filtering-operators--ranges).
185
+ - `--fields a b c` — sparse fieldset (also limits table/CSV columns).
186
+ - `--include a b` — sideload associations (returned under `linked`).
187
+ - `--limit N` — stop after N records (follows pages as needed; also sizes the
188
+ page request to `min(N, 100)`).
189
+ - `--all` — fetch every page.
190
+ - `--page` / `--per-page` — fetch one specific page.
191
+ - `--max-pages N` — hard backstop on auto-pagination (default 1000); the CLI
192
+ warns if it stops early.
193
+ - `--retry N` — retry `429 Too Many Requests` up to N times, waiting for the
194
+ rate-limit window (`Retry-After` / `X-RateLimit-Reset`). Off by default.
195
+
196
+ ### Creating & updating
197
+
198
+ Provide attributes as a JSON object; the `{ "<resource>": [ … ] }` envelope
199
+ that v3 expects is added for you:
200
+
201
+ ```bash
202
+ smily rentals create --data '{"name":"Villa Sunset","sleeps":6}'
203
+ smily clients create --data @client.json # from a file
204
+ echo '{"sleeps":8}' | smily rentals update 42 --data - # from stdin
205
+ ```
206
+
207
+ For nested creates (e.g. a booking under a rental), point `--path` at the
208
+ parent collection (the envelope key stays the resource's):
209
+
210
+ ```bash
211
+ smily bookings create --path rentals/42/bookings --data @booking.json
212
+ ```
213
+
214
+ ## The raw `api` command
215
+
216
+ `smily api <method> <path>` is the universal escape hatch — it can reach any
217
+ endpoint, with any verb, and sends your body verbatim:
218
+
219
+ ```bash
220
+ smily api get me
221
+ smily api get rentals --query per_page=5
222
+ smily api get bookings/123
223
+ smily api post rentals --data '{"rentals":[{"name":"X"}]}'
224
+ smily api get rentals --paginate -o json # combine all pages into one array
225
+ ```
226
+
227
+ By default it prints the raw JSON response envelope. With `--paginate` it
228
+ follows pagination and prints the combined records.
229
+
230
+ ## MCP mode (`smily mcp`)
231
+
232
+ Besides the REST API, `smily` can talk to a **BookingSync MCP server** (Model
233
+ Context Protocol) — the same servers an AI agent connects to. This is a separate
234
+ mode with its own credential (`mcp_token`) and endpoint (`mcp_url`, defaulting to
235
+ `<base-url>/mcp`).
236
+
237
+ ```bash
238
+ # One-time setup: store the MCP token (and optionally a non-default endpoint)
239
+ smily mcp login "$MCP_TOKEN"
240
+ smily mcp login "$MCP_TOKEN" --url https://www.bookingsync.com/mcp --profile prod
241
+
242
+ # Handshake + capabilities, and discover the tools the server exposes
243
+ smily mcp info
244
+ smily mcp tools
245
+
246
+ # Convenience wrappers that mirror the REST commands, over the generic
247
+ # list / get / resources / resource_schema tools:
248
+ smily mcp resources
249
+ smily mcp list rentals --limit 5
250
+ smily mcp list bookings --filter status=booked --account-id 42
251
+ smily mcp get rentals 42
252
+ smily mcp schema bookings
253
+
254
+ # Low-level escape hatch — call any tool with a raw JSON argument object:
255
+ smily mcp call list --args '{"resource":"rentals","limit":5}'
256
+ smily mcp call resource_schema --args @args.json
257
+ ```
258
+
259
+ The convenience wrappers resolve the generic tool names for you — they work
260
+ against a server that exposes `list`/`get`/… as well as one that prefixes them
261
+ (`api_v3_list`, `api_v3_get`, …).
262
+
263
+ ### Advanced filtering (operators & ranges)
264
+
265
+ `smily mcp list` speaks the API v3 filter DSL. A plain `attribute=value` is an
266
+ exact match; `attribute[op]=value` applies an operator; repeating an attribute
267
+ combines the conditions with AND (e.g. a range). Operators: `eq`, `not_eq`,
268
+ `gt`, `gteq`, `lt`, `lteq`, `in`, `matches`, `does_not_match` (exactly the set a
269
+ resource reports via `smily mcp schema <resource>`).
270
+
271
+ ```bash
272
+ # final_price > 1000
273
+ smily mcp list bookings --filter 'final_price[gt]=1000'
274
+
275
+ # 600 <= final_price < 800 (compound AND range)
276
+ smily mcp list bookings --filter 'final_price[gteq]=600' --filter 'final_price[lt]=800'
277
+
278
+ # starts on/after a date, and one of several references
279
+ smily mcp list bookings --filter 'start_at[gteq]=2026-01-01T00:00:00Z' --filter 'reference[in]=A123,B456'
280
+ ```
281
+
282
+ Numeric and boolean values are coerced automatically; ISO 8601 timestamps are
283
+ kept as strings. Use `smily mcp schema <resource>` to see which attributes are
284
+ filterable and the operators each accepts.
285
+
286
+ Credentials/endpoint resolve like everything else — `--mcp-token` /
287
+ `SMILY_MCP_TOKEN` / profile `mcp_token`, and `--mcp-url` / `SMILY_MCP_URL` /
288
+ profile `mcp_url`. `--account-id` scopes tool calls to a single account (as the
289
+ `account_id` tool argument), which is how a multi-account MCP token is pinned.
290
+ Output honors `-o` just like the REST commands.
291
+
292
+ Transport details: `smily` speaks the Streamable-HTTP JSON-RPC 2.0 transport,
293
+ performing the `initialize` handshake (capturing the `Mcp-Session-Id`) and
294
+ accepting either `application/json` or SSE-framed responses.
295
+
296
+ ## Output formats
297
+
298
+ `-o, --output` selects the format. The default is a human-readable **table** on
299
+ an interactive terminal, and **json** when the output is piped — so scripts get
300
+ machine-readable data automatically.
301
+
302
+ | Format | Notes |
303
+ |----------|---------------------------------------------------|
304
+ | `table` | Aligned grid (collections) or key/value (single). |
305
+ | `json` | Pretty JSON — array for lists, object for `get`. |
306
+ | `yaml` | YAML. |
307
+ | `csv` | Header + rows; nested values become JSON. |
308
+ | `ndjson` | One compact JSON object per line (a.k.a. `jsonl`).|
309
+
310
+ ```bash
311
+ smily rentals list -o csv --fields id name status > rentals.csv
312
+ smily bookings list --all -o ndjson | jq 'select(.status=="booked")'
313
+ ```
314
+
315
+ ## Global options
316
+
317
+ Single-value global flags work before or after the subcommand
318
+ (`smily -o json rentals list` and `smily rentals list -o json` are equivalent).
319
+ Array-valued flags (`--fields`, `--filter`) must follow the subcommand.
320
+
321
+ | Flag | Env | Meaning |
322
+ |------|-----|---------|
323
+ | `--profile NAME` | `SMILY_PROFILE` | Config profile to use |
324
+ | `--token TOKEN` | `SMILY_TOKEN` | OAuth access token (REST) |
325
+ | `--mcp-token TOKEN` | `SMILY_MCP_TOKEN` | MCP access token (`smily mcp`) |
326
+ | `--mcp-url URL` | `SMILY_MCP_URL` | MCP endpoint (default `<base-url>/mcp`) |
327
+ | `--base-url URL` | `SMILY_BASE_URL` | API base (default `https://www.bookingsync.com`) |
328
+ | `--account-id ID` | `SMILY_ACCOUNT_ID` | Scope to an account (client-credentials tokens) |
329
+ | `--max-pages N` | `SMILY_MAX_PAGES` | Auto-pagination backstop (default 1000) |
330
+ | `--retry N` | `SMILY_RETRY` | Retry 429s N times, honoring the rate-limit reset |
331
+ | `-o, --output` | `SMILY_OUTPUT` | Output format |
332
+ | `--fields a b c` | | Field/column selection |
333
+ | `--no-color` | `NO_COLOR` | Disable ANSI color |
334
+ | `-q, --quiet` | | Suppress status messages |
335
+ | `-v, --verbose` | `SMILY_VERBOSE` | Log HTTP traffic to stderr (secrets redacted) |
336
+
337
+ ### Security notes
338
+
339
+ - Config files are written `0600` in a `0700` directory; secrets are masked in
340
+ `smily config list` / `smily auth status`.
341
+ - `--verbose` scrubs `Authorization` / `client_secret` / tokens from the logged
342
+ traffic, so verbose output is safe to share.
343
+ - The CLI warns (like the GitHub CLI) if a credential would be sent over a
344
+ non-HTTPS endpoint.
345
+
346
+ ## Shell completion
347
+
348
+ ```bash
349
+ smily completion bash > /usr/local/etc/bash_completion.d/smily
350
+ smily completion zsh > "${fpath[1]}/_smily"
351
+ smily completion fish > ~/.config/fish/completions/smily.fish
352
+ ```
353
+
354
+ ## Exit codes
355
+
356
+ `0` success · `1` generic/API error · `2` usage error · `3` auth error
357
+ (401/403) · `4` not found (404) · `5` rate limited (429).
358
+
359
+ ## Using it as a library
360
+
361
+ The library layer is usable without Thor:
362
+
363
+ ```ruby
364
+ require "smily_cli"
365
+
366
+ client = SmilyCli::Client.new(token: ENV["SMILY_TOKEN"])
367
+ result = client.list("rentals", limit: 10)
368
+ result.records # => [ { "id" => 1, ... }, ... ]
369
+
370
+ puts SmilyCli::Formatters.render(result, format: "json")
371
+ ```
372
+
373
+ ## Development
374
+
375
+ ```bash
376
+ bin/setup # install dependencies
377
+ bundle exec rake # run the specs and RuboCop
378
+ bundle exec rspec # just the specs
379
+ bin/console # an IRB session with the gem loaded
380
+ ```
381
+
382
+ CI (GitHub Actions) runs the specs across Ruby 3.2–3.4, RuboCop, and Brakeman.
383
+
384
+ ## License
385
+
386
+ Available as open source under the terms of the
387
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ # RuboCop is a development-only dependency; only define the task when present so
9
+ # the gem still loads its Rakefile without it (e.g. in a production install).
10
+ begin
11
+ require "rubocop/rake_task"
12
+ RuboCop::RakeTask.new
13
+ rescue LoadError
14
+ task(:rubocop) { warn "rubocop is not available" }
15
+ end
16
+
17
+ task default: %i[spec rubocop]
data/exe/smily ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "smily_cli"
5
+
6
+ begin
7
+ SmilyCli.cli.start(ARGV)
8
+ rescue SmilyCli::Error => e
9
+ warn SmilyCli::Color.red("Error: #{e.message}")
10
+ exit e.exit_status
11
+ rescue Interrupt
12
+ warn "\nAborted."
13
+ exit 130
14
+ rescue Errno::EPIPE
15
+ # Downstream pipe closed (e.g. `smily ... | head`); exit quietly.
16
+ exit 0
17
+ end