@series-a/cli 1.3.2 → 1.4.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,21 @@ 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
+ ## 1.4.0 — Company news, brand imagery fix, API reliability
8
+
9
+ Version jumps from 0.28.0 to 1.4.0 because npm already has releases up to 1.3.2.
10
+
11
+ - Images on posts: `--image <url-or-file>` (repeat up to 8) on `content create|update|schedule` and `calendar create|update`; `--clear-images` on updates. Local JPG/PNG/GIF/WebP files (≤7 MB) are uploaded automatically.
12
+ - Fix: scheduling or re-timing an existing calendar post no longer wipes its images, text, hashtags or CTA.
13
+ - Image limits: posts scheduled through Taplio only carry the first image, because that is all Taplio accepts. Uploaded image files are publicly viewable by link — workspace settings block new public storage folders, so files are saved in the knowledge base's existing public folder.
14
+
15
+ - New `advisoros leads company-refresh <ids...>` fetches company details and recent company news for contacts through Brave Search. It only works when the workspace's "Company news" switch is on, each company is refreshed at most once every 14 days, and every lookup is billed as `web_search` credits.
16
+ - `brand set-imagery` works again for workspaces whose owner is stored in the older `ws_…` format. Before, it failed with "One of the supplied ids is not a valid identifier".
17
+ - API reliability: the CLI, MCP and Agent API briefly returned 503 because the server function crashed on startup. That is fixed. No change is needed on the CLI side.
18
+ - Server-side agent fixes also reach CLI-triggered runs: Market Research, News Monitor, Competitive Intel, Social Listening and the Intelligence Synthesizer no longer crash when the AI sends its lists in a different format.
19
+ - New Firecrawl commands (through Madnion, billed as workspace credits): `knowledge import-site <url> --limit N` (up to 25 pages into the knowledge base), `knowledge parse <file>` (PDF/Word up to 5 MB), and `research start|status|cancel|list` for deep research jobs with a credit cap (only when the workspace's Deep research switch is on). Agent API: new `research` resource.
20
+ - Campaigns: "Stop if replied (else continue)" checks only that contact's own saved replies (it never spends a LinkedIn read), and each contact's branches and waits are tracked separately.
21
+
7
22
  ## 0.28.0 — Instantly email campaigns
8
23
 
9
24
  - New `advisoros email` command group (alias `instantly`) drives the workspace's Instantly account through its own connected key — the key never leaves the server.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-a/cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "description": "AdvisorPro command-line interface — drive leads, campaigns, sequences and agents from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,71 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
3
+ import { call } from "../api.mjs";
4
+ import { emit, fail, ctxFrom, withCommonOpts, table } from "../ui.mjs";
5
+
6
+ const jobLine = (j) => `${j.id} ${j.kind} ${j.status} ${j.credits_used ?? 0} credits`;
7
+
8
+ export function registerResearch(program) {
9
+ const kb = program.commands.find((c) => c.name() === "knowledge") ?? program.command("knowledge");
10
+
11
+ withCommonOpts(kb.command("import-site <url>"))
12
+ .description("Import a whole website's key pages into the knowledge base (Firecrawl via Madnion)")
13
+ .option("--limit <n>", "maximum pages (1-25)", "10")
14
+ .action(async function (url, opts) {
15
+ try {
16
+ const limit = Math.max(1, Math.min(25, Number(opts.limit) || 10));
17
+ const r = await call("research", "crawl", { url, limit, save_to_knowledge: true }, ctxFrom(this));
18
+ emit(this, r, (d) => process.stdout.write(`✓ Import started: ${jobLine(d.job || {})}\n Check with: advisoros research status ${d.job?.id}\n`));
19
+ } catch (e) { fail(e); }
20
+ });
21
+
22
+ withCommonOpts(kb.command("parse <file>"))
23
+ .description("Extract text from a PDF or Word document (max 5 MB) into the knowledge base")
24
+ .action(async function (file) {
25
+ try {
26
+ const buf = await readFile(file);
27
+ if (buf.length > 5 * 1024 * 1024) throw new Error("File is larger than 5 MB");
28
+ const r = await call("research", "parse", { filename: basename(file), file_base64: buf.toString("base64"), save_to_knowledge: true }, ctxFrom(this));
29
+ emit(this, r, (d) => process.stdout.write(`✓ Parsed ${basename(file)} (${(d.markdown || "").length} characters)\n`));
30
+ } catch (e) { fail(e); }
31
+ });
32
+
33
+ const rs = program.command("research").description("Deep research and long Firecrawl jobs (off until enabled per workspace)");
34
+
35
+ withCommonOpts(rs.command("start <question...>"))
36
+ .description("Start a deep research job (needs the workspace's Deep research switch on)")
37
+ .option("--max-credits <n>", "credit cap for this job", "200")
38
+ .action(async function (question, opts) {
39
+ try {
40
+ const r = await call("research", "start", { prompt: question.join(" "), max_credits: Number(opts.maxCredits) || 200 }, ctxFrom(this));
41
+ emit(this, r, (d) => process.stdout.write(`✓ Research started: ${jobLine(d.job || {})}\n`));
42
+ } catch (e) { fail(e); }
43
+ });
44
+
45
+ withCommonOpts(rs.command("status <id>"))
46
+ .description("Show a job's progress and result")
47
+ .action(async function (id) {
48
+ try {
49
+ const r = await call("research", "status", { id }, ctxFrom(this));
50
+ emit(this, r, (d) => process.stdout.write(`${jobLine(d.job || {})}\n`));
51
+ } catch (e) { fail(e); }
52
+ });
53
+
54
+ withCommonOpts(rs.command("cancel <id>"))
55
+ .description("Cancel a running job and release its unused credit hold")
56
+ .action(async function (id) {
57
+ try {
58
+ const r = await call("research", "cancel", { id }, ctxFrom(this));
59
+ emit(this, r, (d) => process.stdout.write(`✓ ${jobLine(d.job || { id, status: "cancelled" })}\n`));
60
+ } catch (e) { fail(e); }
61
+ });
62
+
63
+ withCommonOpts(rs.command("list"))
64
+ .description("List recent Firecrawl jobs for this workspace")
65
+ .action(async function () {
66
+ try {
67
+ const r = await call("research", "list", {}, ctxFrom(this));
68
+ emit(this, r, (d) => table((d.jobs || []).map((j) => ({ id: j.id, kind: j.kind, status: j.status, credits: j.credits_used ?? 0, created: j.created_at }))));
69
+ } catch (e) { fail(e); }
70
+ });
71
+ }
@@ -2,6 +2,30 @@ import { readFile } from "node:fs/promises";
2
2
  import { call } from "../api.mjs";
3
3
  import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
4
4
 
5
+ const IMG_TYPES = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp" };
6
+ const collect = (v, prev = []) => prev.concat([v]);
7
+ /** URLs pass through; local files are uploaded to workspace storage first. */
8
+ async function resolveImages(list, cmd) {
9
+ if (!list || !list.length) return undefined;
10
+ if (list.length > 8) throw new Error("At most 8 images per post");
11
+ const out = [];
12
+ for (const item of list) {
13
+ if (/^https?:\/\//i.test(item)) { out.push(item); continue; }
14
+ const ext = (item.match(/\.[a-z0-9]+$/i)?.[0] || "").toLowerCase();
15
+ const type = IMG_TYPES[ext];
16
+ if (!type) throw new Error(`${item}: use a JPG, PNG, GIF or WebP file, or an http(s) link`);
17
+ const buf = await readFile(item);
18
+ if (buf.length > 7 * 1024 * 1024) throw new Error(`${item}: larger than 7 MB`);
19
+ const r = await call("media", "upload", { content_type: type, data_base64: buf.toString("base64") }, ctxFrom(cmd));
20
+ const url = r?.data?.url ?? r?.url;
21
+ if (!url) throw new Error(`${item}: upload failed`);
22
+ out.push(url);
23
+ }
24
+ return out;
25
+ }
26
+ const mediaFrom = async (opts, cmd) => (opts.clearImages ? [] : await resolveImages(opts.image, cmd));
27
+ const IMG_OPT = ["--image <url-or-file>", "attach an image (repeat up to 8; first is the cover)", collect];
28
+
5
29
  function parseCsv(text) {
6
30
  // Tiny CSV parser: header row + comma-separated values, quoted fields supported.
7
31
  const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
@@ -68,6 +92,20 @@ export function registerLeads(program) {
68
92
  } catch (e) { fail(e); }
69
93
  });
70
94
 
95
+ withCommonOpts(leads.command("company-refresh <ids...>"))
96
+ .description("Refresh company news (Brave via Madnion) for up to 25 leads")
97
+ .option("--force", "Ignore the 14-day freshness window")
98
+ .action(async function (ids, opts) {
99
+ try {
100
+ const r = await call("leads", "company_refresh", { ids, force: !!opts.force }, ctxFrom(this));
101
+ emit(this, r, (d) => {
102
+ const c = d.company_refresh || {};
103
+ if (c.skipped) return process.stdout.write(`Skipped: ${c.reason ?? "company news is off for this workspace"}\n`);
104
+ process.stdout.write(`✓ ${c.enriched ?? 0} companies refreshed, ${c.skipped_fresh ?? 0} already fresh, ${(c.errors || []).length} errors\n`);
105
+ });
106
+ } catch (e) { fail(e); }
107
+ });
108
+
71
109
  withCommonOpts(leads.command("set-status <id> <status>"))
72
110
  .description("Update a lead's status")
73
111
  .action(async function (id, status) {
@@ -1147,11 +1185,14 @@ export function registerContent(program) {
1147
1185
  .option("--tags <csv>", "tags")
1148
1186
  .option("--scheduled <ts>", "ISO timestamp (sets status=scheduled)")
1149
1187
  .option("--campaign <id>", "campaign_id")
1188
+ .option(...IMG_OPT)
1150
1189
  .action(async function (opts) {
1151
1190
  try {
1152
1191
  let body = opts.body;
1153
1192
  if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
1193
+ const media_urls = await resolveImages(opts.image, this);
1154
1194
  const r = await call("content", "create", {
1195
+ media_urls,
1155
1196
  title: opts.title, type: opts.type, body: body ?? "",
1156
1197
  target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
1157
1198
  tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
@@ -1171,11 +1212,15 @@ export function registerContent(program) {
1171
1212
  .option("--platforms <csv>")
1172
1213
  .option("--tags <csv>")
1173
1214
  .option("--scheduled <ts>")
1215
+ .option(...IMG_OPT)
1216
+ .option("--clear-images", "remove all images")
1174
1217
  .action(async function (id, opts) {
1175
1218
  try {
1176
1219
  let body = opts.body;
1177
1220
  if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
1221
+ const media_urls = await mediaFrom(opts, this);
1178
1222
  const r = await call("content", "update", {
1223
+ media_urls,
1179
1224
  id, title: opts.title, body, status: opts.status,
1180
1225
  target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
1181
1226
  tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
@@ -1191,9 +1236,12 @@ export function registerContent(program) {
1191
1236
  .option("--publish-as <p>", "personal | company")
1192
1237
  .option("--company-page <id>", "company page id when publishing as company")
1193
1238
  .option("--timezone <tz>", "IANA timezone label to store")
1239
+ .option("--image <url-or-file>", "override the draft's images for this post (repeatable)", collect)
1194
1240
  .action(async function (id, ts, opts) {
1195
1241
  try {
1242
+ const media_urls = await resolveImages(opts.image, this);
1196
1243
  const r = await call("content", "schedule", {
1244
+ media_urls,
1197
1245
  id, scheduled_for: ts, channel: opts.channel,
1198
1246
  publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
1199
1247
  }, ctxFrom(this));
@@ -1408,11 +1456,14 @@ export function registerCalendar(program) {
1408
1456
  .option("--campaign <id>", "campaign_id")
1409
1457
  .option("--status <s>", "draft | scheduled | published", "draft")
1410
1458
  .option("--at <iso>", "ISO timestamp — schedules the post immediately via the live engine")
1459
+ .option(...IMG_OPT)
1411
1460
  .action(async function (opts) {
1412
1461
  try {
1413
1462
  let content = opts.content;
1414
1463
  if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
1464
+ const media_urls = await resolveImages(opts.image, this);
1415
1465
  const r = await call("calendar", "create", {
1466
+ media_urls,
1416
1467
  scheduled_for: opts.at,
1417
1468
  title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
1418
1469
  platform: opts.platform, content: content ?? "",
@@ -1434,11 +1485,15 @@ export function registerCalendar(program) {
1434
1485
  .option("--platform <p>")
1435
1486
  .option("--hashtags <csv>")
1436
1487
  .option("--cta <text>")
1488
+ .option(...IMG_OPT)
1489
+ .option("--clear-images", "remove all images")
1437
1490
  .action(async function (id, opts) {
1438
1491
  try {
1439
1492
  let content = opts.content;
1440
1493
  if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
1494
+ const media_urls = await mediaFrom(opts, this);
1441
1495
  const r = await call("calendar", "update", {
1496
+ media_urls,
1442
1497
  id, title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
1443
1498
  content, status: opts.status, platform: opts.platform,
1444
1499
  hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
package/src/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { registerStrategies } from "./commands/strategies.mjs";
2
2
  import { registerVoice } from "./commands/voice.mjs";
3
3
  import { registerBrand } from "./commands/brand.mjs";
4
+ import { registerResearch } from "./commands/research.mjs";
4
5
  import { Command } from "commander";
5
6
  import { readFileSync } from "node:fs";
6
7
  import { registerAuth } from "./commands/auth.mjs";
@@ -38,6 +39,7 @@ registerInbox(program);
38
39
  registerContent(program);
39
40
  registerCalendar(program);
40
41
  registerKnowledge(program);
42
+ registerResearch(program);
41
43
  registerSignals(program);
42
44
  registerPipelines(program);
43
45
  registerStrategies(program);