@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/CHANGELOG.md CHANGED
@@ -1,9 +1,34 @@
1
1
  # @rolino/cli
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - c340405: Request the complete opt-in capability set during browser authorization so users can grant draft and scheduling access to local agents. Remote MCP setup now uses host-native configuration and standard OAuth client discovery instead of a fixed Rolino-managed client.
8
+ - @rolino/contracts@0.7.1
9
+ - @rolino/sdk@0.7.1
10
+ - @rolino/local-auth@0.7.1
11
+
12
+ ## 0.7.0
13
+
14
+ ### Minor Changes
15
+
16
+ - f78e0ff: Prefer advertised Streamable HTTP MCP with client-owned OAuth, add explicit HTTP and STDIO transport selection, and keep local MCP as a fallback.
17
+ - 358f37a: Expose additive safe Blog preparation, next-action, and quality summaries through the shared read contracts and existing clients.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [358f37a]
22
+ - @rolino/contracts@0.7.0
23
+ - @rolino/sdk@0.7.0
24
+ - @rolino/local-auth@0.7.0
25
+
3
26
  ## 0.6.0
4
27
 
5
28
  ### Minor Changes
6
29
 
30
+ - Add matching backlink prospecting commands with safe JSON output and formula-safe contact CSV export. Rolino does not send email.
31
+
7
32
  - d162f85: Add safe Blog editorial cadence preview and apply operations across contracts, SDK, CLI, and MCP.
8
33
  - 50cb9fc: Add Google Business Profile as a disabled-by-default standard text and single-image publishing destination across shared public clients.
9
34
  - 1ac7ed5: Add signed custom Blog webhook setup, connection testing, one-time secret rotation, and safe idempotent replay parity.
@@ -93,6 +118,10 @@
93
118
 
94
119
  ### Minor Changes
95
120
 
121
+ - Show additive safe Blog preparation and quality state in existing JSON reads.
122
+
123
+ ### Minor Changes
124
+
96
125
  - Add Blog image generation, exact local-file upload, image review, confirmed
97
126
  exact-bundle approval, future scheduling, and schedule cancellation commands
98
127
  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,26 +102,35 @@ 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`. For Codex, it leaves
120
+ OAuth client registration and the loopback callback to Codex so Codex can use
121
+ Rolino's advertised Client ID Metadata Document support. Run `codex mcp login
122
+ rolino` after remote Codex setup. Use `--transport http` to
123
+ require remote MCP or `--transport stdio` to require the local package and its
124
+ shared CLI OAuth session. The command previews the exact token-free
125
+ configuration and asks before changing anything. User scope is the default.
106
126
  Codex configuration is written to `CODEX_HOME/config.toml` or
107
127
  `~/.codex/config.toml`; Claude Code user scope is applied through its official
108
128
  CLI. Project scope writes `.codex/config.toml` or `.mcp.json` in the current
109
129
  directory. Existing file-backed configuration is backed up with a
110
- `.rolino-backup` suffix. Use `--dry-run` to preview in scripts and `--yes` to
111
- provide explicit non-interactive consent; `--agent` never implies consent.
130
+ `.rolino-backup` suffix. Use `--dry-run` to preview in scripts and `--yes` to
131
+ provide explicit non-interactive consent; `--agent` never implies consent.
132
+ An orphan Rolino-managed marker left by an earlier removal is repaired without
133
+ changing unrelated Codex settings.
112
134
 
113
135
  Draft creation and update never schedule or publish. Updates change only the
114
136
  fields supplied; omitted media, destinations, captions, and provider settings
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.1",
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.1",
95
+ "@rolino/local-auth": "0.7.1",
96
+ "@rolino/sdk": "0.7.1",
97
97
  commander: "^15.0.0",
98
98
  open: "^11.0.0"
99
99
  },
@@ -106,24 +106,21 @@ var package_default = {
106
106
  };
107
107
 
108
108
  // src/cli.ts
109
- var import_contracts = require("@rolino/contracts");
109
+ var import_contracts2 = require("@rolino/contracts");
110
110
  var import_sdk = require("@rolino/sdk");
111
111
 
112
112
  // src/browser-login.ts
113
113
  var import_node_crypto = require("crypto");
114
114
  var import_node_http = require("http");
115
+ var import_contracts = require("@rolino/contracts");
115
116
  var import_local_auth = require("@rolino/local-auth");
116
117
  var import_open = __toESM(require("open"), 1);
117
118
  var OAUTH_CALLBACK_PORT = 48391;
118
119
  var OAUTH_CALLBACK_PATH = "/oauth/callback";
119
120
  var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
120
- var DEFAULT_SCOPES = [
121
+ var AUTHORIZATION_REQUEST_SCOPES = [
121
122
  "offline_access",
122
- "identity:read",
123
- "projects:read",
124
- "posts:read",
125
- "integrations:read",
126
- "calendar:read"
123
+ ...import_contracts.AGENT_CAPABILITIES
127
124
  ];
128
125
  function escapeHtml(value) {
129
126
  return value.replace(/[&<>"']/g, (character) => ({
@@ -163,7 +160,7 @@ function createBrowserAuthorizationRequest(baseUrl) {
163
160
  url.searchParams.set("response_type", "code");
164
161
  url.searchParams.set("client_id", configuration.clientId);
165
162
  url.searchParams.set("redirect_uri", redirectUri);
166
- url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
163
+ url.searchParams.set("scope", AUTHORIZATION_REQUEST_SCOPES.join(" "));
167
164
  url.searchParams.set("resource", configuration.resource);
168
165
  url.searchParams.set("state", state);
169
166
  url.searchParams.set("code_challenge", codeChallenge);
@@ -202,7 +199,7 @@ async function exchangeAuthorizationCode(options) {
202
199
  accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
203
200
  refreshToken: payload.refresh_token,
204
201
  refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
205
- scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...DEFAULT_SCOPES]
202
+ scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...AUTHORIZATION_REQUEST_SCOPES]
206
203
  };
207
204
  }
208
205
  async function loginWithBrowser(options) {
@@ -649,12 +646,18 @@ function removeExistingCodexEntry(source, newline) {
649
646
  return kept.join(newline).trimEnd();
650
647
  }
651
648
  function codexBlock(server, newline) {
649
+ const configuration = server.transport === "http" ? [
650
+ `url = ${tomlString(server.url)}`,
651
+ `auth = ${tomlString(server.auth)}`
652
+ ] : [
653
+ `command = ${tomlString(server.command)}`,
654
+ `args = [${server.args.map(tomlString).join(", ")}]`,
655
+ `env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`
656
+ ];
652
657
  return [
653
658
  CODEX_BEGIN,
654
659
  "[mcp_servers.rolino]",
655
- `command = ${tomlString(server.command)}`,
656
- `args = [${server.args.map(tomlString).join(", ")}]`,
657
- `env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`,
660
+ ...configuration,
658
661
  CODEX_END
659
662
  ].join(newline);
660
663
  }
@@ -663,13 +666,12 @@ function updateCodexConfig(source, server) {
663
666
  const block = codexBlock(server, newline);
664
667
  const begin = source.indexOf(CODEX_BEGIN);
665
668
  const end = source.indexOf(CODEX_END);
666
- if (begin === -1 !== (end === -1) || begin !== -1 && end < begin) {
667
- throw new TypeError("Codex config contains an incomplete Rolino-managed block.");
668
- }
669
669
  let unmanagedSource = source;
670
- if (begin !== -1) {
670
+ if (begin !== -1 && end !== -1 && end > begin) {
671
671
  const after = end + CODEX_END.length;
672
672
  unmanagedSource = `${source.slice(0, begin)}${source.slice(after)}`;
673
+ } else if (begin !== -1 || end !== -1) {
674
+ unmanagedSource = source.split(/\r?\n/).filter((line) => line.trim() !== CODEX_BEGIN && line.trim() !== CODEX_END).join(newline);
673
675
  }
674
676
  const withoutRolino = removeExistingCodexEntry(unmanagedSource, newline);
675
677
  return `${withoutRolino ? `${withoutRolino}${newline}${newline}` : ""}${block}${newline}`;
@@ -694,7 +696,12 @@ function updateClaudeProjectConfig(source, server) {
694
696
  ...parsed,
695
697
  mcpServers: {
696
698
  ...currentServers,
697
- rolino: { type: "stdio", ...server }
699
+ rolino: server.transport === "http" ? { type: "http", url: server.url } : {
700
+ type: "stdio",
701
+ command: server.command,
702
+ args: server.args,
703
+ env: server.env
704
+ }
698
705
  }
699
706
  }, null, 2)}
700
707
  `;
@@ -726,6 +733,7 @@ async function setupCodex(options, server) {
726
733
  backupPath,
727
734
  changed,
728
735
  dryRun: options.dryRun ?? false,
736
+ transport: server.transport,
729
737
  server
730
738
  };
731
739
  }
@@ -746,6 +754,7 @@ async function setupClaudeCode(options, server) {
746
754
  backupPath,
747
755
  changed,
748
756
  dryRun: options.dryRun ?? false,
757
+ transport: server.transport,
749
758
  server
750
759
  };
751
760
  }
@@ -758,6 +767,7 @@ async function setupClaudeCode(options, server) {
758
767
  backupPath: null,
759
768
  changed: true,
760
769
  dryRun: true,
770
+ transport: server.transport,
761
771
  server
762
772
  };
763
773
  }
@@ -771,7 +781,12 @@ async function setupClaudeCode(options, server) {
771
781
  "mcp",
772
782
  "add-json",
773
783
  "rolino",
774
- JSON.stringify({ type: "stdio", ...server }),
784
+ JSON.stringify(server.transport === "http" ? { type: "http", url: server.url } : {
785
+ type: "stdio",
786
+ command: server.command,
787
+ args: server.args,
788
+ env: server.env
789
+ }),
775
790
  "--scope",
776
791
  "user"
777
792
  ], options);
@@ -787,14 +802,51 @@ async function setupClaudeCode(options, server) {
787
802
  backupPath: null,
788
803
  changed: true,
789
804
  dryRun: false,
805
+ transport: server.transport,
790
806
  server
791
807
  };
792
808
  }
809
+ function remoteMcpUrl(baseUrl) {
810
+ return new URL("mcp", `${baseUrl.replace(/\/$/, "")}/`).toString();
811
+ }
812
+ async function advertisedRemoteMcp(options) {
813
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
814
+ try {
815
+ const response = await fetchImplementation(
816
+ new URL("api/v1/meta", `${options.baseUrl.replace(/\/$/, "")}/`),
817
+ {
818
+ headers: { accept: "application/json" },
819
+ signal: AbortSignal.timeout(5e3)
820
+ }
821
+ );
822
+ if (!response.ok) return false;
823
+ const payload = await response.json();
824
+ return payload.data?.mcp?.streamableHttp === true;
825
+ } catch {
826
+ return false;
827
+ }
828
+ }
829
+ async function resolveTransport(options) {
830
+ const requested = options.transport ?? "stdio";
831
+ if (requested === "stdio") return "stdio";
832
+ if (await advertisedRemoteMcp(options)) return "http";
833
+ if (requested === "http") {
834
+ throw new TypeError(
835
+ "This Rolino instance does not advertise Streamable HTTP MCP. Use --transport stdio, or enable and verify remote MCP on the server."
836
+ );
837
+ }
838
+ return "stdio";
839
+ }
793
840
  async function setupMcp(options) {
794
- const serverPath = await resolveServerPath(options);
795
- const server = {
841
+ const transport = await resolveTransport(options);
842
+ const server = transport === "http" ? {
843
+ transport: "http",
844
+ url: remoteMcpUrl(options.baseUrl),
845
+ auth: "oauth"
846
+ } : {
847
+ transport: "stdio",
796
848
  command: options.nodePath,
797
- args: [serverPath],
849
+ args: [await resolveServerPath(options)],
798
850
  env: { ROLINO_URL: options.baseUrl }
799
851
  };
800
852
  return options.client === "codex" ? setupCodex(options, server) : setupClaudeCode(options, server);
@@ -825,40 +877,40 @@ function outputFormat(value) {
825
877
  throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
826
878
  }
827
879
  function postStatus(value) {
828
- const parsed = import_contracts.PostStatusSchema.safeParse(value.toUpperCase());
880
+ const parsed = import_contracts2.PostStatusSchema.safeParse(value.toUpperCase());
829
881
  if (parsed.success) return parsed.data;
830
882
  throw new import_commander.InvalidArgumentError(
831
- `Status must be one of ${import_contracts.PostStatusSchema.options.join(", ")}.`
883
+ `Status must be one of ${import_contracts2.PostStatusSchema.options.join(", ")}.`
832
884
  );
833
885
  }
834
886
  function seoOpportunityKind(value) {
835
- const parsed = import_contracts.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
887
+ const parsed = import_contracts2.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
836
888
  if (parsed.success) return parsed.data;
837
- throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts.SeoOpportunityKindSchema.options.join(", ")}.`);
889
+ throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts2.SeoOpportunityKindSchema.options.join(", ")}.`);
838
890
  }
839
891
  function seoExpectedImpact(value) {
840
- const parsed = import_contracts.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
892
+ const parsed = import_contracts2.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
841
893
  if (parsed.success) return parsed.data;
842
894
  throw new import_commander.InvalidArgumentError("SEO impact must be HIGH, MEDIUM, or LOW.");
843
895
  }
844
896
  function seoReportCompleteness(value) {
845
- const parsed = import_contracts.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
897
+ const parsed = import_contracts2.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
846
898
  if (parsed.success) return parsed.data;
847
899
  throw new import_commander.InvalidArgumentError("Report status must be COMPLETE or PARTIAL.");
848
900
  }
849
901
  function projectType(value) {
850
- const parsed = import_contracts.ProjectTypeSchema.safeParse(value.toUpperCase());
902
+ const parsed = import_contracts2.ProjectTypeSchema.safeParse(value.toUpperCase());
851
903
  if (parsed.success) return parsed.data;
852
904
  throw new import_commander.InvalidArgumentError(
853
- `Type must be one of ${import_contracts.ProjectTypeSchema.options.join(", ")}.`
905
+ `Type must be one of ${import_contracts2.ProjectTypeSchema.options.join(", ")}.`
854
906
  );
855
907
  }
856
908
  function publishingProvider(value) {
857
909
  const normalized = value.toUpperCase();
858
- const parsed = import_contracts.PublishingProviderSchema.safeParse(normalized);
910
+ const parsed = import_contracts2.PublishingProviderSchema.safeParse(normalized);
859
911
  if (parsed.success) return parsed.data;
860
912
  throw new import_commander.InvalidArgumentError(
861
- `Platform must be one of ${import_contracts.PublishingProviderSchema.options.join(", ")}.`
913
+ `Platform must be one of ${import_contracts2.PublishingProviderSchema.options.join(", ")}.`
862
914
  );
863
915
  }
864
916
  function collectPublishingProvider(value, previous) {
@@ -869,10 +921,10 @@ function collectString(value, previous) {
869
921
  }
870
922
  function deliveryOptionsProvider(value) {
871
923
  const normalized = value.toUpperCase();
872
- const parsed = import_contracts.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
924
+ const parsed = import_contracts2.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
873
925
  if (parsed.success) return parsed.data;
874
926
  throw new import_commander.InvalidArgumentError(
875
- `Provider must be one of ${import_contracts.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
927
+ `Provider must be one of ${import_contracts2.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
876
928
  );
877
929
  }
878
930
  function tiktokPostMode(value) {
@@ -910,7 +962,7 @@ function yesOrNo(value) {
910
962
  function youtubeSettings(options) {
911
963
  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;
912
964
  if (!hasYouTubeOptions) return null;
913
- return import_contracts.YouTubePostSettingsSchema.parse({
965
+ return import_contracts2.YouTubePostSettingsSchema.parse({
914
966
  title: options.youtubeTitle,
915
967
  categoryId: options.youtubeCategoryId,
916
968
  privacyStatus: options.youtubePrivacy,
@@ -935,6 +987,10 @@ function mcpScope(value) {
935
987
  if (value === "user" || value === "project") return value;
936
988
  throw new import_commander.InvalidArgumentError("MCP setup scope must be user or project.");
937
989
  }
990
+ function mcpTransport(value) {
991
+ if (value === "auto" || value === "http" || value === "stdio") return value;
992
+ throw new import_commander.InvalidArgumentError("MCP transport must be auto, http, or stdio.");
993
+ }
938
994
  function isoDateTime(value) {
939
995
  const date = new Date(value);
940
996
  if (Number.isNaN(date.getTime())) {
@@ -995,13 +1051,21 @@ function requireBlogExecutionConsent(options) {
995
1051
  throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
996
1052
  }
997
1053
  function formatMcpSetupPreview(result) {
1054
+ const connection = result.server.transport === "http" ? [
1055
+ `Transport: Streamable HTTP`,
1056
+ `Endpoint: ${result.server.url}`,
1057
+ "Authentication: OAuth in the MCP client"
1058
+ ] : [
1059
+ "Transport: local STDIO",
1060
+ `Command: ${result.server.command}`,
1061
+ `Arguments: ${result.server.args.join(" ")}`,
1062
+ `Rolino URL: ${result.server.env.ROLINO_URL}`
1063
+ ];
998
1064
  return [
999
1065
  `Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
1000
1066
  `Scope: ${result.scope}`,
1001
1067
  `Target: ${result.target}`,
1002
- `Command: ${result.server.command}`,
1003
- `Arguments: ${result.server.args.join(" ")}`,
1004
- `Rolino URL: ${result.server.env.ROLINO_URL}`,
1068
+ ...connection,
1005
1069
  "No token will be written to MCP configuration."
1006
1070
  ].join("\n");
1007
1071
  }
@@ -1087,7 +1151,7 @@ function errorForOutput(error) {
1087
1151
  function tiktokSettings(options) {
1088
1152
  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;
1089
1153
  if (!hasTikTokOptions) return void 0;
1090
- return import_contracts.TikTokDraftSettingsSchema.parse({
1154
+ return import_contracts2.TikTokDraftSettingsSchema.parse({
1091
1155
  postMode: options.tiktokMode,
1092
1156
  privacyLevel: options.tiktokVisibility,
1093
1157
  allowComment: options.tiktokComments,
@@ -1231,7 +1295,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1231
1295
  global,
1232
1296
  runtime,
1233
1297
  async action(context, client) {
1234
- const parsed = import_contracts.ProjectCreateInputSchema.safeParse({
1298
+ const parsed = import_contracts2.ProjectCreateInputSchema.safeParse({
1235
1299
  name: local.name,
1236
1300
  type: local.type,
1237
1301
  websiteUrl: local.website,
@@ -1464,7 +1528,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1464
1528
  });
1465
1529
  });
1466
1530
  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) => {
1531
+ 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
1532
  const global = program.opts();
1469
1533
  commandExitCode = await execute({
1470
1534
  command: "setup mcp",
@@ -1477,7 +1541,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1477
1541
  cwd: runtime.cwd,
1478
1542
  env: runtime.env,
1479
1543
  nodePath: runtime.nodePath,
1480
- cliEntryPath: runtime.cliEntryPath
1544
+ cliEntryPath: runtime.cliEntryPath,
1545
+ fetch: runtime.fetch
1481
1546
  };
1482
1547
  const preview = await setupMcp({ ...setupOptions, dryRun: true });
1483
1548
  let result = preview;
@@ -1504,11 +1569,12 @@ async function runCli(argv = process.argv, overrides = {}) {
1504
1569
  `${state} Rolino MCP for ${clientLabel}.`,
1505
1570
  `Scope: ${result.scope}`,
1506
1571
  `Target: ${result.target}`,
1572
+ `Transport: ${result.transport === "http" ? "Streamable HTTP" : "local STDIO"}`,
1507
1573
  ...result.backupPath && !result.dryRun ? [`Backup: ${result.backupPath}`] : [],
1508
1574
  `Rolino URL: ${client.baseUrl}`,
1509
1575
  "No token was written to MCP configuration."
1510
1576
  ].join("\n"),
1511
- result.client === "codex" ? ["codex mcp list", "rolino auth login"] : ["claude mcp get rolino", "rolino auth login"]
1577
+ 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
1578
  );
1513
1579
  }
1514
1580
  });
@@ -2105,7 +2171,7 @@ This moves eligible editorial dates only. It does not schedule, publish, or unpu
2105
2171
  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) => {
2106
2172
  const global = program.opts();
2107
2173
  commandExitCode = await execute({ command: "blog articles update", global, runtime, async action(context, client) {
2108
- const draft = import_contracts.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2174
+ const draft = import_contracts2.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2109
2175
  await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Save a new immutable Blog draft revision?
2110
2176
  Project: ${local.project}
2111
2177
  Article: ${articleId}
@@ -2247,6 +2313,106 @@ Revision: ${local.revision}` });
2247
2313
  writeSuccess(context, data, JSON.stringify(data, null, 2));
2248
2314
  } });
2249
2315
  });
2316
+ const backlinks = program.command("backlinks").description("Review backlink prospects and public contact drafts. Rolino never sends email.");
2317
+ const backlinkTargets = backlinks.command("targets");
2318
+ backlinkTargets.command("list").requiredOption("--project <project-id>").action(async (local) => {
2319
+ const global = program.opts();
2320
+ commandExitCode = await execute({ command: "backlinks targets list", global, runtime, async action(context, client) {
2321
+ const data = await client.backlinks.targets.list(local.project, { requestId: context.requestId });
2322
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2323
+ } });
2324
+ });
2325
+ backlinkTargets.command("add").requiredOption("--project <project-id>").requiredOption("--url <url>").requiredOption("--label <label>").action(async (local) => {
2326
+ const global = program.opts();
2327
+ commandExitCode = await execute({ command: "backlinks targets add", global, runtime, async action(context, client) {
2328
+ const data = await client.backlinks.targets.add(local.project, { url: local.url, label: local.label }, context.requestId, { requestId: context.requestId });
2329
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2330
+ } });
2331
+ });
2332
+ backlinks.command("discover").requiredOption("--project <project-id>").option("--limit <number>", "maximum saved prospects", Number, 20).option("--idempotency-key <key>").action(async (local) => {
2333
+ const global = program.opts();
2334
+ commandExitCode = await execute({ command: "backlinks discover", global, runtime, async action(context, client) {
2335
+ const data = await client.backlinks.discoveries.start(local.project, { limit: local.limit }, local.idempotencyKey ?? context.requestId, { requestId: context.requestId });
2336
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2337
+ } });
2338
+ });
2339
+ const backlinkRuns = backlinks.command("runs");
2340
+ backlinkRuns.command("get").requiredOption("--project <project-id>").requiredOption("--run <run-id>").action(async (local) => {
2341
+ const global = program.opts();
2342
+ commandExitCode = await execute({ command: "backlinks runs get", global, runtime, async action(context, client) {
2343
+ const data = await client.backlinks.discoveries.get(local.project, local.run, { requestId: context.requestId });
2344
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2345
+ } });
2346
+ });
2347
+ const backlinkProspects = backlinks.command("prospects");
2348
+ backlinkProspects.command("list").requiredOption("--project <project-id>").option("--stage <stage>").action(async (local) => {
2349
+ const global = program.opts();
2350
+ commandExitCode = await execute({ command: "backlinks prospects list", global, runtime, async action(context, client) {
2351
+ const stage = local.stage ? import_contracts2.BacklinkProspectStageSchema.parse(local.stage.toUpperCase()) : void 0;
2352
+ const data = await client.backlinks.prospects.list(local.project, { stage }, { requestId: context.requestId });
2353
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2354
+ } });
2355
+ });
2356
+ backlinkProspects.command("get").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
2357
+ const global = program.opts();
2358
+ commandExitCode = await execute({ command: "backlinks prospects get", global, runtime, async action(context, client) {
2359
+ const data = await client.backlinks.prospects.get(local.project, local.prospect, { requestId: context.requestId });
2360
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2361
+ } });
2362
+ });
2363
+ backlinkProspects.command("approve").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--expected-version <number>", "current optimistic version", Number).action(async (local) => {
2364
+ const global = program.opts();
2365
+ commandExitCode = await execute({ command: "backlinks prospects approve", global, runtime, async action(context, client) {
2366
+ const data = await client.backlinks.prospects.updateStage(local.project, local.prospect, { stage: "APPROVED", expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2367
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2368
+ } });
2369
+ });
2370
+ const backlinkContacts = backlinks.command("contacts");
2371
+ backlinkContacts.command("research").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
2372
+ const global = program.opts();
2373
+ commandExitCode = await execute({ command: "backlinks contacts research", global, runtime, async action(context, client) {
2374
+ const data = await client.backlinks.prospects.researchContact(local.project, local.prospect, context.requestId, { requestId: context.requestId });
2375
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2376
+ } });
2377
+ });
2378
+ backlinkContacts.command("list").requiredOption("--project <project-id>").action(async (local) => {
2379
+ const global = program.opts();
2380
+ commandExitCode = await execute({ command: "backlinks contacts list", global, runtime, async action(context, client) {
2381
+ const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
2382
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2383
+ } });
2384
+ });
2385
+ backlinkContacts.command("export").requiredOption("--project <project-id>").option("--format <format>", "json or csv", "csv").action(async (local) => {
2386
+ const global = program.opts();
2387
+ commandExitCode = await execute({ command: "backlinks contacts export", global, runtime, async action(context, client) {
2388
+ const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
2389
+ if (local.format !== "csv") return writeSuccess(context, data, JSON.stringify(data, null, 2));
2390
+ const cell = (value) => {
2391
+ let text = String(value ?? "").replace(/[\r\n]+/g, " ");
2392
+ if (/^[=+\-@\t]/.test(text)) text = `'${text}`;
2393
+ return `"${text.replaceAll('"', '""')}"`;
2394
+ };
2395
+ 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");
2396
+ context.stdout.write(`${csv}
2397
+ `);
2398
+ } });
2399
+ });
2400
+ const backlinkOutreach = backlinks.command("outreach");
2401
+ 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) => {
2402
+ const global = program.opts();
2403
+ commandExitCode = await execute({ command: "backlinks outreach update", global, runtime, async action(context, client) {
2404
+ const payload = JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.file), "utf8"));
2405
+ const data = await client.backlinks.prospects.updateOutreach(local.project, local.prospect, { ...payload, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2406
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2407
+ } });
2408
+ });
2409
+ 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) => {
2410
+ const global = program.opts();
2411
+ commandExitCode = await execute({ command: "backlinks verify", global, runtime, async action(context, client) {
2412
+ const data = await client.backlinks.prospects.verify(local.project, local.prospect, { url: local.url, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
2413
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2414
+ } });
2415
+ });
2250
2416
  const seo = program.command("seo").description("Read authorized SEO opportunities and weekly reports");
2251
2417
  const seoOpportunities = seo.command("opportunities").description("Read accepted SEO opportunities");
2252
2418
  seoOpportunities.command("list").description("List bounded SEO opportunities").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum opportunities to return", (value) => {