@series-a/cli 1.2.0 → 1.3.1

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,151 +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**.
22
+
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).
24
+
25
+ ## 1. Get an API key
26
+
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.
31
+
32
+ The **CLI quickstart** card on that page shows a copy-paste `npx` command pre-filled with your workspace ID.
33
+
34
+ > Admin keys (cross-workspace) also work but are intentionally restricted. Default to workspace keys.
15
35
 
16
- ## Build one profile
36
+ ## 2. Log in
17
37
 
18
38
  ```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>
39
+ npx @series-a/cli@latest login
40
+ # Prompts for API key + workspace ID (the 6-char slug, e.g. dmw9wj)
23
41
  ```
24
42
 
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`.
43
+ ### Admin login
28
44
 
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`).
45
+ Admin keys (`advos_admin_…`) are detected automatically — no workspace prompt:
33
46
 
34
- ## Bulk import
47
+ ```bash
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)
54
+ ```
35
55
 
36
- CSV with any of these columns: `name, website, town, postcode, email, phone`.
56
+ Multiple workspaces?
37
57
 
38
58
  ```bash
39
- setu practice import practices.csv --publish
59
+ advisoros login --profile acme # save as "acme"
60
+ advisoros leads list --profile acme # use the named profile
40
61
  ```
41
62
 
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.
44
-
45
- ## Claim links
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`.
46
64
 
47
65
  ```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
66
+ advisoros whoami
67
+ advisoros profiles
68
+ advisoros logout --profile acme
51
69
  ```
52
70
 
53
- Send the claim link in outreach. The practice signs in, and is verified either
54
- by their email domain matching their website, or by a code sent to the public
55
- contact address on their Google listing. On success the profile transfers to
56
- them, pre-filled, and they continue through the normal onboarding and
57
- subscription flow.
71
+ ## 3. Command reference
58
72
 
59
- ## Clinicians
73
+ Global flags (work on every subcommand):
74
+
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
60
83
 
61
84
  ```bash
62
- setu dentist create --name "Dr Jane Roe" --practice <id> --profile-url https://…/team/jane
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>
63
94
  ```
64
95
 
65
- ## 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).
97
+
98
+ `leads import-from` runs any of the import sources in **Get Leads → Import Leads**:
66
99
 
67
- `--json` for machine-readable output, `--operator "Your Name"` to attribute
68
- seeded profiles to you.
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 |
69
110
 
70
- ## Reliability
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>`.
71
112
 
72
- - Every request has a timeout (`SETU_TIMEOUT_MS`, default 240s) and retries
73
- transient failures (network drops, 429, 5xx) with exponential backoff
74
- (`SETU_RETRIES`, default 3). `Retry-After` is honoured.
75
- - `--verbose` prints retry attempts and stack traces.
76
- - `setu logout` removes the stored key from this machine.
77
- - Unknown commands and failed bulk imports exit non-zero (safe for scripts/CI).
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:
78
115
 
79
- ## Bulk import
116
+ ```bash
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>
121
+ ```
122
+
123
+ Cancelling keeps the leads already imported.
124
+
125
+ ### Campaigns
80
126
 
81
127
  ```bash
82
- setu practice import practices.csv --dry-run # validate only
83
- setu practice import practices.csv --publish \
84
- --concurrency 3 --out results.json # run
85
- setu practice import practices.csv --resume results.json # retry failures only
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
86
140
  ```
87
141
 
88
- Rows need either a `website`/`url`, or a `name` plus `town`/`city`; anything
89
- else is reported and skipped before any API call is made.
142
+ `launch` flips status to `active` — the `enforce_campaign_char_limits` trigger validates every step's copy against LinkedIn limits before allowing the transition.
90
143
 
91
- ## Editing seeded profiles
144
+ ### Sequences / Inbox / Content / Agents / Meetings / Newsletter / Integrations
92
145
 
93
146
  ```bash
94
- setu practice update <practice-id> --fields # what you can set
95
- setu practice update <practice-id> --set tagline="Gentle care" --set emergency_available=true
96
- setu dentist update <dentist-id> --set bio="20 years in implants"
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`).
180
+
181
+ ### Obsidian vault mirror (`kb`) — one-way
97
182
 
98
- # Booking / website link shortcuts
99
- setu practice update <practice-id> --booking https://clinic.co.uk/book
100
- setu dentist update <dentist-id> --booking https://clinic.co.uk/book --website https://clinic.co.uk
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
101
188
  ```
102
189
 
103
- 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
+
104
194
 
105
- Everything on the practice profile settings screen is settable. The few values that
106
- live in the nested profile record — positioning, amenities, default currency — use
107
- `--set-enrichment`:
195
+ ## Examples
196
+
197
+ **Spin up a campaign from a fresh CSV:**
108
198
 
109
199
  ```bash
110
- setu practice update <practice-id> --set-enrichment positioning.summary="Calm, modern care in Leeds"
111
- setu practice update <practice-id> --set-enrichment amenities="Wi-Fi,Free parking,Step-free access"
112
- setu practice update <practice-id> --set-enrichment positioning.archetype=boutique
113
- setu practice update <practice-id> --set parking_info="Two-hour free bay on site" \
114
- --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-...
115
210
  ```
116
211
 
117
- ## Photos and logos
212
+ **Tail a long-running agent job:**
118
213
 
119
214
  ```bash
120
- setu practice images <practice-id> --list
121
- setu practice images <practice-id> --add https://.../photo.jpg
122
- setu practice images <practice-id> --remove 2 # by index, or pass the URL
123
- setu practice images <practice-id> --order "https://.../hero.jpg" # move these to the front
124
- setu practice images <practice-id> --replace "https://a.jpg,https://b.jpg"
125
- setu practice images <practice-id> --logo https://.../logo.png
126
- 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}'"
217
+ ```
218
+
219
+ ## Safety rails
220
+
221
+ Anything that stops sending, un-enrols leads or deletes data now asks first.
222
+
223
+ ```bash
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
233
+ ```
234
+
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
+ });
127
264
  ```
128
265
 
129
- ## Prices and services
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)
130
274
 
131
275
  ```bash
132
- setu practice pricing <practice-id> --list
133
- setu practice pricing <practice-id> --set "Dental implant=1200" --set "Whitening=300-450"
134
- setu practice pricing <practice-id> --remove "Whitening"
135
- setu dentist pricing <dentist-id> --set "Hygiene visit=75"
136
- 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
137
283
  ```
138
284
 
139
- Practice prices update the practice's treatment list too. Dentist prices are
140
- 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)
141
288
 
142
- ## Removing seeded profiles
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`.
293
+
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.
143
304
 
144
305
  ```bash
145
- setu practice delete <practice-id> --unpublish # hide from patients
146
- setu practice delete <practice-id> # delete (unclaimed only)
147
- setu dentist delete <dentist-id> --unpublish
148
- 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
149
310
  ```
150
311
 
151
- 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.