@setu-dental/cli
v1.3.0
Published
Setu CLI — build, review and publish claimable practice and dentist profiles.
Readme
Setu CLI (internal)
Build claimable practice and dentist profiles from the command line.
Install
cd packages/cli && npm link # gives you the `setu` command
setu login # paste the internal CLI keyThe key is the SETU_CLI_API_KEY secret from the Setu backend. It is stored at
~/.config/setu/config.json (chmod 600). You can also set SETU_CLI_API_KEY
and SETU_API_URL as environment variables in CI.
Build one profile
setu practice create --url https://smiledental.co.uk
setu practice create --name "Smile Dental" --town Portsmouth
setu practice review <draft-id>
setu practice publish <draft-id>create scrapes the practice website (services, pricing, hours, team, images),
pulls their Google listing (address, phone, rating, reviews, opening hours) and
returns a readiness score. Nothing is public until publish.
publish creates an unclaimed listing: it appears in patient search with an
"Unclaimed" badge, ranked below live subscribed practices, with enquiry capture
instead of booking. It also seeds any clinicians the AI detected (skip with
--no-dentists).
Bulk import
CSV with any of these columns: name, website, town, postcode, email, phone.
setu practice import practices.csv --publishRows are processed one at a time (Firecrawl / Google rate limits) and failures are listed at the end so you can retry them.
Claim links
setu practice claim-link <practice-id>
setu status # every seeded profile and its claim state
setu status <practice-id> # one profile plus its claim attempts
setu status <profile-id> --full # every field, photos, prices, services, clinicians
setu status --dentist # list clinicians instead of practicesSend the claim link in outreach. The practice signs in, and is verified either by their email domain matching their website, or by a code sent to the public contact address on their Google listing. On success the profile transfers to them, pre-filled, and they continue through the normal onboarding and subscription flow.
Reviewing claims
setu claims # pending claims + unclaimed profiles by readiness
setu claims --ready # only profiles ready to promote
setu claims --status all # every claim, any status
setu claims show <claim-id> # claimant, method, attempts, target profile
setu claims approve <claim-id> # transfer the profile to the claimant
setu claims decline <claim-id> --reason "Not the practice owner"Approving a practice claim makes the claimant the owner, adds an owner membership and marks the practice's clinicians as claimed. A claim can only be approved once the claimant has signed in, so there's an account to transfer to.
Clinicians
setu dentist create --name "Dr Jane Roe" --practice <id> --profile-url https://…/team/jane \
--photo https://…/jane.jpg --gallery "https://…/a.jpg,https://…/b.jpg" \
--phone 02392000000 --website https://… --booking https://…/book
setu practice list --limit 100 --search "smile"
setu dentist list --practice <practice-id>
setu dentist status <dentist-id> --fullFlags
--json for machine-readable output, --operator "Your Name" to attribute
seeded profiles to you.
Reliability
- Every request has a timeout (
SETU_TIMEOUT_MS, default 240s) and retries transient failures (network drops, 429, 5xx) with exponential backoff (SETU_RETRIES, default 3).Retry-Afteris honoured. --verboseprints retry attempts and stack traces.setu logoutremoves the stored key from this machine.- Unknown commands and failed bulk imports exit non-zero (safe for scripts/CI).
Bulk import
setu practice import practices.csv --dry-run # validate only
setu practice import practices.csv --publish \
--concurrency 3 --out results.json # run
setu practice import practices.csv --resume results.json # retry failures onlyRows need either a website/url, or a name plus town/city; anything
else is reported and skipped before any API call is made.
Editing seeded profiles
setu practice update <practice-id> --fields # what you can set
setu practice update <practice-id> --set tagline="Gentle care" --set emergency_available=true
setu dentist update <dentist-id> --set bio="20 years in implants"
# Booking / website link shortcuts
setu practice update <practice-id> --booking https://clinic.co.uk/book
setu dentist update <dentist-id> --booking https://clinic.co.uk/book --website https://clinic.co.ukLists take comma-separated values, e.g. --set services="Implants,Whitening".
Everything on the practice profile settings screen is settable. The few values that
live in the nested profile record — positioning, amenities, default currency — use
--set-enrichment:
setu practice update <practice-id> --set-enrichment positioning.summary="Calm, modern care in Leeds"
setu practice update <practice-id> --set-enrichment amenities="Wi-Fi,Free parking,Step-free access"
setu practice update <practice-id> --set-enrichment positioning.archetype=boutique
setu practice update <practice-id> --set parking_info="Two-hour free bay on site" \
--set accessibility="Step-free,Hearing loop" --set languages="English,Polish"Photos and logos
setu practice images <practice-id> --list
setu practice images <practice-id> --add https://.../photo.jpg
setu practice images <practice-id> --remove 2 # by index, or pass the URL
setu practice images <practice-id> --order "https://.../hero.jpg" # move these to the front
setu practice images <practice-id> --replace "https://a.jpg,https://b.jpg"
setu practice images <practice-id> --logo https://.../logo.png
setu dentist images <dentist-id> --logo https://.../headshot.jpg # main photoUpload from your machine (or copy a remote image into Setu storage) instead of linking to someone else's server:
setu practice images <practice-id> --upload ./reception.jpg
setu practice images <practice-id> --upload-logo ./logo.png
setu dentist images <dentist-id> --upload-logo https://.../headshot.jpgPrices and services
setu practice pricing <practice-id> --list
setu practice pricing <practice-id> --set "Dental implant=1200" --set "Whitening=300-450"
setu practice pricing <practice-id> --remove "Whitening"
setu dentist pricing <dentist-id> --set "Hygiene visit=75"
setu practice update <practice-id> --set services="Implants,Whitening,Invisalign"Practice prices update the practice's treatment list too. Dentist prices are saved as individual treatment rows shown on the clinician's profile.
Removing seeded profiles
setu practice delete <practice-id> --unpublish # hide from patients
setu practice delete <practice-id> # delete (unclaimed only)
setu dentist delete <dentist-id> --unpublish
setu dentist delete <dentist-id>Claimed or user-owned profiles are always refused.
Completing existing profiles (admin keys)
A key created with "Let this key complete claimed listings" ticked carries the
admin:profiles scope. It can read and improve any practice or clinician —
claimed, self-serve or seeded. It can never delete a record, transfer ownership,
resolve a claim, verify a profile or change whether it is live.
setu permissions # what this key may do
setu search "Islington Smiles" --website https://www.islingtonsmiles.co.uk/ --postcode "N1 1RU"
setu status --all # list every profile, not just seeded ones
# Preview first — nothing is written
setu practice enhance <id> \
--set description="…" --set booking_url=https://… \
--price "Root canal treatment=from £295" \
--logo https://site/logo.png --photo https://site/1.jpg --photo https://site/2.jpg \
--team team.json --source description=https://site/about --researched 2026-09-09
setu practice enhance <id> … --apply # save
setu practice enhance <id> --history # audit trail
setu practice enhance <id> --rollback <run-id>Rules the server enforces on every run:
- Records are never deleted, recreated, unclaimed, transferred or unpublished.
- Empty fields are filled; a populated field is only changed when you pass
--replace <field>. - A blank value never overwrites existing content.
- Photos are imported into Setu storage, deduplicated and added to the existing gallery.
- Prices keep their "from" qualifiers, ranges and conditions; conflicting sources are flagged instead of guessed, and expired offers are excluded.
- Clinicians are matched first (GDC number, then name + practice). Nothing is created unless you
pass
--create-dentists, and existing practice associations are only added to, never removed. Hygienists, therapists and orthodontic therapists keep their real role. - Runs are idempotent, audited, and can be rolled back.
--expect-updated <timestamp>refuses the write if the owner has edited the record in the meantime.
team.json example:
[{ "name": "Dr Marisa Monteiro", "role": "principal", "credentials": "Principal and Invisalign Dentist",
"gdc_number": "123456", "photo_url": "https://…", "source_url": "https://…/meet-the-team/" }]