@rolino/cli 0.6.0 → 0.7.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/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ var import_commander = require("commander");
50
50
  // package.json
51
51
  var package_default = {
52
52
  name: "@rolino/cli",
53
- version: "0.6.0",
53
+ version: "0.7.1",
54
54
  description: "Agent-friendly command-line interface for Rolino",
55
55
  type: "module",
56
56
  license: "MIT",
@@ -107,9 +107,9 @@ var package_default = {
107
107
  dev: "tsx src/bin.ts"
108
108
  },
109
109
  dependencies: {
110
- "@rolino/contracts": "0.6.0",
111
- "@rolino/local-auth": "0.6.0",
112
- "@rolino/sdk": "0.6.0",
110
+ "@rolino/contracts": "0.7.1",
111
+ "@rolino/local-auth": "0.7.1",
112
+ "@rolino/sdk": "0.7.1",
113
113
  commander: "^15.0.0",
114
114
  open: "^11.0.0"
115
115
  },
@@ -122,24 +122,21 @@ var package_default = {
122
122
  };
123
123
 
124
124
  // src/cli.ts
125
- var import_contracts = require("@rolino/contracts");
125
+ var import_contracts2 = require("@rolino/contracts");
126
126
  var import_sdk = require("@rolino/sdk");
127
127
 
128
128
  // src/browser-login.ts
129
129
  var import_node_crypto = require("crypto");
130
130
  var import_node_http = require("http");
131
+ var import_contracts = require("@rolino/contracts");
131
132
  var import_local_auth = require("@rolino/local-auth");
132
133
  var import_open = __toESM(require("open"), 1);
133
134
  var OAUTH_CALLBACK_PORT = 48391;
134
135
  var OAUTH_CALLBACK_PATH = "/oauth/callback";
135
136
  var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
136
- var DEFAULT_SCOPES = [
137
+ var AUTHORIZATION_REQUEST_SCOPES = [
137
138
  "offline_access",
138
- "identity:read",
139
- "projects:read",
140
- "posts:read",
141
- "integrations:read",
142
- "calendar:read"
139
+ ...import_contracts.AGENT_CAPABILITIES
143
140
  ];
144
141
  function escapeHtml(value) {
145
142
  return value.replace(/[&<>"']/g, (character) => ({
@@ -179,7 +176,7 @@ function createBrowserAuthorizationRequest(baseUrl) {
179
176
  url.searchParams.set("response_type", "code");
180
177
  url.searchParams.set("client_id", configuration.clientId);
181
178
  url.searchParams.set("redirect_uri", redirectUri);
182
- url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
179
+ url.searchParams.set("scope", AUTHORIZATION_REQUEST_SCOPES.join(" "));
183
180
  url.searchParams.set("resource", configuration.resource);
184
181
  url.searchParams.set("state", state);
185
182
  url.searchParams.set("code_challenge", codeChallenge);
@@ -218,7 +215,7 @@ async function exchangeAuthorizationCode(options) {
218
215
  accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
219
216
  refreshToken: payload.refresh_token,
220
217
  refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
221
- scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...DEFAULT_SCOPES]
218
+ scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...AUTHORIZATION_REQUEST_SCOPES]
222
219
  };
223
220
  }
224
221
  async function loginWithBrowser(options) {
@@ -665,12 +662,18 @@ function removeExistingCodexEntry(source, newline) {
665
662
  return kept.join(newline).trimEnd();
666
663
  }
667
664
  function codexBlock(server, newline) {
665
+ const configuration = server.transport === "http" ? [
666
+ `url = ${tomlString(server.url)}`,
667
+ `auth = ${tomlString(server.auth)}`
668
+ ] : [
669
+ `command = ${tomlString(server.command)}`,
670
+ `args = [${server.args.map(tomlString).join(", ")}]`,
671
+ `env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`
672
+ ];
668
673
  return [
669
674
  CODEX_BEGIN,
670
675
  "[mcp_servers.rolino]",
671
- `command = ${tomlString(server.command)}`,
672
- `args = [${server.args.map(tomlString).join(", ")}]`,
673
- `env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`,
676
+ ...configuration,
674
677
  CODEX_END
675
678
  ].join(newline);
676
679
  }
@@ -679,13 +682,12 @@ function updateCodexConfig(source, server) {
679
682
  const block = codexBlock(server, newline);
680
683
  const begin = source.indexOf(CODEX_BEGIN);
681
684
  const end = source.indexOf(CODEX_END);
682
- if (begin === -1 !== (end === -1) || begin !== -1 && end < begin) {
683
- throw new TypeError("Codex config contains an incomplete Rolino-managed block.");
684
- }
685
685
  let unmanagedSource = source;
686
- if (begin !== -1) {
686
+ if (begin !== -1 && end !== -1 && end > begin) {
687
687
  const after = end + CODEX_END.length;
688
688
  unmanagedSource = `${source.slice(0, begin)}${source.slice(after)}`;
689
+ } else if (begin !== -1 || end !== -1) {
690
+ unmanagedSource = source.split(/\r?\n/).filter((line) => line.trim() !== CODEX_BEGIN && line.trim() !== CODEX_END).join(newline);
689
691
  }
690
692
  const withoutRolino = removeExistingCodexEntry(unmanagedSource, newline);
691
693
  return `${withoutRolino ? `${withoutRolino}${newline}${newline}` : ""}${block}${newline}`;
@@ -710,7 +712,12 @@ function updateClaudeProjectConfig(source, server) {
710
712
  ...parsed,
711
713
  mcpServers: {
712
714
  ...currentServers,
713
- rolino: { type: "stdio", ...server }
715
+ rolino: server.transport === "http" ? { type: "http", url: server.url } : {
716
+ type: "stdio",
717
+ command: server.command,
718
+ args: server.args,
719
+ env: server.env
720
+ }
714
721
  }
715
722
  }, null, 2)}
716
723
  `;
@@ -742,6 +749,7 @@ async function setupCodex(options, server) {
742
749
  backupPath,
743
750
  changed,
744
751
  dryRun: options.dryRun ?? false,
752
+ transport: server.transport,
745
753
  server
746
754
  };
747
755
  }
@@ -762,6 +770,7 @@ async function setupClaudeCode(options, server) {
762
770
  backupPath,
763
771
  changed,
764
772
  dryRun: options.dryRun ?? false,
773
+ transport: server.transport,
765
774
  server
766
775
  };
767
776
  }
@@ -774,6 +783,7 @@ async function setupClaudeCode(options, server) {
774
783
  backupPath: null,
775
784
  changed: true,
776
785
  dryRun: true,
786
+ transport: server.transport,
777
787
  server
778
788
  };
779
789
  }
@@ -787,7 +797,12 @@ async function setupClaudeCode(options, server) {
787
797
  "mcp",
788
798
  "add-json",
789
799
  "rolino",
790
- JSON.stringify({ type: "stdio", ...server }),
800
+ JSON.stringify(server.transport === "http" ? { type: "http", url: server.url } : {
801
+ type: "stdio",
802
+ command: server.command,
803
+ args: server.args,
804
+ env: server.env
805
+ }),
791
806
  "--scope",
792
807
  "user"
793
808
  ], options);
@@ -803,14 +818,51 @@ async function setupClaudeCode(options, server) {
803
818
  backupPath: null,
804
819
  changed: true,
805
820
  dryRun: false,
821
+ transport: server.transport,
806
822
  server
807
823
  };
808
824
  }
825
+ function remoteMcpUrl(baseUrl) {
826
+ return new URL("mcp", `${baseUrl.replace(/\/$/, "")}/`).toString();
827
+ }
828
+ async function advertisedRemoteMcp(options) {
829
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
830
+ try {
831
+ const response = await fetchImplementation(
832
+ new URL("api/v1/meta", `${options.baseUrl.replace(/\/$/, "")}/`),
833
+ {
834
+ headers: { accept: "application/json" },
835
+ signal: AbortSignal.timeout(5e3)
836
+ }
837
+ );
838
+ if (!response.ok) return false;
839
+ const payload = await response.json();
840
+ return payload.data?.mcp?.streamableHttp === true;
841
+ } catch {
842
+ return false;
843
+ }
844
+ }
845
+ async function resolveTransport(options) {
846
+ const requested = options.transport ?? "stdio";
847
+ if (requested === "stdio") return "stdio";
848
+ if (await advertisedRemoteMcp(options)) return "http";
849
+ if (requested === "http") {
850
+ throw new TypeError(
851
+ "This Rolino instance does not advertise Streamable HTTP MCP. Use --transport stdio, or enable and verify remote MCP on the server."
852
+ );
853
+ }
854
+ return "stdio";
855
+ }
809
856
  async function setupMcp(options) {
810
- const serverPath = await resolveServerPath(options);
811
- const server = {
857
+ const transport = await resolveTransport(options);
858
+ const server = transport === "http" ? {
859
+ transport: "http",
860
+ url: remoteMcpUrl(options.baseUrl),
861
+ auth: "oauth"
862
+ } : {
863
+ transport: "stdio",
812
864
  command: options.nodePath,
813
- args: [serverPath],
865
+ args: [await resolveServerPath(options)],
814
866
  env: { ROLINO_URL: options.baseUrl }
815
867
  };
816
868
  return options.client === "codex" ? setupCodex(options, server) : setupClaudeCode(options, server);
@@ -841,40 +893,40 @@ function outputFormat(value) {
841
893
  throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
842
894
  }
843
895
  function postStatus(value) {
844
- const parsed = import_contracts.PostStatusSchema.safeParse(value.toUpperCase());
896
+ const parsed = import_contracts2.PostStatusSchema.safeParse(value.toUpperCase());
845
897
  if (parsed.success) return parsed.data;
846
898
  throw new import_commander.InvalidArgumentError(
847
- `Status must be one of ${import_contracts.PostStatusSchema.options.join(", ")}.`
899
+ `Status must be one of ${import_contracts2.PostStatusSchema.options.join(", ")}.`
848
900
  );
849
901
  }
850
902
  function seoOpportunityKind(value) {
851
- const parsed = import_contracts.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
903
+ const parsed = import_contracts2.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
852
904
  if (parsed.success) return parsed.data;
853
- throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts.SeoOpportunityKindSchema.options.join(", ")}.`);
905
+ throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts2.SeoOpportunityKindSchema.options.join(", ")}.`);
854
906
  }
855
907
  function seoExpectedImpact(value) {
856
- const parsed = import_contracts.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
908
+ const parsed = import_contracts2.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
857
909
  if (parsed.success) return parsed.data;
858
910
  throw new import_commander.InvalidArgumentError("SEO impact must be HIGH, MEDIUM, or LOW.");
859
911
  }
860
912
  function seoReportCompleteness(value) {
861
- const parsed = import_contracts.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
913
+ const parsed = import_contracts2.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
862
914
  if (parsed.success) return parsed.data;
863
915
  throw new import_commander.InvalidArgumentError("Report status must be COMPLETE or PARTIAL.");
864
916
  }
865
917
  function projectType(value) {
866
- const parsed = import_contracts.ProjectTypeSchema.safeParse(value.toUpperCase());
918
+ const parsed = import_contracts2.ProjectTypeSchema.safeParse(value.toUpperCase());
867
919
  if (parsed.success) return parsed.data;
868
920
  throw new import_commander.InvalidArgumentError(
869
- `Type must be one of ${import_contracts.ProjectTypeSchema.options.join(", ")}.`
921
+ `Type must be one of ${import_contracts2.ProjectTypeSchema.options.join(", ")}.`
870
922
  );
871
923
  }
872
924
  function publishingProvider(value) {
873
925
  const normalized = value.toUpperCase();
874
- const parsed = import_contracts.PublishingProviderSchema.safeParse(normalized);
926
+ const parsed = import_contracts2.PublishingProviderSchema.safeParse(normalized);
875
927
  if (parsed.success) return parsed.data;
876
928
  throw new import_commander.InvalidArgumentError(
877
- `Platform must be one of ${import_contracts.PublishingProviderSchema.options.join(", ")}.`
929
+ `Platform must be one of ${import_contracts2.PublishingProviderSchema.options.join(", ")}.`
878
930
  );
879
931
  }
880
932
  function collectPublishingProvider(value, previous) {
@@ -885,10 +937,10 @@ function collectString(value, previous) {
885
937
  }
886
938
  function deliveryOptionsProvider(value) {
887
939
  const normalized = value.toUpperCase();
888
- const parsed = import_contracts.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
940
+ const parsed = import_contracts2.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
889
941
  if (parsed.success) return parsed.data;
890
942
  throw new import_commander.InvalidArgumentError(
891
- `Provider must be one of ${import_contracts.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
943
+ `Provider must be one of ${import_contracts2.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
892
944
  );
893
945
  }
894
946
  function tiktokPostMode(value) {
@@ -926,7 +978,7 @@ function yesOrNo(value) {
926
978
  function youtubeSettings(options) {
927
979
  const hasYouTubeOptions = options.youtubeTitle !== void 0 || options.youtubeCategoryId !== void 0 || options.youtubePrivacy !== void 0 || options.youtubeMadeForKids !== void 0 || options.youtubeSyntheticMedia === true || options.youtubeNotifySubscribers === false || (options.youtubeTag?.length ?? 0) > 0;
928
980
  if (!hasYouTubeOptions) return null;
929
- return import_contracts.YouTubePostSettingsSchema.parse({
981
+ return import_contracts2.YouTubePostSettingsSchema.parse({
930
982
  title: options.youtubeTitle,
931
983
  categoryId: options.youtubeCategoryId,
932
984
  privacyStatus: options.youtubePrivacy,
@@ -951,6 +1003,10 @@ function mcpScope(value) {
951
1003
  if (value === "user" || value === "project") return value;
952
1004
  throw new import_commander.InvalidArgumentError("MCP setup scope must be user or project.");
953
1005
  }
1006
+ function mcpTransport(value) {
1007
+ if (value === "auto" || value === "http" || value === "stdio") return value;
1008
+ throw new import_commander.InvalidArgumentError("MCP transport must be auto, http, or stdio.");
1009
+ }
954
1010
  function isoDateTime(value) {
955
1011
  const date = new Date(value);
956
1012
  if (Number.isNaN(date.getTime())) {
@@ -1011,13 +1067,21 @@ function requireBlogExecutionConsent(options) {
1011
1067
  throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
1012
1068
  }
1013
1069
  function formatMcpSetupPreview(result) {
1070
+ const connection = result.server.transport === "http" ? [
1071
+ `Transport: Streamable HTTP`,
1072
+ `Endpoint: ${result.server.url}`,
1073
+ "Authentication: OAuth in the MCP client"
1074
+ ] : [
1075
+ "Transport: local STDIO",
1076
+ `Command: ${result.server.command}`,
1077
+ `Arguments: ${result.server.args.join(" ")}`,
1078
+ `Rolino URL: ${result.server.env.ROLINO_URL}`
1079
+ ];
1014
1080
  return [
1015
1081
  `Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
1016
1082
  `Scope: ${result.scope}`,
1017
1083
  `Target: ${result.target}`,
1018
- `Command: ${result.server.command}`,
1019
- `Arguments: ${result.server.args.join(" ")}`,
1020
- `Rolino URL: ${result.server.env.ROLINO_URL}`,
1084
+ ...connection,
1021
1085
  "No token will be written to MCP configuration."
1022
1086
  ].join("\n");
1023
1087
  }
@@ -1103,7 +1167,7 @@ function errorForOutput(error) {
1103
1167
  function tiktokSettings(options) {
1104
1168
  const hasTikTokOptions = options.tiktokMode !== void 0 || options.tiktokVisibility !== void 0 || options.tiktokComments !== void 0 || options.tiktokDuet !== void 0 || options.tiktokStitch !== void 0 || options.tiktokCommercialContent !== void 0 || options.tiktokPromotesOwnBrand !== void 0 || options.tiktokPromotesThirdParty !== void 0 || options.tiktokAiGenerated !== void 0 || options.tiktokCoverTimestampMs !== void 0 || options.tiktokSettingsReviewed === true;
1105
1169
  if (!hasTikTokOptions) return void 0;
1106
- return import_contracts.TikTokDraftSettingsSchema.parse({
1170
+ return import_contracts2.TikTokDraftSettingsSchema.parse({
1107
1171
  postMode: options.tiktokMode,
1108
1172
  privacyLevel: options.tiktokVisibility,
1109
1173
  allowComment: options.tiktokComments,
@@ -1247,7 +1311,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1247
1311
  global,
1248
1312
  runtime,
1249
1313
  async action(context, client) {
1250
- const parsed = import_contracts.ProjectCreateInputSchema.safeParse({
1314
+ const parsed = import_contracts2.ProjectCreateInputSchema.safeParse({
1251
1315
  name: local.name,
1252
1316
  type: local.type,
1253
1317
  websiteUrl: local.website,
@@ -1480,7 +1544,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1480
1544
  });
1481
1545
  });
1482
1546
  const setup = program.command("setup").description("Configure local agent tools for Rolino");
1483
- 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) => {
1547
+ 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) => {
1484
1548
  const global = program.opts();
1485
1549
  commandExitCode = await execute({
1486
1550
  command: "setup mcp",
@@ -1493,7 +1557,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1493
1557
  cwd: runtime.cwd,
1494
1558
  env: runtime.env,
1495
1559
  nodePath: runtime.nodePath,
1496
- cliEntryPath: runtime.cliEntryPath
1560
+ cliEntryPath: runtime.cliEntryPath,
1561
+ fetch: runtime.fetch
1497
1562
  };
1498
1563
  const preview = await setupMcp({ ...setupOptions, dryRun: true });
1499
1564
  let result = preview;
@@ -1520,11 +1585,12 @@ async function runCli(argv = process.argv, overrides = {}) {
1520
1585
  `${state} Rolino MCP for ${clientLabel}.`,
1521
1586
  `Scope: ${result.scope}`,
1522
1587
  `Target: ${result.target}`,
1588
+ `Transport: ${result.transport === "http" ? "Streamable HTTP" : "local STDIO"}`,
1523
1589
  ...result.backupPath && !result.dryRun ? [`Backup: ${result.backupPath}`] : [],
1524
1590
  `Rolino URL: ${client.baseUrl}`,
1525
1591
  "No token was written to MCP configuration."
1526
1592
  ].join("\n"),
1527
- result.client === "codex" ? ["codex mcp list", "rolino auth login"] : ["claude mcp get rolino", "rolino auth login"]
1593
+ 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"]
1528
1594
  );
1529
1595
  }
1530
1596
  });
@@ -2121,7 +2187,7 @@ This moves eligible editorial dates only. It does not schedule, publish, or unpu
2121
2187
  blogArticles.command("update").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--input <json-file>", "complete Blog draft JSON file").option("--yes", "confirm the draft save").action(async (articleId, local) => {
2122
2188
  const global = program.opts();
2123
2189
  commandExitCode = await execute({ command: "blog articles update", global, runtime, async action(context, client) {
2124
- const draft = import_contracts.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2190
+ const draft = import_contracts2.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2125
2191
  await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Save a new immutable Blog draft revision?
2126
2192
  Project: ${local.project}
2127
2193
  Article: ${articleId}
@@ -2263,6 +2329,106 @@ Revision: ${local.revision}` });
2263
2329
  writeSuccess(context, data, JSON.stringify(data, null, 2));
2264
2330
  } });
2265
2331
  });
2332
+ const backlinks = program.command("backlinks").description("Review backlink prospects and public contact drafts. Rolino never sends email.");
2333
+ const backlinkTargets = backlinks.command("targets");
2334
+ backlinkTargets.command("list").requiredOption("--project <project-id>").action(async (local) => {
2335
+ const global = program.opts();
2336
+ commandExitCode = await execute({ command: "backlinks targets list", global, runtime, async action(context, client) {
2337
+ const data = await client.backlinks.targets.list(local.project, { requestId: context.requestId });
2338
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2339
+ } });
2340
+ });
2341
+ backlinkTargets.command("add").requiredOption("--project <project-id>").requiredOption("--url <url>").requiredOption("--label <label>").action(async (local) => {
2342
+ const global = program.opts();
2343
+ commandExitCode = await execute({ command: "backlinks targets add", global, runtime, async action(context, client) {
2344
+ const data = await client.backlinks.targets.add(local.project, { url: local.url, label: local.label }, context.requestId, { requestId: context.requestId });
2345
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2346
+ } });
2347
+ });
2348
+ backlinks.command("discover").requiredOption("--project <project-id>").option("--limit <number>", "maximum saved prospects", Number, 20).option("--idempotency-key <key>").action(async (local) => {
2349
+ const global = program.opts();
2350
+ commandExitCode = await execute({ command: "backlinks discover", global, runtime, async action(context, client) {
2351
+ const data = await client.backlinks.discoveries.start(local.project, { limit: local.limit }, local.idempotencyKey ?? context.requestId, { requestId: context.requestId });
2352
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2353
+ } });
2354
+ });
2355
+ const backlinkRuns = backlinks.command("runs");
2356
+ backlinkRuns.command("get").requiredOption("--project <project-id>").requiredOption("--run <run-id>").action(async (local) => {
2357
+ const global = program.opts();
2358
+ commandExitCode = await execute({ command: "backlinks runs get", global, runtime, async action(context, client) {
2359
+ const data = await client.backlinks.discoveries.get(local.project, local.run, { requestId: context.requestId });
2360
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2361
+ } });
2362
+ });
2363
+ const backlinkProspects = backlinks.command("prospects");
2364
+ backlinkProspects.command("list").requiredOption("--project <project-id>").option("--stage <stage>").action(async (local) => {
2365
+ const global = program.opts();
2366
+ commandExitCode = await execute({ command: "backlinks prospects list", global, runtime, async action(context, client) {
2367
+ const stage = local.stage ? import_contracts2.BacklinkProspectStageSchema.parse(local.stage.toUpperCase()) : void 0;
2368
+ const data = await client.backlinks.prospects.list(local.project, { stage }, { requestId: context.requestId });
2369
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2370
+ } });
2371
+ });
2372
+ backlinkProspects.command("get").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
2373
+ const global = program.opts();
2374
+ commandExitCode = await execute({ command: "backlinks prospects get", global, runtime, async action(context, client) {
2375
+ const data = await client.backlinks.prospects.get(local.project, local.prospect, { requestId: context.requestId });
2376
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2377
+ } });
2378
+ });
2379
+ backlinkProspects.command("approve").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--expected-version <number>", "current optimistic version", Number).action(async (local) => {
2380
+ const global = program.opts();
2381
+ commandExitCode = await execute({ command: "backlinks prospects approve", global, runtime, async action(context, client) {
2382
+ const data = await client.backlinks.prospects.updateStage(local.project, local.prospect, { stage: "APPROVED", expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2383
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2384
+ } });
2385
+ });
2386
+ const backlinkContacts = backlinks.command("contacts");
2387
+ backlinkContacts.command("research").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
2388
+ const global = program.opts();
2389
+ commandExitCode = await execute({ command: "backlinks contacts research", global, runtime, async action(context, client) {
2390
+ const data = await client.backlinks.prospects.researchContact(local.project, local.prospect, context.requestId, { requestId: context.requestId });
2391
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2392
+ } });
2393
+ });
2394
+ backlinkContacts.command("list").requiredOption("--project <project-id>").action(async (local) => {
2395
+ const global = program.opts();
2396
+ commandExitCode = await execute({ command: "backlinks contacts list", global, runtime, async action(context, client) {
2397
+ const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
2398
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2399
+ } });
2400
+ });
2401
+ backlinkContacts.command("export").requiredOption("--project <project-id>").option("--format <format>", "json or csv", "csv").action(async (local) => {
2402
+ const global = program.opts();
2403
+ commandExitCode = await execute({ command: "backlinks contacts export", global, runtime, async action(context, client) {
2404
+ const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
2405
+ if (local.format !== "csv") return writeSuccess(context, data, JSON.stringify(data, null, 2));
2406
+ const cell = (value) => {
2407
+ let text = String(value ?? "").replace(/[\r\n]+/g, " ");
2408
+ if (/^[=+\-@\t]/.test(text)) text = `'${text}`;
2409
+ return `"${text.replaceAll('"', '""')}"`;
2410
+ };
2411
+ 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");
2412
+ context.stdout.write(`${csv}
2413
+ `);
2414
+ } });
2415
+ });
2416
+ const backlinkOutreach = backlinks.command("outreach");
2417
+ 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) => {
2418
+ const global = program.opts();
2419
+ commandExitCode = await execute({ command: "backlinks outreach update", global, runtime, async action(context, client) {
2420
+ const payload = JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.file), "utf8"));
2421
+ const data = await client.backlinks.prospects.updateOutreach(local.project, local.prospect, { ...payload, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2422
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2423
+ } });
2424
+ });
2425
+ 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) => {
2426
+ const global = program.opts();
2427
+ commandExitCode = await execute({ command: "backlinks verify", global, runtime, async action(context, client) {
2428
+ const data = await client.backlinks.prospects.verify(local.project, local.prospect, { url: local.url, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2429
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2430
+ } });
2431
+ });
2266
2432
  const seo = program.command("seo").description("Read authorized SEO opportunities and weekly reports");
2267
2433
  const seoOpportunities = seo.command("opportunities").description("Read accepted SEO opportunities");
2268
2434
  seoOpportunities.command("list").description("List bounded SEO opportunities").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum opportunities to return", (value) => {