@series-a/cli 1.1.0 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to `@series-a/cli` (and the paired `@series-a/mcp` server).
4
4
 
5
+ ## Unreleased — claims review dashboard
6
+
7
+ ### Added
8
+ - `setu claims` — review dashboard showing pending claim requests, counts by status and unclaimed profiles scored for readiness (with what's still missing)
9
+ - `setu claims show|approve|decline <claim-id>` (`--reason`, `--yes`), plus `setu claims --status approved|rejected|all`
10
+ - MCP tools `setu_claims_dashboard`, `setu_claims_list`, `setu_claim_detail` and `setu_claim_decide`
11
+
12
+ ## 1.3.0 — lists, full profiles and image uploads
13
+
14
+ ### Added
15
+ - `practice list` and `dentist list` — browse seeded profiles, with `--limit`, `--search` and (for clinicians) `--practice`
16
+ - `setu status <id> --full` and `setu status --dentist` — the complete enriched profile including photos, prices, services and attached clinicians
17
+ - `setu <type> images --upload <file|url>` and `--upload-logo <file|url>` — upload photos straight into Setu storage with your CLI key, no third-party hosting needed
18
+ - `dentist create` now honours `--photo`, `--gallery`, `--phone`, `--website`, `--booking` and `--bio`
19
+ - MCP tool `setu_profile_upload_image`; `setu_profile_status` gained `full`, `list`, `limit`, `search` and `practice_id`; `setu_dentist_create` gained photo/gallery/contact fields
20
+
21
+ ## 1.2.0 — booking links and full profile-settings editing
22
+
23
+ ### Added
24
+ - `practice update` / `dentist update` `--booking <url>` and `--website <url>` shortcuts for setting the booking and website links without `--set`
25
+ - `--set-enrichment path=value` for nested practice profile values (positioning summary/archetype/marketing descriptions, amenities, default currency), so every field on the practice profile settings screen is editable from the CLI
26
+ - `--fields` now also lists the available `--set-enrichment` paths
27
+ - MCP `setu_profile_update` accepts `enrichment_set`
28
+
5
29
  ## 1.1.0 — editing, pricing and media
6
30
 
7
31
  ### Added
package/README.md CHANGED
@@ -48,6 +48,8 @@ are listed at the end so you can retry them.
48
48
  setu practice claim-link <practice-id>
49
49
  setu status # every seeded profile and its claim state
50
50
  setu status <practice-id> # one profile plus its claim attempts
51
+ setu status <profile-id> --full # every field, photos, prices, services, clinicians
52
+ setu status --dentist # list clinicians instead of practices
51
53
  ```
52
54
 
53
55
  Send the claim link in outreach. The practice signs in, and is verified either
@@ -56,10 +58,31 @@ contact address on their Google listing. On success the profile transfers to
56
58
  them, pre-filled, and they continue through the normal onboarding and
57
59
  subscription flow.
58
60
 
61
+ ## Reviewing claims
62
+
63
+ ```bash
64
+ setu claims # pending claims + unclaimed profiles by readiness
65
+ setu claims --ready # only profiles ready to promote
66
+ setu claims --status all # every claim, any status
67
+ setu claims show <claim-id> # claimant, method, attempts, target profile
68
+ setu claims approve <claim-id> # transfer the profile to the claimant
69
+ setu claims decline <claim-id> --reason "Not the practice owner"
70
+ ```
71
+
72
+ Approving a practice claim makes the claimant the owner, adds an owner
73
+ membership and marks the practice's clinicians as claimed. A claim can only be
74
+ approved once the claimant has signed in, so there's an account to transfer to.
75
+
59
76
  ## Clinicians
60
77
 
61
78
  ```bash
62
- setu dentist create --name "Dr Jane Roe" --practice <id> --profile-url https://…/team/jane
79
+ setu dentist create --name "Dr Jane Roe" --practice <id> --profile-url https://…/team/jane \
80
+ --photo https://…/jane.jpg --gallery "https://…/a.jpg,https://…/b.jpg" \
81
+ --phone 02392000000 --website https://… --booking https://…/book
82
+
83
+ setu practice list --limit 100 --search "smile"
84
+ setu dentist list --practice <practice-id>
85
+ setu dentist status <dentist-id> --full
63
86
  ```
64
87
 
65
88
  ## Flags
@@ -94,10 +117,26 @@ else is reported and skipped before any API call is made.
94
117
  setu practice update <practice-id> --fields # what you can set
95
118
  setu practice update <practice-id> --set tagline="Gentle care" --set emergency_available=true
96
119
  setu dentist update <dentist-id> --set bio="20 years in implants"
120
+
121
+ # Booking / website link shortcuts
122
+ setu practice update <practice-id> --booking https://clinic.co.uk/book
123
+ setu dentist update <dentist-id> --booking https://clinic.co.uk/book --website https://clinic.co.uk
97
124
  ```
98
125
 
99
126
  Lists take comma-separated values, e.g. `--set services="Implants,Whitening"`.
100
127
 
128
+ Everything on the practice profile settings screen is settable. The few values that
129
+ live in the nested profile record — positioning, amenities, default currency — use
130
+ `--set-enrichment`:
131
+
132
+ ```bash
133
+ setu practice update <practice-id> --set-enrichment positioning.summary="Calm, modern care in Leeds"
134
+ setu practice update <practice-id> --set-enrichment amenities="Wi-Fi,Free parking,Step-free access"
135
+ setu practice update <practice-id> --set-enrichment positioning.archetype=boutique
136
+ setu practice update <practice-id> --set parking_info="Two-hour free bay on site" \
137
+ --set accessibility="Step-free,Hearing loop" --set languages="English,Polish"
138
+ ```
139
+
101
140
  ## Photos and logos
102
141
 
103
142
  ```bash
@@ -110,6 +149,15 @@ setu practice images <practice-id> --logo https://.../logo.png
110
149
  setu dentist images <dentist-id> --logo https://.../headshot.jpg # main photo
111
150
  ```
112
151
 
152
+ Upload from your machine (or copy a remote image into Setu storage) instead of
153
+ linking to someone else's server:
154
+
155
+ ```bash
156
+ setu practice images <practice-id> --upload ./reception.jpg
157
+ setu practice images <practice-id> --upload-logo ./logo.png
158
+ setu dentist images <dentist-id> --upload-logo https://.../headshot.jpg
159
+ ```
160
+
113
161
  ## Prices and services
114
162
 
115
163
  ```bash
package/bin/setu.mjs CHANGED
@@ -71,20 +71,35 @@ ${c.bold("Commands")}
71
71
  [--unpublish] [--yes]
72
72
  practice update <practice-id> Edit fields on a seeded profile
73
73
  --set field=value repeatable, e.g. --set tagline="Gentle care"
74
+ --set-enrichment path=value e.g. --set-enrichment amenities="Wi-Fi|Parking"
75
+ [--booking <url>] [--website <url>] shortcuts for the booking / website links
74
76
  [--fields] list the fields you can set
75
77
  practice images <practice-id> Manage photos on a seeded profile
76
78
  [--add <url>] [--remove <url|index>] [--order "url1,url2"]
77
79
  [--logo <url>] [--replace "url1,url2"] [--clear] [--list]
80
+ [--upload <file|url>] [--upload-logo <file|url>]
78
81
  practice pricing <practice-id> Manage treatment prices
79
82
  [--set "Implant=1200"] [--remove "Implant"] [--clear] [--list]
83
+ practice list List seeded practices
84
+ [--limit 50] [--search <name>]
80
85
  dentist create --name "Dr Jane Roe" Seed one clinician
81
86
  [--practice <id>] [--profile-url <url>] [--town <town>]
87
+ [--photo <url>] [--gallery "url1,url2"] [--phone <n>]
88
+ [--website <url>] [--booking <url>] [--bio "..."]
89
+ dentist list List seeded clinicians
90
+ [--limit 50] [--search <name>] [--practice <id>]
82
91
  dentist update <dentist-id> Edit fields on a seeded clinician
83
92
  dentist images <dentist-id> Manage a clinician's photos
84
93
  dentist pricing <dentist-id> Manage a clinician's treatment prices
85
94
  dentist delete <dentist-id> Remove an unclaimed seeded clinician
86
95
  [--unpublish] [--yes]
87
- status [<practice-id>] List or inspect seeded profiles
96
+ status [<profile-id>] List or inspect seeded profiles
97
+ [--dentist] [--full] [--limit 50] [--search <name>]
98
+ claims Review dashboard: pending claims + ready profiles
99
+ [--status pending|approved|rejected|all] [--dentist] [--ready] [--limit 50]
100
+ claims show <claim-id> Full detail for one claim
101
+ claims approve <claim-id> Hand the profile to the claimant
102
+ claims decline <claim-id> Decline a claim [--reason "..."]
88
103
 
89
104
  ${c.bold("Global")} --json --yes --verbose --operator <name> --version
90
105
  `);
@@ -134,9 +149,13 @@ async function cmdProfileUpdate(type, id) {
134
149
  // The server answers with the whitelist when nothing valid is sent.
135
150
  try {
136
151
  const r = await call("cli-profile-update", { id: id || "00000000-0000-0000-0000-000000000000", profile_type: type, read: true });
137
- if (JSON_OUT) return out({ fields: r.allowed_fields });
152
+ if (JSON_OUT) return out({ fields: r.allowed_fields, enrichment_paths: r.allowed_enrichment_paths ?? [] });
138
153
  heading(`Fields you can set on a ${type}`);
139
154
  log(" " + r.allowed_fields.join(", "));
155
+ if (r.allowed_enrichment_paths?.length) {
156
+ heading("Extra fields (use --set-enrichment path=value)");
157
+ log(" " + r.allowed_enrichment_paths.join(", "));
158
+ }
140
159
  return;
141
160
  } catch (e) {
142
161
  const fields = e.data?.allowed_fields;
@@ -150,26 +169,77 @@ async function cmdProfileUpdate(type, id) {
150
169
  }
151
170
  return;
152
171
  }
153
- if (!id) { fail(`Usage: setu ${type} update <id> --set field=value`); process.exit(1); }
172
+ if (!id) { fail(`Usage: setu ${type} update <id> [--set field=value] [--booking <url>] [--website <url>]`); process.exit(1); }
154
173
 
155
- const pairs = asList(flags.set);
156
- if (!pairs.length) { fail("Pass at least one --set field=value (or --fields to see what you can set)."); process.exit(1); }
157
174
  const set = {};
175
+ const pairs = asList(flags.set);
158
176
  for (const pair of pairs) {
159
177
  const raw = String(pair);
160
178
  const eq = raw.indexOf("=");
161
179
  if (eq < 1) { fail(`Bad --set value: "${raw}". Use field=value.`); process.exit(1); }
162
180
  set[raw.slice(0, eq).trim()] = raw.slice(eq + 1);
163
181
  }
182
+ const enrichmentSet = {};
183
+ for (const pair of asList(flags["set-enrichment"])) {
184
+ const raw = String(pair);
185
+ const eq = raw.indexOf("=");
186
+ if (eq < 1) { fail(`Bad --set-enrichment value: "${raw}". Use path=value.`); process.exit(1); }
187
+ enrichmentSet[raw.slice(0, eq).trim()] = raw.slice(eq + 1);
188
+ }
189
+ if (flags.booking !== undefined) set.booking_url = flags.booking === true ? "" : String(flags.booking);
190
+ if (flags.website !== undefined) set[type === "dentist" ? "website_url" : "website"] = flags.website === true ? "" : String(flags.website);
191
+ if (!Object.keys(set).length && !Object.keys(enrichmentSet).length) { fail("Pass at least one --set field=value, --set-enrichment path=value, --booking <url> or --website <url> (or --fields to see what you can set)."); process.exit(1); }
164
192
 
165
- const res = await call("cli-profile-update", { id, profile_type: type, set });
193
+ const res = await call("cli-profile-update", {
194
+ id,
195
+ profile_type: type,
196
+ set,
197
+ ...(Object.keys(enrichmentSet).length ? { enrichment_set: enrichmentSet } : {}),
198
+ });
166
199
  if (JSON_OUT) return out(res);
167
200
  ok(`${res.name} — updated ${res.updated.join(", ")}`);
168
201
  if (res.rejected?.length) warn(`Ignored unknown field(s): ${res.rejected.join(", ")}`);
169
202
  }
170
203
 
204
+ const MIME_BY_EXT = {
205
+ jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
206
+ webp: "image/webp", svg: "image/svg+xml", gif: "image/gif",
207
+ };
208
+
209
+ /** A local path becomes base64; a URL is fetched server-side. */
210
+ function uploadPayload(src) {
211
+ const value = String(src);
212
+ if (/^https?:\/\//i.test(value)) return { source_url: value };
213
+ const ext = value.split(".").pop().toLowerCase();
214
+ return {
215
+ data: readFileSync(value).toString("base64"),
216
+ content_type: MIME_BY_EXT[ext] || "image/jpeg",
217
+ filename: value.split(/[\\/]/).pop(),
218
+ };
219
+ }
220
+
221
+ async function uploadImages(type, id, sources, attach) {
222
+ const urls = [];
223
+ for (const src of sources) {
224
+ const s = spinner(`Uploading ${src}`);
225
+ try {
226
+ const res = await call("cli-profile-upload", { id, profile_type: type, attach, ...uploadPayload(src) });
227
+ s.stop();
228
+ urls.push(res.url);
229
+ if (!JSON_OUT) ok(`${attach === "logo" ? "Logo" : "Photo"} uploaded — ${c.dim(res.url)}`);
230
+ } catch (e) { s.stop(); throw e; }
231
+ }
232
+ return urls;
233
+ }
234
+
171
235
  async function cmdProfileImages(type, id) {
172
- if (!id) { fail(`Usage: setu ${type} images <id> [--add <url>] [--remove <url|index>] [--order "a,b"] [--logo <url>]`); process.exit(1); }
236
+ if (!id) { fail(`Usage: setu ${type} images <id> [--add <url>] [--upload <file>] [--remove <url|index>] [--order "a,b"] [--logo <url>] [--upload-logo <file>]`); process.exit(1); }
237
+
238
+ const uploads = splitList(flags.upload);
239
+ const logoUploads = splitList(flags["upload-logo"]);
240
+ const uploaded = [];
241
+ if (uploads.length) uploaded.push(...await uploadImages(type, id, uploads, "gallery"));
242
+ if (logoUploads.length) await uploadImages(type, id, logoUploads.slice(0, 1), "logo");
173
243
 
174
244
  const images = {};
175
245
  if (splitList(flags.add).length) images.add = splitList(flags.add);
@@ -501,34 +571,170 @@ async function cmdDentistCreate() {
501
571
  town: typeof flags.town === "string" ? flags.town : undefined,
502
572
  postcode: typeof flags.postcode === "string" ? flags.postcode : undefined,
503
573
  email: typeof flags.email === "string" ? flags.email : undefined,
574
+ phone: typeof flags.phone === "string" ? flags.phone : undefined,
575
+ website_url: typeof flags.website === "string" ? flags.website : undefined,
576
+ booking_url: typeof flags.booking === "string" ? flags.booking : undefined,
577
+ bio: typeof flags.bio === "string" ? flags.bio : undefined,
578
+ photo_url: typeof flags.photo === "string" ? flags.photo : undefined,
579
+ gallery: splitList(flags.gallery),
504
580
  });
505
581
  s.stop();
506
582
  if (JSON_OUT) return out(res);
507
583
  ok("Clinician seeded as unclaimed");
508
- table([["Dentist", res.dentist_id], ["AI enriched", res.enriched ? "yes" : "no"], ["Claim link", res.claim_url]]);
584
+ table([
585
+ ["Dentist", res.dentist_id],
586
+ ["AI enriched", res.enriched ? "yes" : "no"],
587
+ ["Photo", res.photo_url ?? "—"],
588
+ ["Claim link", res.claim_url],
589
+ ]);
509
590
  } catch (e) { s.stop(); throw e; }
510
591
  }
511
592
 
512
- async function cmdStatus(id) {
513
- const res = await call("cli-profile-status", id ? { id } : { list: true, limit: flags.limit });
593
+ function printProfileList(res) {
594
+ heading(`Seeded ${res.profile_type === "dentist" ? "clinicians" : "practices"} (${res.items.length})`);
595
+ for (const p of res.items) {
596
+ const state = p.claim_status === "claimed" ? c.green("claimed") : p.is_live ? c.green("live") : c.yellow(p.claim_status);
597
+ log(` ${state.padEnd(20)} ${String(p.name).slice(0, 38).padEnd(40)} ${c.dim(p.id)}`);
598
+ }
599
+ }
600
+
601
+ async function cmdProfileList(type) {
602
+ const res = await call("cli-profile-status", {
603
+ list: true,
604
+ profile_type: type,
605
+ limit: flags.limit,
606
+ search: typeof flags.search === "string" ? flags.search : undefined,
607
+ practice_id: type === "dentist" && typeof flags.practice === "string" ? flags.practice : undefined,
608
+ });
609
+ if (JSON_OUT) return out(res);
610
+ printProfileList(res);
611
+ }
612
+
613
+ async function cmdStatus(id, type = flags.dentist ? "dentist" : "practice") {
614
+ const res = await call(
615
+ "cli-profile-status",
616
+ id
617
+ ? { id, profile_type: type, full: !!flags.full }
618
+ : { list: true, profile_type: type, limit: flags.limit, search: typeof flags.search === "string" ? flags.search : undefined },
619
+ );
514
620
  if (JSON_OUT) return out(res);
515
- if (res.items) {
516
- heading(`Seeded profiles (${res.items.length})`);
517
- for (const p of res.items) {
518
- const state = p.claim_status === "claimed" ? c.green("claimed") : p.is_live ? c.green("live") : c.yellow(p.claim_status);
519
- log(` ${state.padEnd(20)} ${String(p.name).slice(0, 38).padEnd(40)} ${c.dim(p.id)}`);
621
+ if (res.items) return printProfileList(res);
622
+
623
+ const p = res.profile ?? res.practice;
624
+ heading(p.name);
625
+ table([
626
+ ["Type", res.profile_type],
627
+ ["Status", p.claim_status],
628
+ ["Live", p.is_live ? "yes" : "no"],
629
+ ["Source", `${p.source} (${p.created_by_operator ?? "—"})`],
630
+ ["Claim link", p.claim_url],
631
+ ]);
632
+
633
+ if (flags.full) {
634
+ const gallery = res.gallery ?? [];
635
+ heading(`Photos (${gallery.length})`);
636
+ gallery.forEach((url, i) => log(` ${String(i).padEnd(3)} ${c.dim(url)}`));
637
+ log(` ${c.dim(res.profile_type === "dentist" ? "main photo" : "logo")}: ${res.logo ?? c.dim("none")}`);
638
+
639
+ const pricing = res.pricing ?? [];
640
+ heading(`Prices (${pricing.length})`);
641
+ for (const row of pricing) {
642
+ const amount = row.amount_to ? `${row.amount}–${row.amount_to}` : row.amount ?? "—";
643
+ log(` ${String(row.label).slice(0, 38).padEnd(40)} ${amount}`);
644
+ }
645
+
646
+ if (res.dentists) {
647
+ heading(`Clinicians (${res.dentists.length})`);
648
+ for (const d of res.dentists) log(` ${String(d.name).slice(0, 38).padEnd(40)} ${c.dim(d.id)}`);
520
649
  }
650
+ }
651
+
652
+ heading(`Claims (${res.claims.length})`);
653
+ for (const cl of res.claims) log(` ${cl.status.padEnd(10)} ${cl.claimant_email} ${c.dim(cl.method)}`);
654
+ }
655
+
656
+ function printClaimRow(cl) {
657
+ const who = String(cl.claimant_email ?? "—").slice(0, 30).padEnd(32);
658
+ const name = String(cl.profile?.name ?? cl.profile_type ?? "—").slice(0, 26).padEnd(28);
659
+ const flag = cl.awaiting_signin ? c.yellow(" (not signed in)") : "";
660
+ log(` ${String(cl.status).padEnd(9)} ${who} ${name} ${c.dim(cl.method ?? "")}${flag}`);
661
+ log(` ${c.dim(cl.id)}`);
662
+ }
663
+
664
+ async function cmdClaims(sub, arg) {
665
+ if (sub === "show") {
666
+ if (!arg) throw new Error("Usage: setu claims show <claim-id>");
667
+ const res = await call("cli-claims", { action: "detail", claim_id: arg });
668
+ if (JSON_OUT) return out(res);
669
+ heading(`Claim ${res.claim.id}`);
670
+ table([
671
+ ["Status", res.claim.status],
672
+ ["Claimant", res.claim.claimant_email],
673
+ ["Method", res.claim.method],
674
+ ["Attempts", res.claim.attempts],
675
+ ["Signed in", res.claim.claimant_user_id ? "yes" : "no"],
676
+ ["Profile", res.profile ? `${res.profile.name} (${res.claim.profile_type})` : "—"],
677
+ ["Readiness", res.profile ? `${res.profile.score}%` : "—"],
678
+ ["Missing", res.profile?.missing?.join(", ")],
679
+ ["Claim link", res.profile?.claim_url],
680
+ ]);
681
+ return;
682
+ }
683
+
684
+ if (sub === "approve" || sub === "decline" || sub === "reject") {
685
+ if (!arg) throw new Error(`Usage: setu claims ${sub} <claim-id>`);
686
+ if (!flags.yes && !JSON_OUT) {
687
+ const go = await confirm(`${sub === "approve" ? "Approve" : "Decline"} claim ${arg}?`);
688
+ if (!go) return warn("Cancelled.");
689
+ }
690
+ const res = await call("cli-claims", {
691
+ action: sub === "approve" ? "approve" : "decline",
692
+ claim_id: arg,
693
+ reason: typeof flags.reason === "string" ? flags.reason : undefined,
694
+ });
695
+ if (JSON_OUT) return out(res);
696
+ return ok(`Claim ${res.status} — ${res.claim_id}`);
697
+ }
698
+
699
+ if (sub && sub !== "list") throw new Error(`Unknown command: claims ${sub}`);
700
+
701
+ const status = typeof flags.status === "string" ? flags.status : "pending";
702
+ const profileType = flags.dentist ? "dentist" : undefined;
703
+ if (sub === "list" || (typeof flags.status === "string" && flags.status !== "pending")) {
704
+ const res = await call("cli-claims", { action: "list", status, profile_type: profileType, limit: flags.limit });
705
+ if (JSON_OUT) return out(res);
706
+ heading(`Claims — ${status} (${res.claims.length})`);
707
+ res.claims.forEach(printClaimRow);
708
+ if (!res.claims.length) log(c.dim(" none"));
521
709
  return;
522
710
  }
523
- heading(res.practice.name);
711
+
712
+ const res = await call("cli-claims", {
713
+ action: "dashboard",
714
+ profile_type: profileType,
715
+ ready_only: !!flags.ready,
716
+ limit: flags.limit,
717
+ });
718
+ if (JSON_OUT) return out(res);
719
+
720
+ heading("Claims");
524
721
  table([
525
- ["Status", res.practice.claim_status],
526
- ["Live", res.practice.is_live ? "yes" : "no"],
527
- ["Source", `${res.practice.source} (${res.practice.created_by_operator ?? "—"})`],
528
- ["Claim link", res.practice.claim_url],
722
+ ["Pending", res.counts.pending],
723
+ ["Approved", res.counts.approved],
724
+ ["Rejected", res.counts.rejected],
529
725
  ]);
530
- heading(`Claims (${res.claims.length})`);
531
- for (const cl of res.claims) log(` ${cl.status.padEnd(10)} ${cl.claimant_email} ${c.dim(cl.method)}`);
726
+
727
+ heading(`Pending review (${res.pending_claims.length})`);
728
+ res.pending_claims.forEach(printClaimRow);
729
+ if (!res.pending_claims.length) log(c.dim(" nothing waiting"));
730
+
731
+ heading(`Unclaimed profiles — ${res.unclaimed.ready}/${res.unclaimed.total} ready, ${res.unclaimed.not_live} ready but offline`);
732
+ for (const u of res.unclaimed.items) {
733
+ const badge = u.ready ? c.green("ready") : c.yellow("gaps ");
734
+ log(` ${badge} ${String(u.readiness_score + "%").padStart(4)} ${String(u.name).slice(0, 30).padEnd(32)} ${c.dim(u.profile_type)}${u.is_live ? "" : c.dim(" offline")}`);
735
+ if (u.missing.length) log(` ${c.dim("missing: " + u.missing.join(", "))}`);
736
+ }
737
+ if (!res.unclaimed.items.length) log(c.dim(" none"));
532
738
  }
533
739
 
534
740
  async function main() {
@@ -544,6 +750,7 @@ async function main() {
544
750
  case "logout": return cmdLogout();
545
751
  case "whoami": return cmdWhoami();
546
752
  case "status": return cmdStatus(sub);
753
+ case "claims": return cmdClaims(sub, arg);
547
754
  case "practice":
548
755
  switch (sub) {
549
756
  case "create": return cmdPracticeCreate();
@@ -555,6 +762,7 @@ async function main() {
555
762
  case "update": return cmdProfileUpdate("practice", arg);
556
763
  case "images": return cmdProfileImages("practice", arg);
557
764
  case "pricing": return cmdProfilePricing("practice", arg);
765
+ case "list": return cmdProfileList("practice");
558
766
  default:
559
767
  fail(`Unknown command: practice ${sub ?? ""}`.trim());
560
768
  usage();
@@ -563,6 +771,8 @@ async function main() {
563
771
  }
564
772
  case "dentist":
565
773
  if (sub === "create") return cmdDentistCreate();
774
+ if (sub === "list") return cmdProfileList("dentist");
775
+ if (sub === "status") return cmdStatus(arg, "dentist");
566
776
  if (sub === "update") return cmdProfileUpdate("dentist", arg);
567
777
  if (sub === "images") return cmdProfileImages("dentist", arg);
568
778
  if (sub === "pricing") return cmdProfilePricing("dentist", arg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-a/cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "description": "Setu CLI — build, review and publish claimable practice and dentist profiles.",
6
6
  "type": "module",