@series-a/cli 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -1,183 +1,317 @@
1
- # Setu CLI (internal)
1
+ # @series-a/cli
2
2
 
3
- Build claimable practice and dentist profiles from the command line.
3
+ A command-line interface to the AdvisorPro platform — drive leads, campaigns, sequences and agents from your terminal. Built on top of the [`/agent-api`](https://github.com/series-a/advisoros/blob/main/docs/agent-api.md) M2M router, so anything the CLI does is auditable and workspace-scoped.
4
+
5
+ ---
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- cd packages/cli && npm link # gives you the `setu` command
9
- setu login # paste the internal CLI key
10
+ # Run without installing — always fetches latest
11
+ npx @series-a/cli@latest --help
12
+
13
+ # Pin a version (recommended for CI)
14
+ npx @series-a/cli@0.1.0 --help
15
+
16
+ # Or install globally
17
+ npm i -g @series-a/cli
18
+ advisoros --help
10
19
  ```
11
20
 
12
- The key is the `SETU_CLI_API_KEY` secret from the Setu backend. It is stored at
13
- `~/.config/setu/config.json` (chmod 600). You can also set `SETU_CLI_API_KEY`
14
- and `SETU_API_URL` as environment variables in CI.
21
+ Requires **Node.js ≥ 20**.
15
22
 
16
- ## Build one profile
23
+ > Releases are tagged `cli-vX.Y.Z` in the repo and published to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements). See [`CHANGELOG.md`](./CHANGELOG.md).
17
24
 
18
- ```bash
19
- setu practice create --url https://smiledental.co.uk
20
- setu practice create --name "Smile Dental" --town Portsmouth
21
- setu practice review <draft-id>
22
- setu practice publish <draft-id>
23
- ```
25
+ ## 1. Get an API key
24
26
 
25
- `create` scrapes the practice website (services, pricing, hours, team, images),
26
- pulls their Google listing (address, phone, rating, reviews, opening hours) and
27
- returns a readiness score. Nothing is public until `publish`.
27
+ 1. Sign in to AdvisorPro.
28
+ 2. Open **Settings → API Keys**.
29
+ 3. Click **New key** → choose **Workspace** scope (recommended).
30
+ 4. Copy the key — you won't see it again.
28
31
 
29
- `publish` creates an **unclaimed** listing: it appears in patient search with an
30
- "Unclaimed" badge, ranked below live subscribed practices, with enquiry capture
31
- instead of booking. It also seeds any clinicians the AI detected (skip with
32
- `--no-dentists`).
32
+ The **CLI quickstart** card on that page shows a copy-paste `npx` command pre-filled with your workspace ID.
33
33
 
34
- ## Bulk import
34
+ > Admin keys (cross-workspace) also work but are intentionally restricted. Default to workspace keys.
35
35
 
36
- CSV with any of these columns: `name, website, town, postcode, email, phone`.
36
+ ## 2. Log in
37
37
 
38
38
  ```bash
39
- setu practice import practices.csv --publish
39
+ npx @series-a/cli@latest login
40
+ # Prompts for API key + workspace ID (the 6-char slug, e.g. dmw9wj)
40
41
  ```
41
42
 
42
- Rows are processed one at a time (Firecrawl / Google rate limits) and failures
43
- are listed at the end so you can retry them.
43
+ ### Admin login
44
44
 
45
- ## Claim links
45
+ Admin keys (`advos_admin_…`) are detected automatically — no workspace prompt:
46
46
 
47
47
  ```bash
48
- setu practice claim-link <practice-id>
49
- setu status # every seeded profile and its claim state
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
48
+ advisoros login --admin
49
+ # ✓ Logged in as admin profile "default" — N workspaces reachable
50
+
51
+ advisoros workspaces list # admin-only, no workspace needed
52
+ advisoros campaigns list --workspace dmw9wj # pass workspace per command
53
+ advisoros whoami # shows scope: admin (cross-workspace)
53
54
  ```
54
55
 
55
- Send the claim link in outreach. The practice signs in, and is verified either
56
- by their email domain matching their website, or by a code sent to the public
57
- contact address on their Google listing. On success the profile transfers to
58
- them, pre-filled, and they continue through the normal onboarding and
59
- subscription flow.
56
+ Multiple workspaces?
57
+
58
+ ```bash
59
+ advisoros login --profile acme # save as "acme"
60
+ advisoros leads list --profile acme # use the named profile
61
+ ```
60
62
 
61
- ## Reviewing claims
63
+ Config is stored at `~/.advisoros/config.json` with mode `0600`. You can also use env vars: `ADVISOROS_API_KEY`, `ADVISOROS_WORKSPACE_ID`, `ADVISOROS_PROFILE`, `ADVISOROS_BASE_URL`.
62
64
 
63
65
  ```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"
66
+ advisoros whoami
67
+ advisoros profiles
68
+ advisoros logout --profile acme
70
69
  ```
71
70
 
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.
71
+ ## 3. Command reference
75
72
 
76
- ## Clinicians
73
+ Global flags (work on every subcommand):
77
74
 
78
- ```bash
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
75
+ | Flag | Description |
76
+ |---|---|
77
+ | `--json` | Emit raw JSON instead of formatted tables. |
78
+ | `-w, --workspace <id>` | Override the workspace from your profile. |
79
+ | `-p, --profile <name>` | Pick a saved profile. |
80
+ | `--api-key <key>` | Override the saved API key. |
81
+
82
+ ### Leads
82
83
 
83
- setu practice list --limit 100 --search "smile"
84
- setu dentist list --practice <practice-id>
85
- setu dentist status <dentist-id> --full
84
+ ```bash
85
+ advisoros leads list [--status new] [--persona founder] [--search acme] [--limit 50]
86
+ advisoros leads get <id>
87
+ advisoros leads set-status <id> <new|engaged|won|lost>
88
+ advisoros leads import --file leads.csv [--persona founder]
89
+ advisoros leads import-from <source> [--query ...] [--post <url>] [--file <path>] [--audience <name>] [--campaign <id>] [--limit 50] [--no-enrich]
90
+ advisoros leads import-status <jobId>
91
+ advisoros leads update <id> [--name ...] [--title ...] [--company ...] [--email ...] [--linkedin ...] [--persona ...] [--score ...] [--status ...] [--notes ...]
92
+ advisoros leads delete <id>
93
+ advisoros leads verify <id>
86
94
  ```
87
95
 
88
- ## Flags
96
+ `leads import` accepts CSV (header row), JSON array, or JSONL. Upserts on `(workspace_id, linkedin_url)`. Bulk requests are auto-chunked at 200/call (server cap: 500/call).
89
97
 
90
- `--json` for machine-readable output, `--operator "Your Name"` to attribute
91
- seeded profiles to you.
98
+ `leads import-from` runs any of the import sources in **Get Leads → Import Leads**:
92
99
 
93
- ## Reliability
100
+ | source | required params | what it does |
101
+ |---|---|---|
102
+ | `linkedin_search` | `--query` | keyword / title / company people search via Unipile |
103
+ | `company_search` | `--query` | LinkedIn company search |
104
+ | `post_reactors` | `--post <url>` | everyone who reacted to a LinkedIn post |
105
+ | `paste_profiles` | `--file <path>` | name + LinkedIn URL lines; rest is enriched |
106
+ | `csv` | `--file <path>` | same CSV parser the UI uses |
107
+ | `heyreach` | `--file <path>` `--campaign <id>` | HeyReach campaign CSV, statuses preserved |
108
+ | `sales_nav_url` | `--url <sales-nav-search-url>` | queues a background Sales Navigator import, drip-fed inside the LinkedIn read limits |
109
+ | `sales_nav_keywords` | `--query` | same background queue, from Sales Navigator keywords |
94
110
 
95
- - Every request has a timeout (`SETU_TIMEOUT_MS`, default 240s) and retries
96
- transient failures (network drops, 429, 5xx) with exponential backoff
97
- (`SETU_RETRIES`, default 3). `Retry-After` is honoured.
98
- - `--verbose` prints retry attempts and stack traces.
99
- - `setu logout` removes the stored key from this machine.
100
- - Unknown commands and failed bulk imports exit non-zero (safe for scripts/CI).
111
+ All sources accept `--audience` to attach leads to a segment (created if missing). LinkedIn-backed sources run at bulk read priority so live campaigns are not starved. Long-running imports return a `job_id`; poll with `advisoros leads import-status <jobId>`.
101
112
 
102
- ## Bulk import
113
+ Sales Navigator sources do not run inline — they create a **scheduled import** that the
114
+ cap-aware scheduler drip-feeds over hours or days:
103
115
 
104
116
  ```bash
105
- setu practice import practices.csv --dry-run # validate only
106
- setu practice import practices.csv --publish \
107
- --concurrency 3 --out results.json # run
108
- setu practice import practices.csv --resume results.json # retry failures only
117
+ advisoros leads import-from sales_nav_url --url "https://www.linkedin.com/sales/search/people?..." \
118
+ --limit 500 --audience "Q3 ICP" --priority high
119
+ advisoros leads import-schedules
120
+ advisoros leads import-schedule pause|resume|cancel <id>
109
121
  ```
110
122
 
111
- Rows need either a `website`/`url`, or a `name` plus `town`/`city`; anything
112
- else is reported and skipped before any API call is made.
123
+ Cancelling keeps the leads already imported.
113
124
 
114
- ## Editing seeded profiles
125
+ ### Campaigns
115
126
 
116
127
  ```bash
117
- setu practice update <practice-id> --fields # what you can set
118
- setu practice update <practice-id> --set tagline="Gentle care" --set emergency_available=true
119
- setu dentist update <dentist-id> --set bio="20 years in implants"
128
+ advisoros campaigns list [--status active]
129
+ advisoros campaigns show <id>
130
+ advisoros campaigns create --name "Founder Pitch Club" \
131
+ --sequence <sequenceId> \
132
+ --senders acc_a,acc_b \
133
+ --daily-limit 25
134
+ advisoros campaigns add-leads <campaignId> --lead-ids id1,id2,id3
135
+ advisoros campaigns add-leads <campaignId> --file lead-ids.txt
136
+ advisoros campaigns launch <id>
137
+ advisoros campaigns pause <id>
138
+ advisoros campaigns resume <id>
139
+ advisoros campaigns rename <id> "New name" # rename without touching other settings
140
+ ```
141
+
142
+ `launch` flips status to `active` — the `enforce_campaign_char_limits` trigger validates every step's copy against LinkedIn limits before allowing the transition.
143
+
144
+ ### Sequences / Inbox / Content / Agents / Meetings / Newsletter / Integrations
145
+
146
+ ```bash
147
+ advisoros sequences list
148
+ advisoros sequences show <id>
149
+
150
+ advisoros inbox threads [--campaign <id>] [--lead <id>] [--needs-attention] [--limit 50]
151
+
152
+ advisoros content list [--status published] [--type post] [--search ...]
153
+ advisoros content show <id>
154
+ advisoros content publish <id>
155
+
156
+ advisoros agents list
157
+ advisoros agents run <name> --input '{"foo":"bar"}'
158
+ advisoros agents trigger <name> --input '{"foo":"bar"}'
159
+ advisoros agents history [--agent <name>] [--days 30] [--limit 50]
160
+ advisoros agents job <jobId>
161
+
162
+ advisoros meetings list [--limit 50]
163
+ advisoros meetings transcripts [--provider fireflies|roam] [--since 2026-01-01] [--limit 50]
164
+ advisoros meetings transcript <transcriptId> [--original | --mode anonymised|original]
165
+
166
+ advisoros newsletter list
167
+ advisoros newsletter create --subject "Weekly roundup" --body "<html>..." [--preview-text "..."]
168
+ advisoros newsletter edit <id> [--subject ...] [--body ...] [--preview-text ...] [--status draft|scheduled]
169
+ advisoros newsletter delete <id>
170
+
171
+ advisoros integrations status
172
+ advisoros integrations check <platform>
173
+ ```
174
+
175
+ > **Heads-up:** `inbox send`/`reply` are intentionally **not** in v1. Outbound goes through `campaigns launch` (sender resolution + char limits + per-sender daily budget). For one-off DMs, use the web inbox.
176
+
177
+ `meetings transcript` returns anonymised text by default, once anonymisation is ready; otherwise metadata is returned with an explicit reason. Add `--original` to get the original text and speaker identities — your key needs this permission (all unrestricted keys already have it) and every original read is audited.
178
+
179
+ `integrations status` is read-only and never returns credentials. `integrations check <platform>` runs a live health probe for the requested platform (e.g. `linkedin_tool`).
120
180
 
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
181
+ ### Obsidian vault mirror (`kb`) — one-way
182
+
183
+ ```bash
184
+ advisoros kb link ./client-vault --workspace dmw9wj
185
+ advisoros kb status ./client-vault # dry-run preview
186
+ advisoros kb pull ./client-vault # mirror notes + graph entities
187
+ advisoros kb pull ./client-vault --preserve-local
124
188
  ```
125
189
 
126
- Lists take comma-separated values, e.g. `--set services="Implants,Whitening"`.
190
+ One-way push, AdvisorPro → Obsidian. Markdown notes with frontmatter, `[[wikilinks]]` and an
191
+ `## Entities` block, plus `Entities/<Type>/*.md` for graph nodes. Local edits are overwritten unless
192
+ `--preserve-local` is used (remote copy lands as `<slug>.remote.md`). Nothing is pushed back.
193
+
194
+
195
+ ## Examples
127
196
 
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`:
197
+ **Spin up a campaign from a fresh CSV:**
131
198
 
132
199
  ```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"
200
+ advisoros leads import --file ./prospects.csv --persona founder
201
+ advisoros leads list --persona founder --limit 200 --json \
202
+ | jq -r '.leads[].id' > lead-ids.txt
203
+
204
+ advisoros campaigns create --name "Q3 Founder Outreach" \
205
+ --sequence 1a2b3c4d-... --senders acc_milan --daily-limit 25
206
+ # → ✓ Created campaign 8297f04d-...
207
+
208
+ advisoros campaigns add-leads 8297f04d-... --file lead-ids.txt
209
+ advisoros campaigns launch 8297f04d-...
138
210
  ```
139
211
 
140
- ## Photos and logos
212
+ **Tail a long-running agent job:**
141
213
 
142
214
  ```bash
143
- setu practice images <practice-id> --list
144
- setu practice images <practice-id> --add https://.../photo.jpg
145
- setu practice images <practice-id> --remove 2 # by index, or pass the URL
146
- setu practice images <practice-id> --order "https://.../hero.jpg" # move these to the front
147
- setu practice images <practice-id> --replace "https://a.jpg,https://b.jpg"
148
- setu practice images <practice-id> --logo https://.../logo.png
149
- setu dentist images <dentist-id> --logo https://.../headshot.jpg # main photo
215
+ JOB=$(advisoros agents run lead-intelligence --input '{}' --json | jq -r '.run.job_id')
216
+ watch -n 5 "advisoros agents job $JOB --json | jq '{status,progress}'"
150
217
  ```
151
218
 
152
- Upload from your machine (or copy a remote image into Setu storage) instead of
153
- linking to someone else's server:
219
+ ## Safety rails
220
+
221
+ Anything that stops sending, un-enrols leads or deletes data now asks first.
154
222
 
155
223
  ```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
224
+ # Launch is a dry run until you confirm — preflight runs server-side.
225
+ advisoros campaigns preflight 9884b9ec
226
+ advisoros campaigns launch 9884b9ec # shows blockers, changes nothing
227
+ advisoros campaigns launch 9884b9ec --confirm # actually launches
228
+
229
+ # Destructive commands prompt; scripts must pass --yes.
230
+ advisoros campaigns delete 9884b9ec --dry-run
231
+ advisoros campaigns delete 9884b9ec --yes
232
+ advisoros sequences delete-step 6b60d6ad --yes
159
233
  ```
160
234
 
161
- ## Prices and services
235
+ A campaign with no leads, no sequence, no steps or no sender account is
236
+ **refused**, not silently activated. `--force` overrides once you know why.
237
+
238
+ New commands: `campaigns preflight|archive|clone|delete|remove-leads`,
239
+ `sequences delete|update-step|delete-step`.
240
+
241
+ ## Reading more than 200 rows
242
+
243
+ List commands accept `--limit` up to 1000 and every list response carries
244
+ `pagination` (`total`, `limit`, `offset`, `next_cursor`, `has_more`). If you ask
245
+ for more than the maximum, the response says it was clamped instead of quietly
246
+ truncating.
247
+
248
+ ## Exit codes
249
+
250
+ | Code | Meaning |
251
+ |---|---|
252
+ | 0 | Success |
253
+ | 1 | Generic failure (`error` in JSON response or thrown). Stderr contains the message. |
254
+
255
+ ## Programmatic use
256
+
257
+ ```js
258
+ import { call } from "@series-a/cli/src/api.mjs";
259
+
260
+ const { campaigns } = await call("campaigns", "list", { status: "active" }, {
261
+ apiKey: process.env.ADVISOROS_API_KEY,
262
+ workspaceId: "dmw9wj",
263
+ });
264
+ ```
265
+
266
+ ## Troubleshooting
267
+
268
+ - **401 Unauthorized** — key wrong / revoked / wrong base URL. Re-run `advisoros login`.
269
+ - **403 not authorized for the requested workspace_id** — workspace-scoped key + `--workspace` to a different workspace. Use the matching key.
270
+ - **CHAR_LIMIT_EXCEEDED on launch** — a sequence step exceeds LinkedIn limits (200 invitations / 8000 DMs / 1250 comments). Fix in web app.
271
+ - **Network error** — check `ADVISOROS_BASE_URL` if you're pointing at a non-default deployment.
272
+
273
+ ## Releasing (maintainers)
162
274
 
163
275
  ```bash
164
- setu practice pricing <practice-id> --list
165
- setu practice pricing <practice-id> --set "Dental implant=1200" --set "Whitening=300-450"
166
- setu practice pricing <practice-id> --remove "Whitening"
167
- setu dentist pricing <dentist-id> --set "Hygiene visit=75"
168
- setu practice update <practice-id> --set services="Implants,Whitening,Invisalign"
276
+ cd tools/cli
277
+ npx changeset # describe the change
278
+ npm run version-packages # bump package.json + CHANGELOG.md
279
+ git commit -am "chore(cli): release v$(node -p "require('./package.json').version")"
280
+ git tag "cli-v$(node -p "require('./package.json').version")"
281
+ git push --follow-tags
282
+ # → .github/workflows/cli-release.yml publishes to npm with provenance
169
283
  ```
170
284
 
171
- Practice prices update the practice's treatment list too. Dentist prices are
172
- saved as individual treatment rows shown on the clinician's profile.
285
+ `NPM_TOKEN` must be set in the repo's GitHub Actions secrets (automation token with publish access to the `@series-a` org).
286
+
287
+ ## Roadmap (v1.1)
288
+
289
+ - `inbox send` / `inbox reply` with explicit `--confirm` + per-sender budget enforcement.
290
+ - `agents tail <jobId>` (SSE).
291
+ - `admin workspaces …` (admin keys only).
292
+ - Single-binary builds via `pkg`.
173
293
 
174
- ## Removing seeded profiles
294
+ ## Full API reference
295
+
296
+ See [`docs/agent-api.md`](../../docs/agent-api.md) for the underlying `/agent-api` envelope.
297
+
298
+
299
+
300
+ ## Admin keys
301
+
302
+ Admin keys (`advos_admin_…`) are authorised by the key itself — they do **not** need an
303
+ `agent_workspace_bindings` row. `agent_name` is an audit label only.
175
304
 
176
305
  ```bash
177
- setu practice delete <practice-id> --unpublish # hide from patients
178
- setu practice delete <practice-id> # delete (unclaimed only)
179
- setu dentist delete <dentist-id> --unpublish
180
- setu dentist delete <dentist-id>
306
+ advisoros login --admin --profile ops # no workspace prompt; prints reachable workspace count
307
+ advisoros whoami --profile ops # scope: admin (cross-workspace) + reachable count
308
+ advisoros workspaces list --profile ops # every reachable workspace (paginated, up to 1000)
309
+ advisoros analytics overview --workspace dmw9wj --profile ops
181
310
  ```
182
311
 
183
- Claimed or user-owned profiles are always refused.
312
+ Caller identity is overridable: `--as-agent <name>` on any command, `ADVISOROS_AGENT_NAME`
313
+ in the environment, or the `agentName` stored on the profile at login (default `advisoros-cli`).
314
+
315
+ Workspace-scoped keys are unchanged: they can only reach their own workspace (403 otherwise)
316
+ and cannot use the `workspaces` resource. An agent without bindings now gets a typed 403
317
+ explaining to use an admin key, instead of a 500.