@series-a/cli 1.4.0 → 1.5.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 +16 -0
- package/package.json +1 -1
- package/src/commands/references.mjs +76 -0
- package/src/commands/research.mjs +71 -0
- package/src/commands/resources.mjs +41 -0
- package/src/index.mjs +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,14 +4,30 @@ 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.5.0 — Calendar control, references, analytics report, safer campaign holds
|
|
8
|
+
|
|
9
|
+
Version follows 1.4.1, the latest release on npm (it was published without a changelog entry).
|
|
10
|
+
|
|
11
|
+
- Calendar: `calendar schedule <id> --at <iso>` moves or reschedules a post, `calendar unschedule <id>` cancels it and keeps the content as a draft, and `calendar delete <id>` removes it. All three cancel on Taplio first when Taplio owns the post. `content unschedule <id>` does the same from the content side.
|
|
12
|
+
- Links: `references list|show|library|verify` shows the published links stored for each post and checks they still resolve.
|
|
13
|
+
- Reports: `advisoros analytics report [--days n] [--format md|json] [--out file]` writes a full branded performance report (summary, outreach funnel, campaigns, content, revenue impact).
|
|
14
|
+
- Campaigns (server-side, no CLI change needed): LinkedIn holds now last only until the time Madnion gives; with no time given the campaign retries within 5 minutes and asks Madnion again. "If accepted" always checks LinkedIn at send time, so someone who just accepted is no longer sent down the "No" path. Slow-setup campaigns always send at least one action per run. Wrong-name checks no longer block normal greetings such as "Hi, thanks for connecting".
|
|
15
|
+
- Agents run Monday to Friday (UTC); manual runs still work on weekends.
|
|
16
|
+
- Blog generation is white-label: no platform name or platform links in workspace content.
|
|
17
|
+
|
|
7
18
|
## 1.4.0 — Company news, brand imagery fix, API reliability
|
|
8
19
|
|
|
9
20
|
Version jumps from 0.28.0 to 1.4.0 because npm already has releases up to 1.3.2.
|
|
10
21
|
|
|
22
|
+
- 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.
|
|
23
|
+
- Fix: scheduling or re-timing an existing calendar post no longer wipes its images, text, hashtags or CTA.
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
11
26
|
- 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.
|
|
12
27
|
- `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".
|
|
13
28
|
- 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.
|
|
14
29
|
- 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.
|
|
30
|
+
- 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.
|
|
15
31
|
- 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.
|
|
16
32
|
|
|
17
33
|
## 0.28.0 — Instantly email campaigns
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { call } from "../api.mjs";
|
|
2
|
+
import { emit, fail, ctxFrom, withCommonOpts } from "../ui.mjs";
|
|
3
|
+
|
|
4
|
+
const ok = (v) => (v === null || v === undefined ? "" : v ? "" : " (dead)");
|
|
5
|
+
|
|
6
|
+
function printList(d) {
|
|
7
|
+
for (const r of d.references || []) {
|
|
8
|
+
const date = r.published_at ? String(r.published_at).slice(0, 10) : "----------";
|
|
9
|
+
process.stdout.write(`${date} ${r.id} ${r.title || "(untitled)"}\n`);
|
|
10
|
+
if (r.blog_url) process.stdout.write(` blog: ${r.blog_url}${ok(r.blog_ok)}\n`);
|
|
11
|
+
if (r.linkedin_url) process.stdout.write(` linkedin: ${r.linkedin_url}${ok(r.linkedin_ok)}\n`);
|
|
12
|
+
if (r.missing_link) process.stdout.write(` MISSING LINK — published but no address saved\n`);
|
|
13
|
+
}
|
|
14
|
+
for (const v of d.videos || []) process.stdout.write(`video ${v.id} ${v.title}\n ${v.video_url}\n`);
|
|
15
|
+
process.stdout.write(`\n${d.total ?? 0} published post(s), ${d.missing_link_count ?? 0} without a saved link in this page.\n`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerReferences(program) {
|
|
19
|
+
const r = program
|
|
20
|
+
.command("references")
|
|
21
|
+
.alias("refs")
|
|
22
|
+
.description("Published links (blog, LinkedIn, video) that actually went out — the same list AI writers and replies use");
|
|
23
|
+
|
|
24
|
+
withCommonOpts(r.command("list"))
|
|
25
|
+
.description("List published links")
|
|
26
|
+
.option("--channel <c>", "blog | linkedin | video | all", "all")
|
|
27
|
+
.option("--since <date>", "only posts published on/after this date (YYYY-MM-DD)")
|
|
28
|
+
.option("--search <text>", "filter by title")
|
|
29
|
+
.option("--missing", "only published posts with no saved link")
|
|
30
|
+
.option("--limit <n>", "max rows (default 100, max 500)")
|
|
31
|
+
.action(async function (o) {
|
|
32
|
+
try {
|
|
33
|
+
const res = await call("references", "list", {
|
|
34
|
+
channel: o.channel, since: o.since, search: o.search, missing: !!o.missing,
|
|
35
|
+
limit: o.limit ? Number(o.limit) : undefined,
|
|
36
|
+
}, ctxFrom(this));
|
|
37
|
+
emit(this, res, printList);
|
|
38
|
+
} catch (e) { fail(e); }
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
withCommonOpts(r.command("show <id>"))
|
|
42
|
+
.alias("get")
|
|
43
|
+
.description("Show one published post's links and publishing records")
|
|
44
|
+
.action(async function (id) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await call("references", "show", { id }, ctxFrom(this));
|
|
47
|
+
emit(this, res, (d) => {
|
|
48
|
+
printList({ references: [d.reference], total: 1, missing_link_count: d.reference?.missing_link ? 1 : 0 });
|
|
49
|
+
for (const x of d.distributions || []) process.stdout.write(` ${x.platform} ${x.status} ${x.platform_url || "-"}\n`);
|
|
50
|
+
});
|
|
51
|
+
} catch (e) { fail(e); }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
withCommonOpts(r.command("library"))
|
|
55
|
+
.description("Show exactly what the AI writers see, ranked for a topic")
|
|
56
|
+
.option("--topic <text>", "topic to rank by")
|
|
57
|
+
.option("--limit <n>", "how many (default 15)")
|
|
58
|
+
.action(async function (o) {
|
|
59
|
+
try {
|
|
60
|
+
const res = await call("references", "library", { topic: o.topic, limit: o.limit ? Number(o.limit) : undefined }, ctxFrom(this));
|
|
61
|
+
emit(this, res, (d) => {
|
|
62
|
+
for (const i of d.library || []) process.stdout.write(`${i.kind.padEnd(8)} ${i.title}\n ${i.canonical_url || i.linkedin_url || i.video_url || ""}\n`);
|
|
63
|
+
});
|
|
64
|
+
} catch (e) { fail(e); }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
withCommonOpts(r.command("verify"))
|
|
68
|
+
.description("Re-check that published links still work")
|
|
69
|
+
.option("--dry-run", "check without saving results")
|
|
70
|
+
.action(async function (o) {
|
|
71
|
+
try {
|
|
72
|
+
const res = await call("references", "verify", { dry_run: !!o.dryRun }, ctxFrom(this));
|
|
73
|
+
emit(this, res, (d) => process.stdout.write(JSON.stringify(d.verify, null, 2) + "\n"));
|
|
74
|
+
} catch (e) { fail(e); }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -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);
|
|
@@ -1161,11 +1185,14 @@ export function registerContent(program) {
|
|
|
1161
1185
|
.option("--tags <csv>", "tags")
|
|
1162
1186
|
.option("--scheduled <ts>", "ISO timestamp (sets status=scheduled)")
|
|
1163
1187
|
.option("--campaign <id>", "campaign_id")
|
|
1188
|
+
.option(...IMG_OPT)
|
|
1164
1189
|
.action(async function (opts) {
|
|
1165
1190
|
try {
|
|
1166
1191
|
let body = opts.body;
|
|
1167
1192
|
if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
|
|
1193
|
+
const media_urls = await resolveImages(opts.image, this);
|
|
1168
1194
|
const r = await call("content", "create", {
|
|
1195
|
+
media_urls,
|
|
1169
1196
|
title: opts.title, type: opts.type, body: body ?? "",
|
|
1170
1197
|
target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
|
|
1171
1198
|
tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
|
|
@@ -1185,11 +1212,15 @@ export function registerContent(program) {
|
|
|
1185
1212
|
.option("--platforms <csv>")
|
|
1186
1213
|
.option("--tags <csv>")
|
|
1187
1214
|
.option("--scheduled <ts>")
|
|
1215
|
+
.option(...IMG_OPT)
|
|
1216
|
+
.option("--clear-images", "remove all images")
|
|
1188
1217
|
.action(async function (id, opts) {
|
|
1189
1218
|
try {
|
|
1190
1219
|
let body = opts.body;
|
|
1191
1220
|
if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
|
|
1221
|
+
const media_urls = await mediaFrom(opts, this);
|
|
1192
1222
|
const r = await call("content", "update", {
|
|
1223
|
+
media_urls,
|
|
1193
1224
|
id, title: opts.title, body, status: opts.status,
|
|
1194
1225
|
target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
|
|
1195
1226
|
tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
|
|
@@ -1205,9 +1236,12 @@ export function registerContent(program) {
|
|
|
1205
1236
|
.option("--publish-as <p>", "personal | company")
|
|
1206
1237
|
.option("--company-page <id>", "company page id when publishing as company")
|
|
1207
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)
|
|
1208
1240
|
.action(async function (id, ts, opts) {
|
|
1209
1241
|
try {
|
|
1242
|
+
const media_urls = await resolveImages(opts.image, this);
|
|
1210
1243
|
const r = await call("content", "schedule", {
|
|
1244
|
+
media_urls,
|
|
1211
1245
|
id, scheduled_for: ts, channel: opts.channel,
|
|
1212
1246
|
publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
|
|
1213
1247
|
}, ctxFrom(this));
|
|
@@ -1422,11 +1456,14 @@ export function registerCalendar(program) {
|
|
|
1422
1456
|
.option("--campaign <id>", "campaign_id")
|
|
1423
1457
|
.option("--status <s>", "draft | scheduled | published", "draft")
|
|
1424
1458
|
.option("--at <iso>", "ISO timestamp — schedules the post immediately via the live engine")
|
|
1459
|
+
.option(...IMG_OPT)
|
|
1425
1460
|
.action(async function (opts) {
|
|
1426
1461
|
try {
|
|
1427
1462
|
let content = opts.content;
|
|
1428
1463
|
if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
|
|
1464
|
+
const media_urls = await resolveImages(opts.image, this);
|
|
1429
1465
|
const r = await call("calendar", "create", {
|
|
1466
|
+
media_urls,
|
|
1430
1467
|
scheduled_for: opts.at,
|
|
1431
1468
|
title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
|
|
1432
1469
|
platform: opts.platform, content: content ?? "",
|
|
@@ -1448,11 +1485,15 @@ export function registerCalendar(program) {
|
|
|
1448
1485
|
.option("--platform <p>")
|
|
1449
1486
|
.option("--hashtags <csv>")
|
|
1450
1487
|
.option("--cta <text>")
|
|
1488
|
+
.option(...IMG_OPT)
|
|
1489
|
+
.option("--clear-images", "remove all images")
|
|
1451
1490
|
.action(async function (id, opts) {
|
|
1452
1491
|
try {
|
|
1453
1492
|
let content = opts.content;
|
|
1454
1493
|
if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
|
|
1494
|
+
const media_urls = await mediaFrom(opts, this);
|
|
1455
1495
|
const r = await call("calendar", "update", {
|
|
1496
|
+
media_urls,
|
|
1456
1497
|
id, title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
|
|
1457
1498
|
content, status: opts.status, platform: opts.platform,
|
|
1458
1499
|
hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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 { registerReferences } from "./commands/references.mjs";
|
|
5
|
+
import { registerResearch } from "./commands/research.mjs";
|
|
4
6
|
import { Command } from "commander";
|
|
5
7
|
import { readFileSync } from "node:fs";
|
|
6
8
|
import { registerAuth } from "./commands/auth.mjs";
|
|
@@ -38,11 +40,13 @@ registerInbox(program);
|
|
|
38
40
|
registerContent(program);
|
|
39
41
|
registerCalendar(program);
|
|
40
42
|
registerKnowledge(program);
|
|
43
|
+
registerResearch(program);
|
|
41
44
|
registerSignals(program);
|
|
42
45
|
registerPipelines(program);
|
|
43
46
|
registerStrategies(program);
|
|
44
47
|
registerVoice(program);
|
|
45
48
|
registerBrand(program);
|
|
49
|
+
registerReferences(program);
|
|
46
50
|
registerAuthority(program);
|
|
47
51
|
registerEmail(program);
|
|
48
52
|
registerBriefs(program);
|