@rolino/cli 0.6.0 → 0.7.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
@@ -1,9 +1,25 @@
1
1
  # @rolino/cli
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f78e0ff: Prefer advertised Streamable HTTP MCP with client-owned OAuth, add explicit HTTP and STDIO transport selection, and keep local MCP as a fallback.
8
+ - 358f37a: Expose additive safe Blog preparation, next-action, and quality summaries through the shared read contracts and existing clients.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [358f37a]
13
+ - @rolino/contracts@0.7.0
14
+ - @rolino/sdk@0.7.0
15
+ - @rolino/local-auth@0.7.0
16
+
3
17
  ## 0.6.0
4
18
 
5
19
  ### Minor Changes
6
20
 
21
+ - Add matching backlink prospecting commands with safe JSON output and formula-safe contact CSV export. Rolino does not send email.
22
+
7
23
  - d162f85: Add safe Blog editorial cadence preview and apply operations across contracts, SDK, CLI, and MCP.
8
24
  - 50cb9fc: Add Google Business Profile as a disabled-by-default standard text and single-image publishing destination across shared public clients.
9
25
  - 1ac7ed5: Add signed custom Blog webhook setup, connection testing, one-time secret rotation, and safe idempotent replay parity.
@@ -93,6 +109,10 @@
93
109
 
94
110
  ### Minor Changes
95
111
 
112
+ - Show additive safe Blog preparation and quality state in existing JSON reads.
113
+
114
+ ### Minor Changes
115
+
96
116
  - Add Blog image generation, exact local-file upload, image review, confirmed
97
117
  exact-bundle approval, future scheduling, and schedule cancellation commands
98
118
  with deterministic agent output, stable idempotency, safe errors, and
package/README.md CHANGED
@@ -14,16 +14,17 @@ The supported installation keeps the CLI independent from MCP:
14
14
  npm install --global @rolino/cli
15
15
  ```
16
16
 
17
- Users who also want local MCP will install both packages explicitly:
17
+ Users who need local STDIO MCP will install both packages explicitly:
18
18
 
19
19
  ```bash
20
20
  npm install --global @rolino/cli @rolino/mcp
21
21
  ```
22
22
 
23
- The CLI-only installation does not include `@rolino/mcp` or the MCP SDK. If a
24
- CLI-only user runs `rolino setup mcp`, the command explains how to install the
25
- separate MCP package and then retry. Rolino never downloads or installs MCP
26
- silently.
23
+ The default `rolino setup mcp` mode checks the selected Rolino instance. It
24
+ uses hosted Streamable HTTP when the instance advertises it. This mode does
25
+ not need `@rolino/mcp`. If remote MCP is not available, the command uses local
26
+ STDIO and explains how to install the separate MCP package. Rolino never
27
+ downloads or installs MCP silently.
27
28
 
28
29
  Initial commands:
29
30
 
@@ -31,12 +32,24 @@ Initial commands:
31
32
  rolino auth login
32
33
  rolino auth status
33
34
  rolino auth logout
34
- rolino setup mcp --client codex [--scope user|project]
35
- rolino setup mcp --client claude-code [--scope user|project]
35
+ rolino setup mcp --client codex [--transport auto|http|stdio] [--scope user|project]
36
+ rolino setup mcp --client claude-code [--transport auto|http|stdio] [--scope user|project]
36
37
  rolino whoami
37
38
  rolino projects create --name NAME [--type PRODUCT|SERVICE|PERSONAL|COMPANY] [--website URL] [--description TEXT] [--idempotency-key KEY] --yes
38
39
  rolino projects list [--limit 50] [--cursor ID]
39
40
  rolino projects show PROJECT_ID
41
+ rolino backlinks targets list --project PROJECT_ID
42
+ rolino backlinks targets add --project PROJECT_ID --url URL --label LABEL
43
+ rolino backlinks discover --project PROJECT_ID --limit 20
44
+ rolino backlinks runs get --project PROJECT_ID --run RUN_ID
45
+ rolino backlinks prospects list --project PROJECT_ID [--stage STAGE]
46
+ rolino backlinks prospects get --project PROJECT_ID --prospect PROSPECT_ID
47
+ rolino backlinks prospects approve --project PROJECT_ID --prospect PROSPECT_ID --expected-version VERSION
48
+ rolino backlinks contacts research --project PROJECT_ID --prospect PROSPECT_ID
49
+ rolino backlinks contacts list --project PROJECT_ID
50
+ rolino backlinks contacts export --project PROJECT_ID --format csv
51
+ rolino backlinks outreach update --project PROJECT_ID --prospect PROSPECT_ID --file DRAFT.json --expected-version VERSION
52
+ rolino backlinks verify --project PROJECT_ID --prospect PROSPECT_ID --url PUBLIC_URL --expected-version VERSION
40
53
  rolino media list --project PROJECT_ID [--type IMAGE|VIDEO] [--query TEXT] [--limit 50] [--cursor ID]
41
54
  rolino media upload FILE_PATH --project PROJECT_ID --yes
42
55
  rolino posts create --project PROJECT_ID --caption TEXT [--platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY|LINKEDIN|GOOGLE_BUSINESS_PROFILE] [--media ASSET_ID] --yes
@@ -70,10 +83,10 @@ rolino blog schedule cancel execute ARTICLE_ID --project PROJECT_ID --confirmati
70
83
  rolino doctor
71
84
  ```
72
85
 
73
- Interactive login uses standard OAuth Authorization Code with PKCE S256. It
74
- opens the configured Rolino instance, requests the exact \`/api/v1\` resource,
75
- uses the fixed \`http://127.0.0.1:48391/oauth/callback\` redirect, and obtains
76
- explicit workspace approval.
86
+ Interactive login uses standard OAuth Authorization Code with PKCE S256. It
87
+ opens the configured Rolino instance, requests the exact \`/api/v1\` resource,
88
+ uses the fixed \`http://127.0.0.1:48391/oauth/callback\` redirect, and obtains
89
+ explicit workspace approval.
77
90
  The approval screen always includes identity and workspace reads and lets the
78
91
  user independently opt into project creation, draft writing, confirmed
79
92
  scheduling, confirmed immediate publishing, and read-only SEO Intelligence.
@@ -89,20 +102,25 @@ with evidence. It cannot create a final draft in the same action.
89
102
  Optional permissions start off,
90
103
  and existing credentials never gain a newly selected permission. Revoke and
91
104
  reauthorize an existing connection to grant project creation or `seo:read`.
92
- Existing grants never receive `seo:read` automatically. OAuth tokens never
93
- pass through a browser URL. The CLI stores a short-lived access token and a
94
- rotating refresh token by host, issuer, client, and resource in the
95
- operating-system configuration directory. It does not read or migrate the old
96
- interactive credential, so the first use after this cutover requires sign-in;
105
+ Existing grants never receive `seo:read` automatically. OAuth tokens never
106
+ pass through a browser URL. The CLI stores a short-lived access token and a
107
+ rotating refresh token by host, issuer, client, and resource in the
108
+ operating-system configuration directory. It does not read or migrate the old
109
+ interactive credential, so the first use after this cutover requires sign-in;
97
110
  `ROLINO_CONFIG_DIR` overrides that location. `auth logout` revokes the calling
98
- workspace grant and OAuth refresh token before deleting its local copy.
111
+ workspace grant and OAuth refresh token before deleting its local copy.
99
112
 
100
113
  The terminal waits five minutes for browser approval. If that request expires,
101
114
  close the old approval page and run `rolino auth login` again; refreshing the
102
115
  page cannot restart the closed loopback listener.
103
116
 
104
- `setup mcp` finds the built Rolino stdio server, previews the exact token-free
105
- configuration, and asks before changing anything. User scope is the default.
117
+ `setup mcp` checks `/api/v1/meta` and prefers hosted Streamable HTTP when it is
118
+ available. Remote setup writes the `/mcp` URL and OAuth mode, does not start a
119
+ local MCP process, and does not reuse `rolino auth login`. Run
120
+ `codex mcp login rolino` after remote Codex setup. Use `--transport http` to
121
+ require remote MCP or `--transport stdio` to require the local package and its
122
+ shared CLI OAuth session. The command previews the exact token-free
123
+ configuration and asks before changing anything. User scope is the default.
106
124
  Codex configuration is written to `CODEX_HOME/config.toml` or
107
125
  `~/.codex/config.toml`; Claude Code user scope is applied through its official
108
126
  CLI. Project scope writes `.codex/config.toml` or `.mcp.json` in the current
package/dist/bin.cjs CHANGED
@@ -34,7 +34,7 @@ var import_commander = require("commander");
34
34
  // package.json
35
35
  var package_default = {
36
36
  name: "@rolino/cli",
37
- version: "0.6.0",
37
+ version: "0.7.0",
38
38
  description: "Agent-friendly command-line interface for Rolino",
39
39
  type: "module",
40
40
  license: "MIT",
@@ -91,9 +91,9 @@ var package_default = {
91
91
  dev: "tsx src/bin.ts"
92
92
  },
93
93
  dependencies: {
94
- "@rolino/contracts": "0.6.0",
95
- "@rolino/local-auth": "0.6.0",
96
- "@rolino/sdk": "0.6.0",
94
+ "@rolino/contracts": "0.7.0",
95
+ "@rolino/local-auth": "0.7.0",
96
+ "@rolino/sdk": "0.7.0",
97
97
  commander: "^15.0.0",
98
98
  open: "^11.0.0"
99
99
  },
@@ -649,12 +649,18 @@ function removeExistingCodexEntry(source, newline) {
649
649
  return kept.join(newline).trimEnd();
650
650
  }
651
651
  function codexBlock(server, newline) {
652
+ const configuration = server.transport === "http" ? [
653
+ `url = ${tomlString(server.url)}`,
654
+ `auth = ${tomlString(server.auth)}`
655
+ ] : [
656
+ `command = ${tomlString(server.command)}`,
657
+ `args = [${server.args.map(tomlString).join(", ")}]`,
658
+ `env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`
659
+ ];
652
660
  return [
653
661
  CODEX_BEGIN,
654
662
  "[mcp_servers.rolino]",
655
- `command = ${tomlString(server.command)}`,
656
- `args = [${server.args.map(tomlString).join(", ")}]`,
657
- `env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`,
663
+ ...configuration,
658
664
  CODEX_END
659
665
  ].join(newline);
660
666
  }
@@ -694,7 +700,12 @@ function updateClaudeProjectConfig(source, server) {
694
700
  ...parsed,
695
701
  mcpServers: {
696
702
  ...currentServers,
697
- rolino: { type: "stdio", ...server }
703
+ rolino: server.transport === "http" ? { type: "http", url: server.url } : {
704
+ type: "stdio",
705
+ command: server.command,
706
+ args: server.args,
707
+ env: server.env
708
+ }
698
709
  }
699
710
  }, null, 2)}
700
711
  `;
@@ -726,6 +737,7 @@ async function setupCodex(options, server) {
726
737
  backupPath,
727
738
  changed,
728
739
  dryRun: options.dryRun ?? false,
740
+ transport: server.transport,
729
741
  server
730
742
  };
731
743
  }
@@ -746,6 +758,7 @@ async function setupClaudeCode(options, server) {
746
758
  backupPath,
747
759
  changed,
748
760
  dryRun: options.dryRun ?? false,
761
+ transport: server.transport,
749
762
  server
750
763
  };
751
764
  }
@@ -758,6 +771,7 @@ async function setupClaudeCode(options, server) {
758
771
  backupPath: null,
759
772
  changed: true,
760
773
  dryRun: true,
774
+ transport: server.transport,
761
775
  server
762
776
  };
763
777
  }
@@ -771,7 +785,12 @@ async function setupClaudeCode(options, server) {
771
785
  "mcp",
772
786
  "add-json",
773
787
  "rolino",
774
- JSON.stringify({ type: "stdio", ...server }),
788
+ JSON.stringify(server.transport === "http" ? { type: "http", url: server.url } : {
789
+ type: "stdio",
790
+ command: server.command,
791
+ args: server.args,
792
+ env: server.env
793
+ }),
775
794
  "--scope",
776
795
  "user"
777
796
  ], options);
@@ -787,14 +806,51 @@ async function setupClaudeCode(options, server) {
787
806
  backupPath: null,
788
807
  changed: true,
789
808
  dryRun: false,
809
+ transport: server.transport,
790
810
  server
791
811
  };
792
812
  }
813
+ function remoteMcpUrl(baseUrl) {
814
+ return new URL("mcp", `${baseUrl.replace(/\/$/, "")}/`).toString();
815
+ }
816
+ async function advertisedRemoteMcp(options) {
817
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
818
+ try {
819
+ const response = await fetchImplementation(
820
+ new URL("api/v1/meta", `${options.baseUrl.replace(/\/$/, "")}/`),
821
+ {
822
+ headers: { accept: "application/json" },
823
+ signal: AbortSignal.timeout(5e3)
824
+ }
825
+ );
826
+ if (!response.ok) return false;
827
+ const payload = await response.json();
828
+ return payload.data?.mcp?.streamableHttp === true;
829
+ } catch {
830
+ return false;
831
+ }
832
+ }
833
+ async function resolveTransport(options) {
834
+ const requested = options.transport ?? "stdio";
835
+ if (requested === "stdio") return "stdio";
836
+ if (await advertisedRemoteMcp(options)) return "http";
837
+ if (requested === "http") {
838
+ throw new TypeError(
839
+ "This Rolino instance does not advertise Streamable HTTP MCP. Use --transport stdio, or enable and verify remote MCP on the server."
840
+ );
841
+ }
842
+ return "stdio";
843
+ }
793
844
  async function setupMcp(options) {
794
- const serverPath = await resolveServerPath(options);
795
- const server = {
845
+ const transport = await resolveTransport(options);
846
+ const server = transport === "http" ? {
847
+ transport: "http",
848
+ url: remoteMcpUrl(options.baseUrl),
849
+ auth: "oauth"
850
+ } : {
851
+ transport: "stdio",
796
852
  command: options.nodePath,
797
- args: [serverPath],
853
+ args: [await resolveServerPath(options)],
798
854
  env: { ROLINO_URL: options.baseUrl }
799
855
  };
800
856
  return options.client === "codex" ? setupCodex(options, server) : setupClaudeCode(options, server);
@@ -935,6 +991,10 @@ function mcpScope(value) {
935
991
  if (value === "user" || value === "project") return value;
936
992
  throw new import_commander.InvalidArgumentError("MCP setup scope must be user or project.");
937
993
  }
994
+ function mcpTransport(value) {
995
+ if (value === "auto" || value === "http" || value === "stdio") return value;
996
+ throw new import_commander.InvalidArgumentError("MCP transport must be auto, http, or stdio.");
997
+ }
938
998
  function isoDateTime(value) {
939
999
  const date = new Date(value);
940
1000
  if (Number.isNaN(date.getTime())) {
@@ -995,13 +1055,21 @@ function requireBlogExecutionConsent(options) {
995
1055
  throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
996
1056
  }
997
1057
  function formatMcpSetupPreview(result) {
1058
+ const connection = result.server.transport === "http" ? [
1059
+ `Transport: Streamable HTTP`,
1060
+ `Endpoint: ${result.server.url}`,
1061
+ "Authentication: OAuth in the MCP client"
1062
+ ] : [
1063
+ "Transport: local STDIO",
1064
+ `Command: ${result.server.command}`,
1065
+ `Arguments: ${result.server.args.join(" ")}`,
1066
+ `Rolino URL: ${result.server.env.ROLINO_URL}`
1067
+ ];
998
1068
  return [
999
1069
  `Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
1000
1070
  `Scope: ${result.scope}`,
1001
1071
  `Target: ${result.target}`,
1002
- `Command: ${result.server.command}`,
1003
- `Arguments: ${result.server.args.join(" ")}`,
1004
- `Rolino URL: ${result.server.env.ROLINO_URL}`,
1072
+ ...connection,
1005
1073
  "No token will be written to MCP configuration."
1006
1074
  ].join("\n");
1007
1075
  }
@@ -1464,7 +1532,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1464
1532
  });
1465
1533
  });
1466
1534
  const setup = program.command("setup").description("Configure local agent tools for Rolino");
1467
- setup.command("mcp").description("Configure the Rolino stdio MCP server for a supported client").requiredOption("--client <client>", "codex or claude-code", mcpClient).option("--scope <scope>", "user or project", mcpScope, "user").option("--server-path <path>", "absolute or working-directory-relative MCP server path").option("--dry-run", "show the intended configuration without writing it").option("--yes", "apply without an interactive confirmation").option("--force", "replace an existing Claude Code user-scoped Rolino server").action(async (local) => {
1535
+ setup.command("mcp").description("Configure Rolino MCP with Streamable HTTP or local STDIO").requiredOption("--client <client>", "codex or claude-code", mcpClient).option("--scope <scope>", "user or project", mcpScope, "user").option("--transport <transport>", "auto, http, or stdio", mcpTransport, "auto").option("--server-path <path>", "absolute or working-directory-relative MCP server path").option("--dry-run", "show the intended configuration without writing it").option("--yes", "apply without an interactive confirmation").option("--force", "replace an existing Claude Code user-scoped Rolino server").action(async (local) => {
1468
1536
  const global = program.opts();
1469
1537
  commandExitCode = await execute({
1470
1538
  command: "setup mcp",
@@ -1477,7 +1545,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1477
1545
  cwd: runtime.cwd,
1478
1546
  env: runtime.env,
1479
1547
  nodePath: runtime.nodePath,
1480
- cliEntryPath: runtime.cliEntryPath
1548
+ cliEntryPath: runtime.cliEntryPath,
1549
+ fetch: runtime.fetch
1481
1550
  };
1482
1551
  const preview = await setupMcp({ ...setupOptions, dryRun: true });
1483
1552
  let result = preview;
@@ -1504,11 +1573,12 @@ async function runCli(argv = process.argv, overrides = {}) {
1504
1573
  `${state} Rolino MCP for ${clientLabel}.`,
1505
1574
  `Scope: ${result.scope}`,
1506
1575
  `Target: ${result.target}`,
1576
+ `Transport: ${result.transport === "http" ? "Streamable HTTP" : "local STDIO"}`,
1507
1577
  ...result.backupPath && !result.dryRun ? [`Backup: ${result.backupPath}`] : [],
1508
1578
  `Rolino URL: ${client.baseUrl}`,
1509
1579
  "No token was written to MCP configuration."
1510
1580
  ].join("\n"),
1511
- result.client === "codex" ? ["codex mcp list", "rolino auth login"] : ["claude mcp get rolino", "rolino auth login"]
1581
+ result.client === "codex" ? result.transport === "http" ? ["codex mcp login rolino", "codex mcp list"] : ["rolino auth login", "codex mcp list"] : result.transport === "http" ? ["claude mcp get rolino", "Complete OAuth when Claude prompts you"] : ["rolino auth login", "claude mcp get rolino"]
1512
1582
  );
1513
1583
  }
1514
1584
  });
@@ -2247,6 +2317,106 @@ Revision: ${local.revision}` });
2247
2317
  writeSuccess(context, data, JSON.stringify(data, null, 2));
2248
2318
  } });
2249
2319
  });
2320
+ const backlinks = program.command("backlinks").description("Review backlink prospects and public contact drafts. Rolino never sends email.");
2321
+ const backlinkTargets = backlinks.command("targets");
2322
+ backlinkTargets.command("list").requiredOption("--project <project-id>").action(async (local) => {
2323
+ const global = program.opts();
2324
+ commandExitCode = await execute({ command: "backlinks targets list", global, runtime, async action(context, client) {
2325
+ const data = await client.backlinks.targets.list(local.project, { requestId: context.requestId });
2326
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2327
+ } });
2328
+ });
2329
+ backlinkTargets.command("add").requiredOption("--project <project-id>").requiredOption("--url <url>").requiredOption("--label <label>").action(async (local) => {
2330
+ const global = program.opts();
2331
+ commandExitCode = await execute({ command: "backlinks targets add", global, runtime, async action(context, client) {
2332
+ const data = await client.backlinks.targets.add(local.project, { url: local.url, label: local.label }, context.requestId, { requestId: context.requestId });
2333
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2334
+ } });
2335
+ });
2336
+ backlinks.command("discover").requiredOption("--project <project-id>").option("--limit <number>", "maximum saved prospects", Number, 20).option("--idempotency-key <key>").action(async (local) => {
2337
+ const global = program.opts();
2338
+ commandExitCode = await execute({ command: "backlinks discover", global, runtime, async action(context, client) {
2339
+ const data = await client.backlinks.discoveries.start(local.project, { limit: local.limit }, local.idempotencyKey ?? context.requestId, { requestId: context.requestId });
2340
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2341
+ } });
2342
+ });
2343
+ const backlinkRuns = backlinks.command("runs");
2344
+ backlinkRuns.command("get").requiredOption("--project <project-id>").requiredOption("--run <run-id>").action(async (local) => {
2345
+ const global = program.opts();
2346
+ commandExitCode = await execute({ command: "backlinks runs get", global, runtime, async action(context, client) {
2347
+ const data = await client.backlinks.discoveries.get(local.project, local.run, { requestId: context.requestId });
2348
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2349
+ } });
2350
+ });
2351
+ const backlinkProspects = backlinks.command("prospects");
2352
+ backlinkProspects.command("list").requiredOption("--project <project-id>").option("--stage <stage>").action(async (local) => {
2353
+ const global = program.opts();
2354
+ commandExitCode = await execute({ command: "backlinks prospects list", global, runtime, async action(context, client) {
2355
+ const stage = local.stage ? import_contracts.BacklinkProspectStageSchema.parse(local.stage.toUpperCase()) : void 0;
2356
+ const data = await client.backlinks.prospects.list(local.project, { stage }, { requestId: context.requestId });
2357
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2358
+ } });
2359
+ });
2360
+ backlinkProspects.command("get").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
2361
+ const global = program.opts();
2362
+ commandExitCode = await execute({ command: "backlinks prospects get", global, runtime, async action(context, client) {
2363
+ const data = await client.backlinks.prospects.get(local.project, local.prospect, { requestId: context.requestId });
2364
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2365
+ } });
2366
+ });
2367
+ backlinkProspects.command("approve").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--expected-version <number>", "current optimistic version", Number).action(async (local) => {
2368
+ const global = program.opts();
2369
+ commandExitCode = await execute({ command: "backlinks prospects approve", global, runtime, async action(context, client) {
2370
+ const data = await client.backlinks.prospects.updateStage(local.project, local.prospect, { stage: "APPROVED", expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2371
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2372
+ } });
2373
+ });
2374
+ const backlinkContacts = backlinks.command("contacts");
2375
+ backlinkContacts.command("research").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
2376
+ const global = program.opts();
2377
+ commandExitCode = await execute({ command: "backlinks contacts research", global, runtime, async action(context, client) {
2378
+ const data = await client.backlinks.prospects.researchContact(local.project, local.prospect, context.requestId, { requestId: context.requestId });
2379
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2380
+ } });
2381
+ });
2382
+ backlinkContacts.command("list").requiredOption("--project <project-id>").action(async (local) => {
2383
+ const global = program.opts();
2384
+ commandExitCode = await execute({ command: "backlinks contacts list", global, runtime, async action(context, client) {
2385
+ const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
2386
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2387
+ } });
2388
+ });
2389
+ backlinkContacts.command("export").requiredOption("--project <project-id>").option("--format <format>", "json or csv", "csv").action(async (local) => {
2390
+ const global = program.opts();
2391
+ commandExitCode = await execute({ command: "backlinks contacts export", global, runtime, async action(context, client) {
2392
+ const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
2393
+ if (local.format !== "csv") return writeSuccess(context, data, JSON.stringify(data, null, 2));
2394
+ const cell = (value) => {
2395
+ let text = String(value ?? "").replace(/[\r\n]+/g, " ");
2396
+ if (/^[=+\-@\t]/.test(text)) text = `'${text}`;
2397
+ return `"${text.replaceAll('"', '""')}"`;
2398
+ };
2399
+ const csv = ["prospectId,name,role,email,sourceUrl,checkedAt", ...data.items.map((item) => [item.prospectId, item.name, item.role, item.email, item.sourceUrl, item.checkedAt].map(cell).join(","))].join("\n");
2400
+ context.stdout.write(`${csv}
2401
+ `);
2402
+ } });
2403
+ });
2404
+ const backlinkOutreach = backlinks.command("outreach");
2405
+ backlinkOutreach.command("update").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--file <file>").requiredOption("--expected-version <number>", "current draft version", Number).action(async (local) => {
2406
+ const global = program.opts();
2407
+ commandExitCode = await execute({ command: "backlinks outreach update", global, runtime, async action(context, client) {
2408
+ const payload = JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.file), "utf8"));
2409
+ const data = await client.backlinks.prospects.updateOutreach(local.project, local.prospect, { ...payload, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2410
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2411
+ } });
2412
+ });
2413
+ backlinks.command("verify").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--url <url>").requiredOption("--expected-version <number>", "current prospect version", Number).action(async (local) => {
2414
+ const global = program.opts();
2415
+ commandExitCode = await execute({ command: "backlinks verify", global, runtime, async action(context, client) {
2416
+ const data = await client.backlinks.prospects.verify(local.project, local.prospect, { url: local.url, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2417
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2418
+ } });
2419
+ });
2250
2420
  const seo = program.command("seo").description("Read authorized SEO opportunities and weekly reports");
2251
2421
  const seoOpportunities = seo.command("opportunities").description("Read accepted SEO opportunities");
2252
2422
  seoOpportunities.command("list").description("List bounded SEO opportunities").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum opportunities to return", (value) => {