@series-a/cli 0.22.3 → 0.23.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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ 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
+
7
21
  ## 0.22.3 — Sales Navigator imports from the CLI
8
22
 
9
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-a/cli",
3
- "version": "0.22.3",
3
+ "version": "0.23.1",
4
4
  "description": "AdvisorPro command-line interface — drive leads, campaigns, sequences and agents from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
  }
@@ -1916,6 +1916,25 @@ export function registerAgents(program) {
1916
1916
  } catch (e) { fail(e); }
1917
1917
  });
1918
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
+
1919
1938
  withCommonOpts(a.command("runs"))
1920
1939
  .description("List recent agent runs in this workspace")
1921
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 ?? {});