rankcontrol 0.8.2 → 0.9.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/README.md CHANGED
@@ -43,7 +43,7 @@ npx rankcontrol query-edit <queryId> "new text"
43
43
  npx rankcontrol query-track <queryId> off # pause weekly checks (frees a slot)
44
44
  npx rankcontrol query-remove <queryId>
45
45
  npx rankcontrol competitors
46
- npx rankcontrol competitor-add "Acme" https://acme.com
46
+ npx rankcontrol competitor-add "Acme" # site resolved from the name; pass a URL to override
47
47
  npx rankcontrol competitor-remove <competitorId>
48
48
 
49
49
  # Content pipeline
@@ -96,8 +96,8 @@ npx rankcontrol network-remove-placement <placementId> --confirm
96
96
  npx rankcontrol repurpose [contentId] # published articles + social drafts (8 platforms)
97
97
  npx rankcontrol repurpose-generate <contentId> --platforms twitter --confirm
98
98
  npx rankcontrol repurpose-edit <draftId> --title "New hook"
99
- npx rankcontrol repurpose-channels # Postiz channel ids
100
- npx rankcontrol repurpose-push <draftId> --channels <id> --confirm
99
+ npx rankcontrol repurpose-channels # Postiz and Buffer channel ids
100
+ npx rankcontrol repurpose-push <draftId> --channels <id> --confirm # --scheduler postiz|buffer when both are connected
101
101
  npx rankcontrol repurpose-mark-posted <draftId>
102
102
  npx rankcontrol social # Reddit/X threads where your articles fit
103
103
  npx rankcontrol social-stats
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "rankcontrol",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "RankControl CLI + MCP server: drive your SEO/AI-visibility workspace from the terminal or any AI agent",
5
5
  "license": "MIT",
6
+ "scripts": {
7
+ "prepublishOnly": "node ../../scripts/check-package-parity.mjs"
8
+ },
6
9
  "homepage": "https://rctrl.com",
7
10
  "type": "module",
8
11
  "bin": {
package/src/cli.mjs CHANGED
@@ -63,6 +63,11 @@ export function runCli(argv) {
63
63
  .description("Composite visibility score blending AI citation rate with Google and Bing rank share, with subscores")
64
64
  .action(() => api.visibilityScore().then(out).catch(fail));
65
65
 
66
+ program
67
+ .command("expedition")
68
+ .description("First-run setup progress: five acts with per-step completion and the current act")
69
+ .action(() => api.expedition().then(out).catch(fail));
70
+
66
71
  program
67
72
  .command("visibility")
68
73
  .description("Daily AI visibility score trend from the stored weekly citation checks")
@@ -228,9 +233,9 @@ export function runCli(argv) {
228
233
 
229
234
  program
230
235
  .command("competitor-add")
231
- .description("Track a competitor (max 10)")
236
+ .description("Track a competitor (max 10); the site is resolved from the name when omitted")
232
237
  .argument("<name>", "Competitor name")
233
- .argument("<websiteUrl>", "Competitor website URL")
238
+ .argument("[websiteUrl]", "Competitor website URL (optional)")
234
239
  .action((name, websiteUrl) =>
235
240
  api.addCompetitor(name, websiteUrl).then(out).catch(fail)
236
241
  );
@@ -487,11 +492,10 @@ export function runCli(argv) {
487
492
 
488
493
  program
489
494
  .command("reschedule <contentId> <date>")
490
- .description("Move a planned article to a day (YYYY-MM-DD, local time; needs the flexible schedule)")
495
+ .description("Move a planned article to a day (YYYY-MM-DD in the workspace's timezone; needs the flexible schedule)")
491
496
  .action((contentId, date) => {
492
- const day = new Date(`${date}T00:00:00`);
493
- if (Number.isNaN(day.getTime())) return fail(new Error("Date must be YYYY-MM-DD"));
494
- return api.reschedule(contentId, day.getTime()).then(out).catch(fail);
497
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return fail(new Error("Date must be YYYY-MM-DD"));
498
+ return api.reschedule(contentId, date).then(out).catch(fail);
495
499
  });
496
500
 
497
501
  program
@@ -567,20 +571,22 @@ export function runCli(argv) {
567
571
 
568
572
  program
569
573
  .command("repurpose-channels")
570
- .description("Connected Postiz channels (ids needed for repurpose-push)")
574
+ .description("Connected Postiz and Buffer channels (ids needed for repurpose-push)")
571
575
  .action(() => api.repurposeChannels().then(out).catch(fail));
572
576
 
573
577
  program
574
578
  .command("repurpose-push <draftId>")
575
- .description("Send a draft to Postiz (dry run unless --confirm)")
576
- .option("--channels <id1,id2>", "Postiz channel ids (see repurpose-channels)")
577
- .option("--when <now|schedule|draft>", "Postiz mode", "now")
579
+ .description("Send a draft to Postiz or Buffer (dry run unless --confirm)")
580
+ .option("--scheduler <postiz|buffer>", "Which scheduler (required when both are connected)")
581
+ .option("--channels <id1,id2>", "Channel ids (see repurpose-channels)")
582
+ .option("--when <now|schedule|draft|queue>", "Mode; queue is Buffer only", "now")
578
583
  .option("--date <iso>", "ISO time for --when schedule")
579
584
  .option("--confirm", "Actually push (publicly visible)")
580
585
  .action((draftId, opts) =>
581
586
  api
582
587
  .repurposePush({
583
588
  draftId,
589
+ scheduler: opts.scheduler,
584
590
  integrationIds: opts.channels ? list(opts.channels) : undefined,
585
591
  scheduleType: opts.when,
586
592
  date: opts.date,
package/src/client.mjs CHANGED
@@ -103,6 +103,7 @@ export const api = {
103
103
  return request("GET", `/api/v1/citations${qs ? `?${qs}` : ""}`);
104
104
  },
105
105
  visibilityScore: () => request("GET", "/api/v1/visibility/score"),
106
+ expedition: () => request("GET", "/api/v1/expedition"),
106
107
  planningCapacity: () => request("GET", "/api/v1/content/planning-capacity"),
107
108
  jobs: (limit = 20) => request("GET", `/api/v1/jobs?limit=${limit}`),
108
109
  content: () => request("GET", "/api/v1/content"),
@@ -123,8 +124,8 @@ export const api = {
123
124
  articleSettings: () => request("GET", "/api/v1/settings/articles"),
124
125
  updateArticleSettings: (opts) =>
125
126
  request("POST", "/api/v1/settings/articles", opts),
126
- reschedule: (contentId, targetDayStartMs) =>
127
- request("POST", "/api/v1/content/reschedule", { contentId, targetDayStartMs }),
127
+ reschedule: (contentId, date) =>
128
+ request("POST", "/api/v1/content/reschedule", { contentId, date }),
128
129
  sitePages: () => request("GET", "/api/v1/links/pages"),
129
130
  detectSiteLinks: (source, url) =>
130
131
  request("POST", "/api/v1/links/detect", { source, url }),
package/src/login.mjs CHANGED
@@ -13,13 +13,10 @@ const DEFAULT_SCOPES = [
13
13
  "read:content",
14
14
  "read:analytics",
15
15
  "write:content",
16
- "write:queries",
17
16
  "write:backlinks",
18
17
  "write:social",
19
18
  "write:publish",
20
- "manage:integrations",
21
19
  "manage:org",
22
- "trigger:agents",
23
20
  ];
24
21
 
25
22
  function openBrowser(url) {
package/src/mcp.mjs CHANGED
@@ -29,7 +29,7 @@ const plannedTitle = z.object({
29
29
  });
30
30
 
31
31
  export async function startMcpServer() {
32
- const server = new McpServer({ name: "rankcontrol", version: "0.8.2" });
32
+ const server = new McpServer({ name: "rankcontrol", version: "0.9.0" });
33
33
 
34
34
  server.tool(
35
35
  "get_overview_funnel",
@@ -62,6 +62,13 @@ export async function startMcpServer() {
62
62
  run(() => api.visibilityScore())
63
63
  );
64
64
 
65
+ server.tool(
66
+ "get_setup_progress",
67
+ "First-run setup progress for the workspace: five acts from first article live through analytics, each with per-step completion, plus the current act. Use to tell the user what remains to finish setting up.",
68
+ {},
69
+ run(() => api.expedition())
70
+ );
71
+
65
72
  server.tool(
66
73
  "get_citability_report",
67
74
  "Published pages ranked by citability score (how extractable each page is for AI answers), worst first, each with its top open fixes. Use to decide which articles to improve.",
@@ -229,10 +236,13 @@ export async function startMcpServer() {
229
236
 
230
237
  server.tool(
231
238
  "add_competitor",
232
- "Track a competitor (max 10). They enter weekly share-of-voice comparisons against the org's own citation rate.",
239
+ "Track a competitor (max 10). They enter weekly share-of-voice comparisons against the org's own citation rate. Omit websiteUrl to have the site resolved from the name.",
233
240
  {
234
241
  name: z.string().describe("Competitor name"),
235
- websiteUrl: z.string().describe("Competitor website URL"),
242
+ websiteUrl: z
243
+ .string()
244
+ .optional()
245
+ .describe("Competitor website URL (optional; resolved from the name when omitted)"),
236
246
  },
237
247
  run(({ name, websiteUrl }) => api.addCompetitor(name, websiteUrl))
238
248
  );
@@ -432,14 +442,14 @@ export async function startMcpServer() {
432
442
 
433
443
  server.tool(
434
444
  "get_article_settings",
435
- "The workspace article policy: per-article defaults (auto-write, auto-publish, images, title-in-hero, section infographics, Related Reading, YouTube, emojis, link counts, global instructions) plus the flexible-scheduling flag.",
445
+ "The workspace article policy: per-article defaults (auto-write, auto-publish, images, title-in-hero, section infographics, Related Reading, YouTube, emojis, link counts, global instructions) plus the flexible-scheduling flag. News and trending articles always publish manually, regardless of auto-publish.",
436
446
  {},
437
447
  run(() => api.articleSettings())
438
448
  );
439
449
 
440
450
  const articleSettingsShape = {
441
451
  autoPublish: z.boolean().optional(),
442
- autoGenerate: z.boolean().optional().describe("Off = fully manual mode: articles are written only on demand (generate_article) and publish only from the editor"),
452
+ autoGenerate: z.boolean().optional().describe("Off = fully manual mode: articles are written only on demand (generate_article) and publish only from the editor. Turning it on needs a connected publishing destination."),
443
453
  includeInfographics: z.boolean().optional().describe("AI hero + section images"),
444
454
  titleInHeroImage: z.boolean().optional().describe("Write the post title into the hero image"),
445
455
  includeSectionInfographics: z.boolean().optional().describe("Informational panels in 2,000+ word articles"),
@@ -474,12 +484,12 @@ export async function startMcpServer() {
474
484
 
475
485
  server.tool(
476
486
  "reschedule_article",
477
- "Move a PLANNED article to another day (requires the flexible schedule; max 5/day; the exact hour is placed automatically). Pass the target day as epoch ms of local midnight.",
487
+ "Move a PLANNED article to another day (requires the flexible schedule; max 5/day; the exact hour is placed automatically). Pass the target day as YYYY-MM-DD in the workspace's timezone.",
478
488
  {
479
489
  contentId: z.string(),
480
- targetDayStartMs: z.number().describe("Epoch ms of the target day's local midnight"),
490
+ date: z.string().describe("Target day as YYYY-MM-DD in the workspace's timezone"),
481
491
  },
482
- run(({ contentId, targetDayStartMs }) => api.reschedule(contentId, targetDayStartMs))
492
+ run(({ contentId, date }) => api.reschedule(contentId, date))
483
493
  );
484
494
 
485
495
  server.tool(
@@ -572,26 +582,30 @@ export async function startMcpServer() {
572
582
 
573
583
  server.tool(
574
584
  "list_postiz_channels",
575
- "Connected Postiz social channels (id, name, platform). Channel ids are required by push_repurpose_draft.",
585
+ "Connected social channels across Postiz and Buffer (scheduler, id, name, platform). Channel ids are required by push_repurpose_draft.",
576
586
  {},
577
587
  run(() => api.repurposeChannels())
578
588
  );
579
589
 
580
590
  server.tool(
581
591
  "push_repurpose_draft",
582
- "Send a repurpose draft to connected Postiz channels (post now, schedule, or save as a Postiz draft). PUBLICLY VISIBLE side effect: defaults to a DRY RUN; a human should approve before calling again with confirm=true.",
592
+ "Send a repurpose draft to a connected scheduler's channels: Postiz or Buffer (post now, schedule, save as a draft, or add to Buffer's queue). PUBLICLY VISIBLE side effect: defaults to a DRY RUN; a human should approve before calling again with confirm=true.",
583
593
  {
584
594
  draftId: z.string(),
595
+ scheduler: z
596
+ .enum(["postiz", "buffer"])
597
+ .optional()
598
+ .describe("Which scheduler; required when both are connected"),
585
599
  integrationIds: z
586
600
  .array(z.string())
587
601
  .optional()
588
602
  .describe(
589
- "Postiz channel ids from list_postiz_channels (required with confirm)"
603
+ "Channel ids from list_postiz_channels (required with confirm)"
590
604
  ),
591
605
  scheduleType: z
592
- .enum(["now", "schedule", "draft"])
606
+ .enum(["now", "schedule", "draft", "queue"])
593
607
  .optional()
594
- .describe("Default now"),
608
+ .describe("Default now; queue is Buffer only"),
595
609
  date: z.string().optional().describe("ISO time for scheduleType=schedule"),
596
610
  confirm: z.boolean().optional(),
597
611
  },
@@ -655,13 +669,13 @@ export async function startMcpServer() {
655
669
 
656
670
  server.tool(
657
671
  "list_backlinks",
658
- "The workspace's backlink table, newest first.",
672
+ "The workspace's backlink table, newest first. Each row carries its source (index, outreach, managed, network).",
659
673
  {
660
674
  status: z
661
675
  .string()
662
676
  .optional()
663
677
  .describe(
664
- "Filter: discovered, verified, lost, identified, contacted, replied, link_placed, rejected"
678
+ "Filter: discovered, verified, lost, placed, identified, contacted, replied, link_placed, rejected"
665
679
  ),
666
680
  },
667
681
  run(({ status }) => api.backlinks(status))