@series-a/cli 0.22.2 → 0.23.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
@@ -4,6 +4,28 @@ All notable changes to this package are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/) and uses
5
5
  [Changesets](https://github.com/changesets/changesets) to manage releases.
6
6
 
7
+ ## 0.23.0 — Bulk lead upserts, named profiles, atomic mutations
8
+
9
+ - `leads bulk_upsert` works at scale again. The database now enforces one lead per LinkedIn URL per workspace, so bulk imports update existing leads in place instead of failing with a Postgres conflict error. Repeated URLs inside a single request are collapsed, and leads without a LinkedIn URL stay separate records.
10
+ - Named workspace profiles: `login -p <name>` stores a profile per workspace, and logging into a new workspace no longer silently re-points your `default` profile — you are warned and can opt in with `--set-default` (or skip the prompt with `--force`).
11
+ - Workspace isolation on agent reads and mutations: `agents show <name>` and every agent mutation now resolve strictly inside the authenticated workspace, so same-named agents in other workspaces can never be returned or changed.
12
+ - Every mutation returns JSON. `voice card --rebuild`, `intel-prompts resync` and watchlist toggles now return the changed record, the workspace id and an explicit success/error status instead of exiting silently.
13
+ - Watchlist operations are atomic and de-duplicated by workspace + normalised name + type; removed items stay removed (suppressions) and are no longer reintroduced by automatic prompt sync, which now only reads active, approved, human-authored knowledge records.
14
+ - `agents pipeline [name]` (alias `inspect`) exposes an agent's configuration, pipeline stages, blocking gates and advisory gates for inspection — defaults to the Content Creator Agent.
15
+ - Content pipeline quality: signals are retrieved workspace-wide (drafts no longer report "0 signals" when teammates persisted them), signal sources are integrity-checked and quarantined when URL, title, publisher, text and summary disagree, publication dates and source authority are required before evidence is accepted, and alignment + evidence + citation checks must all complete before an article is eligible.
16
+ - P0 watchlist topics are treated as highest research priority, not mandatory inclusion — all P0s are searched, only relevant ones are used, and searched/selected/excluded topics are recorded with scores and reasons.
17
+ - Voice benchmarks are built from cleaned document structure (PDF page furniture, headers/footers and hard line wraps removed), so paragraph and heading metrics are realistic, and generation and scoring share the same benchmark and reference set.
18
+ - Image generation failures now surface the real reason (model, input or storage error) in the app instead of a generic non-2xx message.
19
+
20
+
21
+ ## 0.22.3 — Sales Navigator imports from the CLI
22
+
23
+ - Import Sales Navigator lead lists straight from the CLI: `leads import-from sales_nav_url --url "<search-url>"` or `sales_nav_keywords --query "..."`. These queue as cap-aware background imports that drip-feed inside your LinkedIn read limits instead of blocking live campaigns.
24
+ - Track and control them: `leads import-schedules` (progress, ETA, waiting reason) and `leads import-schedule pause|resume|cancel <id>`. Resuming resets retries and nudges the scheduler immediately.
25
+ - Idempotent: re-running an identical import reuses the active schedule instead of duplicating it; match is per-source (URL for url imports, keywords for keyword imports).
26
+ - Options: `--limit` (1–1000, default 100), `--audience` (segment id or name; created if missing), `--priority normal|high|rush`.
27
+ - Docs updated in README and SKILLS.md; MCP gains `lead_import_schedules` and extended `import_leads`.
28
+
7
29
  ## 0.22.0 — Admin keys, analytics parity, safety hardening, Obsidian sync
8
30
 
9
31
  - Admin keys work end-to-end. `login --admin` persists admin scope and reports reachable workspace count, `whoami` shows `scope: admin (cross-workspace)` plus reachable workspaces, and the caller identity is no longer hardcoded — override with `--as-agent <name>` or `ADVISOROS_AGENT_NAME`, persisted per profile.
package/README.md CHANGED
@@ -95,7 +95,7 @@ advisoros leads verify <id>
95
95
 
96
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
97
 
98
- `leads import-from` runs any of the six import sources in **Get Leads → Import Leads**:
98
+ `leads import-from` runs any of the import sources in **Get Leads → Import Leads**:
99
99
 
100
100
  | source | required params | what it does |
101
101
  |---|---|---|
@@ -105,9 +105,23 @@ advisoros leads verify <id>
105
105
  | `paste_profiles` | `--file <path>` | name + LinkedIn URL lines; rest is enriched |
106
106
  | `csv` | `--file <path>` | same CSV parser the UI uses |
107
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 |
108
110
 
109
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>`.
110
112
 
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:
115
+
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
+
111
125
  ### Campaigns
112
126
 
113
127
  ```bash
package/SKILLS.md ADDED
@@ -0,0 +1,319 @@
1
+ # AdvisorPro Skills Guide for AI Agents
2
+
3
+ This file teaches any AI agent — CLI user or MCP client — how to operate
4
+ AdvisorPro safely and effectively. It covers the mental model, the core
5
+ workflows (campaigns, content, brand), the safety rails you must respect,
6
+ and the tricks that separate a smooth run from a failed one.
7
+
8
+ Two surfaces, one backend:
9
+
10
+ - **CLI** — `advisoros` binary (`npm i -g @series-a/cli`), authenticates with
11
+ an API key saved per profile in `~/.advisoros/config.json`.
12
+ - **MCP server** — `advisoros-mcp`, 44 tools, OAuth for the signed-in user.
13
+
14
+ Both talk to the same `/agent-api` gateway. Everything is **scoped to one
15
+ workspace**. Every mutation is **audited** (who, when, what, outcome).
16
+
17
+ ---
18
+
19
+ ## 1. The non-negotiable first steps
20
+
21
+ ### MCP
22
+ 1. **Always call `list_workspaces` first.** Every other tool requires a
23
+ `workspace_id`. You can only see workspaces you belong to — never guess an
24
+ ID from another session.
25
+ 2. Read the tool description before calling: destructive tools document their
26
+ own confirmation protocol.
27
+
28
+ ### CLI
29
+ ```bash
30
+ advisoros login --profile main # saves API key + workspace
31
+ advisoros whoami # confirm the active profile/workspace
32
+ ```
33
+ Global flags on every command: `--json` (machine output — **always use this
34
+ when an AI is parsing the result**), `-w/--workspace`, `-p/--profile`,
35
+ `--api-key`, `--as-agent <name>` (labels your audit rows — set it to your
36
+ agent's name).
37
+
38
+ ---
39
+
40
+ ## 2. Mental model
41
+
42
+ - **Leads** are people. They enter via import and are enriched from LinkedIn
43
+ afterwards. Dedupe key is `(workspace_id, linkedin_url)` — re-importing the
44
+ same URL upserts, never duplicates.
45
+ - **Sequences** are message trees (steps with actions, delays, branches).
46
+ They are **not append-only** — steps can be edited, deleted, re-parented,
47
+ or wholesale replaced.
48
+ - **Campaigns** bind leads + a sequence + sender LinkedIn accounts + a daily
49
+ limit. Lifecycle: draft → launch → pause/resume → archive/delete.
50
+ - **Content** items are drafts until scheduled/published. Nothing is ever
51
+ auto-published from a draft.
52
+ - **Knowledge base** grounds all AI generation. Only **confirmed
53
+ human-authored** items train the voice and count as citable evidence.
54
+ - **Agents** run asynchronously: you get a `job_id` back, then poll.
55
+
56
+ ---
57
+
58
+ ## 3. LinkedIn safety — read this before any outreach
59
+
60
+ LinkedIn account safety overrides everything else. The server (not the CLI or
61
+ MCP) is the enforcing authority; you are expected to check before acting.
62
+
63
+ Hard limits (server-enforced):
64
+ - Invite note: **200 characters**
65
+ - Direct message: **8,000 characters** (CLI caps client-side at 7,900)
66
+ - Comment: **1,250 characters**
67
+ - Per sender: **~25 invites/day, ~80 messages/day**
68
+
69
+ Rules for agents:
70
+ - **Before planning outreach or a bulk import, check capacity.**
71
+ CLI: `advisoros safety check` / `safety limits` / `safety reads`.
72
+ MCP: `linkedin_safety` (returns remaining capacity, cooldowns, and
73
+ `safe_bulk_import_size`).
74
+ - **A cooldown or a 429 is not a failure.** Back off using `Retry-After` /
75
+ `retry_after_seconds` and retry. Never hammer.
76
+ - `campaigns launch` and `sequences validate` run the same char-limit checks
77
+ the server enforces — **validate before launch**, not after.
78
+
79
+ ---
80
+
81
+ ## 4. How to create a campaign (end to end)
82
+
83
+ ### The fast path: `campaign-flow`
84
+ One command does import → sequence → launch → status:
85
+
86
+ ```bash
87
+ advisoros campaign-flow \
88
+ --name "Q3 CFO outreach" \
89
+ --file leads.csv \
90
+ --goal "Book discovery calls with CFOs" \
91
+ --senders <senderId1>,<senderId2> \
92
+ --daily-limit 25 \
93
+ --launch
94
+ ```
95
+ Add `--dry-run` first to print the plan without touching the API.
96
+
97
+ ### The step-by-step path
98
+ ```bash
99
+ # 1. Check capacity
100
+ advisoros safety check
101
+
102
+ # 2. Import leads (auto-chunks at 200/call, upserts on linkedin_url)
103
+ advisoros leads import --file leads.csv
104
+ # or from a source:
105
+ advisoros leads import-from linkedin_search --query "CFO fintech London"
106
+ # Sales Navigator (queued in the background, cap-aware):
107
+ advisoros leads import-from sales_nav_url --url "<sales-nav-search-url>" --limit 500
108
+ advisoros leads import-schedules # progress, ETA, why it is waiting
109
+ advisoros leads import-schedule pause|resume|cancel <scheduleId>
110
+
111
+ # 3. Build or pick a sequence
112
+ advisoros sequences create --name "CFO 3-touch"
113
+ advisoros sequences add-step <seqId> --action sendInvitation --message "..."
114
+ advisoros sequences add-step <seqId> --action wait --days 3
115
+ advisoros sequences add-step <seqId> --action sendMessage --message "..."
116
+ advisoros sequences validate <seqId> # same checks as launch — do this
117
+
118
+ # 4. Create the campaign and attach everything
119
+ advisoros campaigns create --name "Q3 CFO outreach" \
120
+ --sequence <seqId> --senders <id1,id2> --daily-limit 25
121
+ advisoros campaigns add-leads <campaignId> --file leads.csv
122
+
123
+ # 5. Preflight, then launch (launch runs preflight itself and asks --confirm)
124
+ advisoros campaigns preflight <campaignId>
125
+ advisoros campaigns launch <campaignId> --confirm
126
+ ```
127
+
128
+ MCP equivalent: `import_leads` → `manage_sequence` → `update_campaign` →
129
+ `manage_campaign` (`preflight`, then `launch`).
130
+
131
+ ### After launch
132
+ ```bash
133
+ advisoros campaigns stats <id> # funnel: invites → accepts → replies → meetings
134
+ advisoros campaigns health <id> # run state, stalls, retries, watchdog
135
+ advisoros campaigns events <id> # full event log
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 5. Sequences: the tricks
141
+
142
+ - Allowed step actions: `viewProfile`, `addReaction`, `sendInvitation`,
143
+ `sendMessage`, `wait`. Nothing else — the server rejects unknown actions.
144
+ - `sequences show <id>` accepts a **unique ID prefix**, not just full UUIDs.
145
+ - `replace-steps` is **atomic**: it swaps the whole tree from a JSON file,
146
+ validated server-side. Prefer it over many `add-step` calls when an AI is
147
+ generating a full sequence.
148
+ - `update-step` patches one step in place; `delete-step` re-parents its
149
+ children automatically.
150
+ - Message templates support `{first_name}`-style tokens; the server guards
151
+ against unfilled tokens (~20-char guard). Keep variables real.
152
+ - Waits can be expressed in days/hours/minutes. A lead who **replies is
153
+ removed from all further automation automatically** — never re-add them.
154
+
155
+ ---
156
+
157
+ ## 6. Keeping a consistent brand
158
+
159
+ Brand consistency lives in three places; keep all three in sync.
160
+
161
+ ### a) The brand record (visual identity)
162
+ ```bash
163
+ advisoros brand show # colours, fonts, logos, tagline, imagery
164
+ advisoros brand set --primary-color "#1B4B8F" --font-heading "Inter"
165
+ advisoros brand fields # every editable field
166
+ advisoros brand imagery set --style "minimal editorial" --mood "calm, expert"
167
+ advisoros brand logos set --variant primary --url https://...
168
+ advisoros brand social set --linkedin https://linkedin.com/company/...
169
+ ```
170
+ MCP: `manage_brand` (same fields; supports partial updates, add/remove of
171
+ motifs and reference URLs, logo upload, and the imagery direction that steers
172
+ **every generated image**).
173
+
174
+ ### b) Tone of voice (written identity)
175
+ ```bash
176
+ advisoros voice show # directive + apply-by-default flag
177
+ advisoros voice set --directive "..." --apply-default
178
+ advisoros voice sync # re-pull from Business Build
179
+ advisoros voice references # KB items used as writing samples
180
+ advisoros voice add-reference <knowledgeItemId>
181
+ advisoros voice set-primary <knowledgeItemId> # pin the benchmark sample
182
+ advisoros voice card # measured voice card (stylometric targets)
183
+ advisoros voice score --file draft.md # score text against the voice
184
+ advisoros voice redraft --file draft.md # rewrite in the author's voice
185
+ ```
186
+ MCP: `tone_of_voice`.
187
+
188
+ **The rule that matters:** only knowledge items confirmed as human-authored
189
+ count as voice-training material. Confirm them first (`voice add-reference`,
190
+ or MCP `curate_knowledge` with `approve`). AI-authored items are down-weighted
191
+ and labelled in every retrieval — don't try to train the voice on them.
192
+
193
+ ### c) Knowledge base (factual identity)
194
+ ```bash
195
+ advisoros knowledge add --url https://... # auto-scraped + AI-classified
196
+ advisoros knowledge add --title "..." --text "..." --category positioning
197
+ advisoros knowledge list
198
+ advisoros knowledge reclassify <id>
199
+ ```
200
+ Categories: positioning, proof-points, frameworks, personas, case-studies,
201
+ transcripts, market-intel, general. Generation is grounded in the KB — a thin
202
+ KB means generic output. Feed it real positioning and real proof points.
203
+
204
+ ### Generating on-brand output
205
+ ```bash
206
+ advisoros content generate --topic "..." --type linkedin_post # KB + reference
207
+ # grounded, URL-sanitised
208
+ advisoros content citations <id> # every source recorded for a draft
209
+ ```
210
+ MCP: `generate_graphic` / `refine_graphic` for images (returns public URLs,
211
+ uses the brand imagery direction automatically), `create_content_draft` for
212
+ text (drafts only — nothing auto-publishes).
213
+
214
+ ---
215
+
216
+ ## 7. Content strategy & calendar
217
+
218
+ ```bash
219
+ advisoros strategies ideate --weeks 4 # multi-week arc from KB + signals
220
+ advisoros strategies create ...
221
+ advisoros strategies resume <id> # continue an interrupted build
222
+ advisoros content list / show / approve / schedule / publish
223
+ advisoros content bulk-approve # approve everything pending
224
+ advisoros calendar list / create
225
+ ```
226
+ Scheduling routes through Taplio when connected, otherwise the built-in cron
227
+ scheduler. MCP: `content_strategy`, `content_strategy_posts`,
228
+ `schedule_content`, `manage_content`.
229
+
230
+ ---
231
+
232
+ ## 8. Running agents (async jobs)
233
+
234
+ ```bash
235
+ advisoros agents list
236
+ advisoros agents run <name> # returns a job_id immediately
237
+ advisoros agents job <jobId> # poll until terminal
238
+ ```
239
+ MCP: `trigger_agent` (or `history: true` for recent runs).
240
+
241
+ Watch recipe:
242
+ ```bash
243
+ JOB=$(advisoros agents run news-monitor --json | jq -r .job_id)
244
+ until advisoros agents job "$JOB" --json | jq -e '.status=="completed"'; do
245
+ sleep 15
246
+ done
247
+ ```
248
+ A job ends in a **terminal state** (`completed` or `failed`). Content jobs
249
+ also carry `content_ids` and trace steps — if a job stays `running`, poll
250
+ longer (large articles take minutes); don't resubmit, dispatch is
251
+ exactly-once.
252
+
253
+ ---
254
+
255
+ ## 9. Destructive actions — the confirm protocol
256
+
257
+ Deletion is allowed but always two-step:
258
+
259
+ - CLI: `campaigns delete <id>` is dry-run by default; re-run with `--force`.
260
+ - MCP: `manage_campaign` / `manage_sequence` with `operation: "delete"`:
261
+ first `dry_run: true` to see the impact, then `confirm: true` to execute.
262
+ Campaigns that already dispatched activity also require `force: true`.
263
+
264
+ `archive` is the reversible alternative — prefer it when unsure.
265
+
266
+ ---
267
+
268
+ ## 10. Tricks & gotchas
269
+
270
+ 1. **`--json` everywhere.** Human tables are for humans; parse JSON.
271
+ 2. **`--as-agent <name>` on every call.** Your actions appear in
272
+ `advisoros audit list` — make them attributable.
273
+ 3. **Mutation responses are live.** Every campaign/sequence mutation returns
274
+ the freshly re-read record with live counts. List `stats` snapshots can
275
+ lag — trust the `live` block after a mutation.
276
+ 4. **Imports are idempotent.** Re-importing the same leads upserts on
277
+ `(workspace_id, linkedin_url)`. Safe to retry after a crash.
278
+ 5. **Inbox sends are dry-run by default.** `inbox send`/`reply` exit code `2`
279
+ without `--confirm` — nothing hits LinkedIn until you confirm.
280
+ 6. **Intelligence before content.** `signals list` and the watchlist (P0–P3
281
+ priorities) drive what the content agents write about. A tuned watchlist +
282
+ rich KB + confirmed voice references = on-topic, on-voice output.
283
+ 7. **`export <resource> <action>`** turns any list into CSV for offline work.
284
+ 8. **`kb sync`** mirrors the knowledge base into an Obsidian vault — one-way
285
+ push only, never a source of truth in the other direction.
286
+ 9. **`integrations status`** is read-only health. It will never return
287
+ credentials — don't ask it to.
288
+ 10. **Transcripts are anonymised** before storage. You will never see raw PII
289
+ from `meetings`/`list_transcripts`; that's by design.
290
+ 11. **Rate limits:** per-key sliding window. On HTTP 429, respect
291
+ `Retry-After`. Empty results are never used as a throttle signal — if you
292
+ get an empty list, the data is genuinely empty.
293
+ 12. **MCP writes are limited** to leads, knowledge items, drafts, and the
294
+ management tools above. Nothing auto-publishes; publishing is a deliberate
295
+ `manage_content publish_now` / `content publish` call.
296
+
297
+ ---
298
+
299
+ ## 11. Cheatsheet
300
+
301
+ | Task | CLI | MCP tool |
302
+ |---|---|---|
303
+ | Discover workspaces | `workspaces list` (admin) | `list_workspaces` (first!) |
304
+ | Check LinkedIn capacity | `safety check` | `linkedin_safety` |
305
+ | Import leads | `leads import` / `import-from` | `import_leads` |
306
+ | Track Sales Nav imports | `leads import-schedules` / `import-schedule` | `lead_import_schedules` |
307
+ | Build a sequence | `sequences create/add-step/replace-steps/validate` | `manage_sequence` |
308
+ | Launch a campaign | `campaign-flow` or `campaigns create/add-leads/launch` | `manage_campaign` |
309
+ | Campaign funnel | `campaigns stats` | `campaign_analytics` |
310
+ | Brand identity | `brand show/set/imagery/logos/social` | `manage_brand` |
311
+ | Voice | `voice show/set/references/score` | `tone_of_voice` |
312
+ | Knowledge | `knowledge add/list/reclassify` | `add_knowledge_item`, `curate_knowledge` |
313
+ | Generate content | `content generate` | `create_content_draft`, `generate_graphic` |
314
+ | Schedule | `content schedule` / `calendar create` | `schedule_content` |
315
+ | Run an agent | `agents run` + `agents job` | `trigger_agent` |
316
+ | Who did what | `audit list` | `list_audit` |
317
+
318
+ When in doubt: `advisoros <command> --help`, `docs/cli.md`, and
319
+ `docs/agent-api.md` are the canonical references.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-a/cli",
3
- "version": "0.22.2",
3
+ "version": "0.23.0",
4
4
  "description": "AdvisorPro command-line interface — drive leads, campaigns, sequences and agents from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin/",
11
11
  "src/",
12
12
  "README.md",
13
+ "SKILLS.md",
13
14
  "CHANGELOG.md",
14
15
  "LICENSE"
15
16
  ],
@@ -1,14 +1,40 @@
1
1
  import prompts from "prompts";
2
2
  import chalk from "chalk";
3
- import { getProfile, setProfile, listProfiles, deleteProfile, CONFIG_PATH, DEFAULTS } from "../config.mjs";
3
+ import { getProfile, setProfiles, listProfiles, deleteProfile, allProfiles, CONFIG_PATH, DEFAULTS } from "../config.mjs";
4
4
  import { call } from "../api.mjs";
5
5
  import { emit, fail, ctxFrom, withCommonOpts } from "../ui.mjs";
6
6
 
7
+ /**
8
+ * Replacing the "default" profile silently re-points every unqualified command
9
+ * at another workspace. Always confirm first; in a non-interactive shell the
10
+ * caller must pass --force.
11
+ */
12
+ async function confirmDefaultReplacement(workspace, opts, currentWorkspace) {
13
+ if (opts.force) return true;
14
+ if (!currentWorkspace) return true;
15
+ if (!process.stdin.isTTY) {
16
+ process.stdout.write(chalk.yellow(
17
+ `\n! Default profile currently points at workspace ${currentWorkspace}.\n` +
18
+ ` Not replacing it — re-run with --force to move it to ${workspace}.\n`,
19
+ ));
20
+ return false;
21
+ }
22
+ const r = await prompts({
23
+ type: "confirm",
24
+ name: "v",
25
+ message: `Replace the default profile (workspace ${currentWorkspace}) with ${workspace}?`,
26
+ initial: false,
27
+ });
28
+ return !!r.v;
29
+ }
30
+
7
31
  export function registerAuth(program) {
8
32
  program
9
33
  .command("login")
10
34
  .description("Save an API key + workspace to ~/.advisoros/config.json")
11
- .option("-p, --profile <name>", "profile name", "default")
35
+ .option("-p, --profile <name>", "profile name (defaults to the workspace id)")
36
+ .option("--set-default", "also point the \"default\" profile at this workspace", false)
37
+ .option("--force", "replace an existing default profile without asking", false)
12
38
  .option("--api-key <key>", "API key (otherwise prompted)")
13
39
  .option("--workspace <id>", "workspace id (otherwise prompted, unless --admin)")
14
40
  .option("--admin", "admin key login — verify cross-workspace, no workspace required", false)
@@ -50,7 +76,15 @@ export function registerAuth(program) {
50
76
  apiKey, baseUrl, agentName,
51
77
  });
52
78
  const count = verify?.pagination?.total ?? verify?.workspaces?.length ?? 0;
53
- await setProfile(profile, { apiKey, baseUrl, admin: true, agentName, ...(opts.email ? { email: opts.email } : {}), ...(workspace ? { workspaceId: workspace } : {}) });
79
+ const adminData = { apiKey, baseUrl, admin: true, agentName, ...(opts.email ? { email: opts.email } : {}), ...(workspace ? { workspaceId: workspace } : {}) };
80
+ const adminName = profile || "admin";
81
+ const adminWrites = { [adminName]: adminData };
82
+ if (adminName !== "default" && (opts.setDefault || !(await getProfile("default")))) {
83
+ const okDefault = await confirmDefaultReplacement(workspace || "(admin)", opts);
84
+ if (okDefault) adminWrites.default = adminData;
85
+ }
86
+ await setProfiles(adminWrites, adminName);
87
+ profile = adminName;
54
88
  process.stdout.write(chalk.green(
55
89
  `\n✓ Logged in as admin profile "${profile}" — ${count} workspaces reachable\n` +
56
90
  ` Config: ${CONFIG_PATH}\n` +
@@ -64,11 +98,40 @@ export function registerAuth(program) {
64
98
  apiKey, workspaceId: workspace, baseUrl, agentName,
65
99
  });
66
100
  const count = verify?.agents?.length ?? 0;
67
- await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl, agentName, ...(opts.email ? { email: opts.email } : {}) });
101
+ // Named workspace profiles: without -p, the profile is named after the
102
+ // workspace so a second login can never silently overwrite the first.
103
+ const profileName = profile || workspace;
104
+ const data = { apiKey, workspaceId: workspace, baseUrl, agentName, ...(opts.email ? { email: opts.email } : {}) };
105
+ const writes = { [profileName]: data };
106
+
107
+ let defaultNote = "";
108
+ if (profileName !== "default") {
109
+ const existingDefault = await getProfile("default");
110
+ if (!existingDefault) {
111
+ writes.default = data;
112
+ defaultNote = ` Default profile set to workspace ${workspace}.\n`;
113
+ } else if (existingDefault.workspaceId === workspace) {
114
+ writes.default = data;
115
+ } else if (opts.setDefault || opts.force) {
116
+ const ok = await confirmDefaultReplacement(workspace, opts, existingDefault.workspaceId);
117
+ if (ok) {
118
+ writes.default = data;
119
+ defaultNote = ` Default profile moved from ${existingDefault.workspaceId} to ${workspace}.\n`;
120
+ } else {
121
+ defaultNote = ` Default profile left on workspace ${existingDefault.workspaceId}.\n`;
122
+ }
123
+ } else {
124
+ defaultNote =
125
+ ` Default profile still points at workspace ${existingDefault.workspaceId} — unchanged.\n` +
126
+ ` Use this workspace with: --profile ${profileName} (or re-run login with --set-default)\n`;
127
+ }
128
+ }
129
+
130
+ await setProfiles(writes, profileName);
68
131
  process.stdout.write(chalk.green(
69
- `\n✓ Logged in as profile "${profile}" — workspace ${workspace} (${count} agents visible)\n` +
132
+ `\n✓ Logged in as profile "${profileName}" — workspace ${workspace} (${count} agents visible)\n` +
70
133
  ` Config: ${CONFIG_PATH}\n`,
71
- ));
134
+ ) + chalk.gray(defaultNote));
72
135
  } catch (e) { fail(e); }
73
136
  });
74
137
 
@@ -109,9 +172,14 @@ export function registerAuth(program) {
109
172
  .command("profiles")
110
173
  .description("List saved profiles")
111
174
  .action(async () => {
112
- const ps = await listProfiles();
113
- if (!ps.length) return process.stdout.write("(no profiles — run `advisoros login`)\n");
114
- for (const name of ps) process.stdout.write(`- ${name}\n`);
175
+ const profiles = await allProfiles();
176
+ const names = Object.keys(profiles);
177
+ if (!names.length) return process.stdout.write("(no profiles — run `advisoros login`)\n");
178
+ for (const name of names) {
179
+ const p = profiles[name];
180
+ const scope = p?.admin ? "admin" : (p?.workspaceId || "(no workspace)");
181
+ process.stdout.write(`- ${name} → ${scope}\n`);
182
+ }
115
183
  });
116
184
 
117
185
  program
@@ -1298,7 +1298,9 @@ export function registerWatchlist(program) {
1298
1298
  if (o.on) body.is_active = true;
1299
1299
  if (o.off) body.is_active = false;
1300
1300
  const r = await call("watchlist", "toggle", body, ctxFrom(this));
1301
- emit(this, r, (d) => process.stdout.write(`✔ ${d.item?.name} → ${d.item?.is_active ? "active" : "inactive"}\n`));
1301
+ emit(this, r, (d) => process.stdout.write(
1302
+ `✔ ${d.item?.name ?? id} → ${d.item?.is_active ? "active" : "inactive"} (id ${d.item?.id ?? id}, workspace ${d.workspace_id || "?"}, ${d.status || "ok"})\n`,
1303
+ ));
1302
1304
  } catch (e) { fail(e); }
1303
1305
  });
1304
1306
 
@@ -1316,7 +1318,10 @@ export function registerWatchlist(program) {
1316
1318
  .action(async function () {
1317
1319
  try {
1318
1320
  const r = await call("watchlist", "sync_prompts", {}, ctxFrom(this));
1319
- emit(this, r, () => process.stdout.write("✔ Prompts resynced\n"));
1321
+ emit(this, r, (d) => process.stdout.write(
1322
+ `✔ Prompts resynced — ${d.count ?? (d.prompts || []).length} prompt(s), workspace ${d.workspace_id || "?"} (${d.status || "ok"})\n` +
1323
+ (d.prompts || []).map((x) => ` - ${x.prompt_type || x.type || x.id}`).join("\n") + "\n",
1324
+ ));
1320
1325
  } catch (e) { fail(e); }
1321
1326
  });
1322
1327
 
@@ -1405,7 +1410,10 @@ export function registerIntelligencePrompts(program) {
1405
1410
  .action(async function () {
1406
1411
  try {
1407
1412
  const r = await call("intelligence_prompts", "resync", {}, ctxFrom(this));
1408
- emit(this, r, () => process.stdout.write("✔ Prompts resynced\n"));
1413
+ emit(this, r, (d) => process.stdout.write(
1414
+ `✔ Prompts resynced — ${d.count ?? (d.prompts || []).length} prompt(s), workspace ${d.workspace_id || "?"} (${d.status || "ok"})\n` +
1415
+ (d.prompts || []).map((x) => ` - ${x.prompt_type || x.type || x.id}`).join("\n") + "\n",
1416
+ ));
1409
1417
  } catch (e) { fail(e); }
1410
1418
  });
1411
1419
  }
@@ -111,12 +111,14 @@ export function registerLeads(program) {
111
111
  });
112
112
 
113
113
  withCommonOpts(leads.command("import-from <source>"))
114
- .description("Import leads from any source: linkedin_search | company_search | post_reactors | paste_profiles | csv | heyreach")
115
- .option("--query <q>", "search keywords (linkedin_search / company_search)")
114
+ .description("Import leads from any source: linkedin_search | company_search | post_reactors | paste_profiles | csv | heyreach | sales_nav_url | sales_nav_keywords")
115
+ .option("--query <q>", "search keywords (linkedin_search / company_search / sales_nav_keywords)")
116
+ .option("--url <url>", "full Sales Navigator search URL (sales_nav_url)")
116
117
  .option("--post <url>", "LinkedIn post URL (post_reactors)")
117
118
  .option("--file <path>", "CSV or pasted-profile text file (csv / paste_profiles / heyreach)")
118
119
  .option("--audience <name>", "audience (segment) name or id to attach to")
119
120
  .option("--campaign <id>", "campaign id (required for heyreach)")
121
+ .option("--priority <p>", "queue priority for Sales Navigator imports: normal | high | rush")
120
122
  .option("--limit <n>", "max prospects for LinkedIn sources", "50")
121
123
  .option("--no-enrich", "skip the LinkedIn verify + enrich chain")
122
124
  .action(async function (source) {
@@ -125,9 +127,11 @@ export function registerLeads(program) {
125
127
  const params = {
126
128
  source,
127
129
  query: o.query,
130
+ search_url: o.url,
128
131
  post_url: o.post,
129
132
  audience: o.audience,
130
133
  campaign_id: o.campaign,
134
+ priority: o.priority,
131
135
  limit: Number(o.limit),
132
136
  enrich: o.enrich !== false,
133
137
  };
@@ -137,12 +141,58 @@ export function registerLeads(program) {
137
141
  else params.csv = text;
138
142
  }
139
143
  const r = await call("leads", "import", params, ctxFrom(this));
140
- emit(this, r, (d) => process.stdout.write(
141
- `✓ Imported ${d.imported ?? 0} leads (${d.duplicates ?? 0} duplicates, ${d.skipped ?? 0} skipped, ${d.failed ?? 0} failed)\n` +
142
- (d.job_id ? ` job: ${d.job_id}\n` : "")));
144
+ emit(this, r, (d) => {
145
+ if (d.schedule) {
146
+ process.stdout.write(
147
+ `${d.reused ? "↻ Reusing existing" : "✓ Queued"} background import — ${d.schedule.target_count} leads\n` +
148
+ ` schedule: ${d.schedule.id} (${d.schedule.status})\n` +
149
+ ` It drip-feeds inside your LinkedIn read limits. Track with \`advisoros leads import-schedules\`.\n`,
150
+ );
151
+ return;
152
+ }
153
+ process.stdout.write(
154
+ `✓ Imported ${d.imported ?? 0} leads (${d.duplicates ?? 0} duplicates, ${d.skipped ?? 0} skipped, ${d.failed ?? 0} failed)\n` +
155
+ (d.job_id ? ` job: ${d.job_id}\n` : ""));
156
+ });
157
+ } catch (e) { fail(e); }
158
+ });
159
+
160
+ withCommonOpts(leads.command("import-schedules"))
161
+ .description("List queued/running Sales Navigator background imports")
162
+ .option("--status <s>", "filter by status (queued|running|awaiting_capacity|paused|completed|failed|cancelled)")
163
+ .option("--all", "include finished imports")
164
+ .option("--limit <n>", "max rows", "25")
165
+ .action(async function () {
166
+ try {
167
+ const o = this.optsWithGlobals();
168
+ const r = await call("leads", "import_schedules", {
169
+ status: o.status,
170
+ active: o.all ? false : true,
171
+ limit: Number(o.limit),
172
+ }, ctxFrom(this));
173
+ emit(this, r, (d) => table(d.schedules ?? [], [
174
+ ["id", (s) => s.id.slice(0, 8)],
175
+ ["status", (s) => s.status],
176
+ ["progress", (s) => `${s.imported_count}/${s.target_count}`],
177
+ ["priority", (s) => s.priority],
178
+ ["source", (s) => s.source],
179
+ ["target", (s) => truncate(s.search_url || s.search_query || "", 48)],
180
+ ["reason", (s) => truncate(s.last_reason || "", 40)],
181
+ ]));
143
182
  } catch (e) { fail(e); }
144
183
  });
145
184
 
185
+ withCommonOpts(leads.command("import-schedule <action> <id>"))
186
+ .description("Pause, resume or cancel a scheduled import (action: pause|resume|cancel)")
187
+ .action(async function (action, id) {
188
+ try {
189
+ const r = await call("leads", "import_schedule_update", { id, action }, ctxFrom(this));
190
+ emit(this, r, (d) =>
191
+ process.stdout.write(`✓ Import ${d.schedule.id} is now ${d.schedule.status}\n`));
192
+ } catch (e) { fail(e); }
193
+ });
194
+
195
+
146
196
  withCommonOpts(leads.command("import-status <jobId>"))
147
197
  .description("Poll a running lead import")
148
198
  .action(async function (jobId) {
@@ -1866,6 +1916,25 @@ export function registerAgents(program) {
1866
1916
  } catch (e) { fail(e); }
1867
1917
  });
1868
1918
 
1919
+ withCommonOpts(a.command("pipeline [name]"))
1920
+ .alias("inspect")
1921
+ .description("Inspect an agent's configuration and content pipeline (default: Content Creator Agent)")
1922
+ .action(async function (name) {
1923
+ try {
1924
+ const r = await call("agents", "pipeline", { agent_name: name || "Content Creator Agent" }, ctxFrom(this));
1925
+ emit(this, r, (d) => {
1926
+ if (!d.agent) { process.stdout.write(`${d.error || "Agent not found"}\n`); return; }
1927
+ process.stdout.write(`${d.agent.name} — ${d.agent.status} (${d.agent.agent_type || "agent"})\n\n`);
1928
+ process.stdout.write(table(
1929
+ ["stage", "what it does"],
1930
+ (d.pipeline?.stages || []).map((s) => [s.id, truncate(s.detail, 80)]),
1931
+ ) + "\n");
1932
+ process.stdout.write(`Blocking gates: ${(d.pipeline?.blocking_gates || []).join(", ")}\n`);
1933
+ process.stdout.write(`Advisory gates: ${(d.pipeline?.advisory_gates || []).join(", ")}\n`);
1934
+ });
1935
+ } catch (e) { fail(e); }
1936
+ });
1937
+
1869
1938
  withCommonOpts(a.command("runs"))
1870
1939
  .description("List recent agent runs in this workspace")
1871
1940
  .option("--agent <name>", "filter by agent_name")
@@ -124,11 +124,21 @@ export function registerVoice(program) {
124
124
  try {
125
125
  const r = await call("voice", "card", { rebuild: !!opts.rebuild }, ctxFrom(this));
126
126
  emit(this, r, (d) => {
127
- if (!d.card) return process.stdout.write("No voice card — add a writing sample first.\n");
127
+ if (!d.card) {
128
+ process.stdout.write(
129
+ `✖ ${d.error || "No voice card — add a writing sample first."}\n` +
130
+ ` workspace: ${d.workspace_id || "?"} status: ${d.status || "error"}\n`,
131
+ );
132
+ process.exitCode = 1;
133
+ return;
134
+ }
128
135
  process.stdout.write(
129
136
  `Built: ${d.card.built_at}\nReferences: ${(d.card.reference_titles || []).join(", ")}\n\nRules:\n` +
130
137
  (d.card.rules || []).map((x) => ` - ${x}`).join("\n") +
131
- `\n\nTargets:\n` + (d.targets || []).map((x) => ` - ${x}`).join("\n") + "\n",
138
+ `\n\nTargets:\n` + (d.targets || []).map((x) => ` - ${x}`).join("\n") +
139
+ `\n\nWorkspace: ${d.workspace_id || "?"} ${d.rebuilt ? "rebuilt" : "cached"} status: ${d.status || "ok"}` +
140
+ `\nReference ids: ${(d.reference_item_ids || []).join(", ") || "(none recorded)"}` +
141
+ (d.primary_reference_id ? `\nPrimary reference: ${d.primary_reference_id}` : "") + "\n",
132
142
  );
133
143
  });
134
144
  } catch (e) { fail(e); }
package/src/config.mjs CHANGED
@@ -38,6 +38,32 @@ export async function setProfile(name, data) {
38
38
  await writeConfig(cfg);
39
39
  }
40
40
 
41
+ /** Save several profiles in one write (e.g. "default" plus a workspace alias). */
42
+ export async function setProfiles(entries, currentProfile) {
43
+ const cfg = await readConfig();
44
+ cfg.profiles = cfg.profiles ?? {};
45
+ for (const [name, data] of Object.entries(entries)) cfg.profiles[name] = data;
46
+ if (currentProfile) cfg.currentProfile = currentProfile;
47
+ await writeConfig(cfg);
48
+ }
49
+
50
+ /** All saved profiles with their data — used to detect overwrite collisions. */
51
+ export async function allProfiles() {
52
+ const cfg = await readConfig();
53
+ return cfg.profiles ?? {};
54
+ }
55
+
56
+ /** The saved profile already pointing at this workspace, if any. */
57
+ export async function findProfileByWorkspace(workspaceId) {
58
+ if (!workspaceId) return null;
59
+ const profiles = await allProfiles();
60
+ for (const [name, p] of Object.entries(profiles)) {
61
+ if (p?.workspaceId === workspaceId) return { name, profile: p };
62
+ }
63
+ return null;
64
+ }
65
+
66
+
41
67
  export async function listProfiles() {
42
68
  const cfg = await readConfig();
43
69
  return Object.keys(cfg.profiles ?? {});