@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 +29 -0
- package/README.md +43 -21
- package/dist/bin.cjs +213 -47
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-3WBZCTM5.js → chunk-KSPZKKBG.js} +198 -31
- package/dist/chunk-KSPZKKBG.js.map +1 -0
- package/dist/index.cjs +213 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -10
- package/dist/index.d.ts +21 -10
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-3WBZCTM5.js.map +0 -1
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
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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`
|
|
105
|
-
|
|
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.
|
|
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.
|
|
95
|
-
"@rolino/local-auth": "0.
|
|
96
|
-
"@rolino/sdk": "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
|
|
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
|
|
121
|
+
var AUTHORIZATION_REQUEST_SCOPES = [
|
|
121
122
|
"offline_access",
|
|
122
|
-
|
|
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",
|
|
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) : [...
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
|
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: [
|
|
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 =
|
|
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 ${
|
|
883
|
+
`Status must be one of ${import_contracts2.PostStatusSchema.options.join(", ")}.`
|
|
832
884
|
);
|
|
833
885
|
}
|
|
834
886
|
function seoOpportunityKind(value) {
|
|
835
|
-
const parsed =
|
|
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 ${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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) => {
|