@rolino/cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +71 -21
- package/dist/bin.cjs +428 -251
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-MBZL2CK6.js → chunk-PEJ2WA66.js} +415 -236
- package/dist/chunk-PEJ2WA66.js.map +1 -0
- package/dist/index.cjs +428 -251
- 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 +5 -5
- package/dist/chunk-MBZL2CK6.js.map +0 -1
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.0",
|
|
38
38
|
description: "Agent-friendly command-line interface for Rolino",
|
|
39
39
|
type: "module",
|
|
40
40
|
license: "MIT",
|
|
@@ -91,14 +91,14 @@ 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.0",
|
|
95
|
+
"@rolino/local-auth": "0.7.0",
|
|
96
|
+
"@rolino/sdk": "0.7.0",
|
|
97
97
|
commander: "^15.0.0",
|
|
98
98
|
open: "^11.0.0"
|
|
99
99
|
},
|
|
100
100
|
devDependencies: {
|
|
101
|
-
tsx: "^4.
|
|
101
|
+
tsx: "^4.23.12"
|
|
102
102
|
},
|
|
103
103
|
engines: {
|
|
104
104
|
node: ">=20.19.0"
|
|
@@ -106,23 +106,25 @@ var package_default = {
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
// src/cli.ts
|
|
109
|
-
var
|
|
109
|
+
var import_contracts = 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
|
|
116
|
-
var import_contracts = require("@rolino/contracts");
|
|
115
|
+
var import_local_auth = require("@rolino/local-auth");
|
|
117
116
|
var import_open = __toESM(require("open"), 1);
|
|
118
|
-
var
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
var OAUTH_CALLBACK_PORT = 48391;
|
|
118
|
+
var OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
119
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
120
|
+
var DEFAULT_SCOPES = [
|
|
121
|
+
"offline_access",
|
|
122
|
+
"identity:read",
|
|
123
|
+
"projects:read",
|
|
124
|
+
"posts:read",
|
|
125
|
+
"integrations:read",
|
|
126
|
+
"calendar:read"
|
|
127
|
+
];
|
|
126
128
|
function escapeHtml(value) {
|
|
127
129
|
return value.replace(/[&<>"']/g, (character) => ({
|
|
128
130
|
"&": "&",
|
|
@@ -132,86 +134,18 @@ function escapeHtml(value) {
|
|
|
132
134
|
"'": "'"
|
|
133
135
|
})[character]);
|
|
134
136
|
}
|
|
135
|
-
function
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const detail = options.success ? '<div class="terminal" aria-label="Terminal authentication status"><div class="terminal-bar"><i></i><i></i><i></i><span>ROLINO CLI</span></div><div class="command"><b>$</b> rolino whoami</div><div class="terminal-result"><span>\u2713</span> Authenticated. Ready for your next command.</div></div>' : '<div class="return-note"><span>\u2192</span><div><strong>Return to your terminal</strong><small>Start the login flow again when you are ready.</small></div></div>';
|
|
139
|
-
return `<!doctype html>
|
|
140
|
-
<html lang="en">
|
|
141
|
-
<head>
|
|
142
|
-
<meta charset="utf-8">
|
|
143
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
144
|
-
<meta name="color-scheme" content="light">
|
|
145
|
-
<title>${escapeHtml(options.title)} \xB7 Rolino</title>
|
|
146
|
-
<style>
|
|
147
|
-
*{box-sizing:border-box}
|
|
148
|
-
html,body{height:100%;margin:0}
|
|
149
|
-
body{min-height:100svh;overflow:hidden;background:#faf9f1;color:#0f172a;font-family:"Poppins","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif}
|
|
150
|
-
.shell{position:relative;isolation:isolate;display:grid;min-height:100svh;grid-template-rows:auto 1fr auto;padding:clamp(18px,3vw,38px);overflow:hidden}
|
|
151
|
-
.shape{position:absolute;z-index:-1;border-radius:999px;pointer-events:none}
|
|
152
|
-
.shape-a{top:-160px;left:-130px;width:390px;height:390px;background:#facc1530;filter:blur(2px)}
|
|
153
|
-
.shape-b{right:-100px;bottom:-160px;width:360px;height:360px;background:#34c78a20}
|
|
154
|
-
.shape-c{right:9%;top:14%;width:22px;height:22px;background:#ef6a5b;transform:rotate(18deg);border-radius:7px}
|
|
155
|
-
.topbar,.footer{display:flex;width:min(100%,1100px);margin:auto;align-items:center;justify-content:space-between}
|
|
156
|
-
.brand{display:flex;align-items:center;gap:10px;color:#0f172a;font-size:22px;font-weight:800;letter-spacing:-.04em}
|
|
157
|
-
.brand svg{display:block;width:36px;height:36px}
|
|
158
|
-
.secure{display:flex;align-items:center;gap:8px;color:#52607a;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
|
|
159
|
-
.secure-dot{display:block;width:8px;height:8px;border-radius:50%;background:#34c78a;box-shadow:0 0 0 5px #34c78a1f}
|
|
160
|
-
.stage{display:grid;min-height:0;place-items:center;padding:12px 0}
|
|
161
|
-
.panel{width:min(100%,590px);padding:clamp(26px,4vw,46px);text-align:center;background:#fffefa;border:1px solid #e3e6ed;border-radius:32px;box-shadow:0 36px 90px -52px #0f172a73;animation:arrive .55s cubic-bezier(.22,1,.36,1) both}
|
|
162
|
-
.mascot-wrap{position:relative;display:grid;width:112px;height:96px;margin:0 auto 12px;place-items:center}
|
|
163
|
-
.mascot-wrap:before{position:absolute;width:88px;height:88px;border-radius:50%;background:#fff4a8;content:"";box-shadow:0 0 0 14px #fff9d8}
|
|
164
|
-
.mascot-wrap svg{position:relative;width:78px;height:78px;filter:drop-shadow(0 10px 12px #c58e0026);animation:mascot-arrive .65s .08s cubic-bezier(.22,1,.36,1) both}
|
|
165
|
-
.eyebrow{display:inline-flex;align-items:center;gap:7px;margin:0 0 12px;padding:6px 11px;border-radius:999px;background:#fff8b8;color:#7a4d00;font-size:10px;font-weight:900;letter-spacing:.14em;text-transform:uppercase}
|
|
166
|
-
.eyebrow:before{width:7px;height:7px;border-radius:50%;background:#34c78a;content:""}
|
|
167
|
-
.failure .eyebrow{background:#fee6df;color:#9f2f24}
|
|
168
|
-
.failure .eyebrow:before{background:#ef6a5b}
|
|
169
|
-
h1{margin:0;color:#0f172a;font-family:"Outfit","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif;font-size:clamp(34px,5vw,48px);line-height:1;letter-spacing:-.045em}
|
|
170
|
-
.message{max-width:450px;margin:14px auto 0;color:#52607a;font-size:15px;line-height:1.65}
|
|
171
|
-
.terminal{max-width:450px;margin:24px auto 0;padding:14px 16px 16px;text-align:left;background:#111b30;color:#eff3fa;border:1px solid #293550;border-radius:18px;box-shadow:0 18px 42px -26px #0f172acc}
|
|
172
|
-
.terminal-bar{display:flex;align-items:center;gap:6px;padding-bottom:12px;color:#8f9bb1;font-size:9px;font-weight:800;letter-spacing:.13em}
|
|
173
|
-
.terminal-bar i{display:block;width:7px;height:7px;border-radius:50%;background:#ef6a5b}
|
|
174
|
-
.terminal-bar i:nth-child(2){background:#facc15}
|
|
175
|
-
.terminal-bar i:nth-child(3){margin-right:5px;background:#34c78a}
|
|
176
|
-
.command{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
|
|
177
|
-
.command b{color:#facc15}
|
|
178
|
-
.terminal-result{display:flex;align-items:center;gap:8px;margin-top:9px;color:#aeb9ca;font-size:12px}
|
|
179
|
-
.terminal-result span{color:#52d49e;font-weight:900}
|
|
180
|
-
.return-note{display:flex;max-width:420px;margin:24px auto 0;padding:14px 16px;align-items:center;gap:12px;text-align:left;background:#fff2ed;border:1px solid #f6c9bd;border-radius:18px;color:#7e2f29}
|
|
181
|
-
.return-note>span{font-size:24px}
|
|
182
|
-
.return-note strong,.return-note small{display:block}
|
|
183
|
-
.return-note small{margin-top:3px;color:#875b56}
|
|
184
|
-
.footer{justify-content:center;color:#6f788a;font-size:11px}
|
|
185
|
-
@keyframes arrive{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
|
|
186
|
-
@keyframes mascot-arrive{from{opacity:0;transform:translateY(10px) rotate(-4deg) scale(.88)}to{opacity:1;transform:none}}
|
|
187
|
-
@media(max-width:600px){.secure{display:none}.panel{border-radius:24px}.shape-c{display:none}}
|
|
188
|
-
@media(max-height:700px){.shell{padding:16px}.stage{padding:6px 0}.panel{padding:20px 26px}.mascot-wrap{width:88px;height:72px;margin-bottom:6px}.mascot-wrap:before{width:68px;height:68px;box-shadow:0 0 0 10px #fff9d8}.mascot-wrap svg{width:60px;height:60px}.eyebrow{margin-bottom:8px}h1{font-size:32px}.message{margin-top:10px}.terminal,.return-note{margin-top:14px}}
|
|
189
|
-
@media(prefers-reduced-motion:reduce){.panel,.mascot-wrap svg{animation:none}}
|
|
190
|
-
</style>
|
|
191
|
-
</head>
|
|
192
|
-
<body class="${tone}">
|
|
193
|
-
<main class="shell">
|
|
194
|
-
<span class="shape shape-a"></span><span class="shape shape-b"></span><span class="shape shape-c"></span>
|
|
195
|
-
<header class="topbar">
|
|
196
|
-
<div class="brand">${ROLINO_MASCOT_SVG}<span>Rolino</span></div>
|
|
197
|
-
<div class="secure"><span class="secure-dot"></span>Secure CLI authorization</div>
|
|
198
|
-
</header>
|
|
199
|
-
<section class="stage">
|
|
200
|
-
<div class="panel">
|
|
201
|
-
<div class="mascot-wrap">${ROLINO_MASCOT_SVG}</div>
|
|
202
|
-
<p class="eyebrow">${eyebrow}</p>
|
|
203
|
-
<h1>${escapeHtml(options.title)}</h1>
|
|
204
|
-
<p class="message">${escapeHtml(options.message)}</p>
|
|
205
|
-
${detail}
|
|
206
|
-
</div>
|
|
207
|
-
</section>
|
|
208
|
-
<footer class="footer">Secure browser handoff \xB7 no password shared</footer>
|
|
209
|
-
</main>
|
|
210
|
-
</body>
|
|
211
|
-
</html>`;
|
|
137
|
+
function renderOAuthAuthorizationResultPage(options) {
|
|
138
|
+
const color = options.success ? "#087f5b" : "#b42318";
|
|
139
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>${escapeHtml(options.title)} \xB7 Rolino</title><style>body{min-height:100vh;margin:0;display:grid;place-items:center;background:#faf9f1;color:#0f172a;font:16px/1.5 system-ui,sans-serif}.card{width:min(34rem,calc(100% - 2rem));box-sizing:border-box;padding:2.5rem;border:1px solid #dfe3ea;border-radius:1.5rem;background:#fff;text-align:center;box-shadow:0 24px 70px #0f172a20}h1{margin:0 0 .75rem;color:${color};font-size:2rem}p{margin:0;color:#52607a}@media(prefers-color-scheme:dark){body{background:#111827;color:#f8fafc}.card{background:#182235;border-color:#344258}p{color:#cbd5e1}}</style></head><body><main class="card"><h1>${escapeHtml(options.title)}</h1><p>${escapeHtml(options.message)}</p></main></body></html>`;
|
|
212
140
|
}
|
|
213
141
|
function writePage(response, status, html) {
|
|
214
|
-
response.writeHead(status,
|
|
142
|
+
response.writeHead(status, {
|
|
143
|
+
"cache-control": "no-store",
|
|
144
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
|
145
|
+
"content-type": "text/html; charset=utf-8",
|
|
146
|
+
"referrer-policy": "no-referrer",
|
|
147
|
+
"x-content-type-options": "nosniff"
|
|
148
|
+
});
|
|
215
149
|
response.end(html);
|
|
216
150
|
}
|
|
217
151
|
function secretsMatch(left, right) {
|
|
@@ -219,41 +153,63 @@ function secretsMatch(left, right) {
|
|
|
219
153
|
const rightBytes = Buffer.from(right);
|
|
220
154
|
return leftBytes.length === rightBytes.length && (0, import_node_crypto.timingSafeEqual)(leftBytes, rightBytes);
|
|
221
155
|
}
|
|
222
|
-
function createBrowserAuthorizationRequest(
|
|
156
|
+
function createBrowserAuthorizationRequest(baseUrl) {
|
|
157
|
+
const configuration = (0, import_local_auth.oauthConfiguration)(baseUrl);
|
|
223
158
|
const state = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
224
159
|
const codeVerifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
225
160
|
const codeChallenge = (0, import_node_crypto.createHash)("sha256").update(codeVerifier).digest("base64url");
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
url.searchParams.set("redirect_uri",
|
|
161
|
+
const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
|
|
162
|
+
const url = new URL(`${configuration.issuer}/oauth2/authorize`);
|
|
163
|
+
url.searchParams.set("response_type", "code");
|
|
164
|
+
url.searchParams.set("client_id", configuration.clientId);
|
|
165
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
166
|
+
url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
|
|
167
|
+
url.searchParams.set("resource", configuration.resource);
|
|
231
168
|
url.searchParams.set("state", state);
|
|
232
169
|
url.searchParams.set("code_challenge", codeChallenge);
|
|
233
170
|
url.searchParams.set("code_challenge_method", "S256");
|
|
234
|
-
url.
|
|
235
|
-
"client_name",
|
|
236
|
-
(options.clientName ?? `Rolino CLI on ${(0, import_node_os.hostname)()}`).slice(0, 32)
|
|
237
|
-
);
|
|
238
|
-
url.searchParams.set("request_expires_at", requestExpiresAt);
|
|
239
|
-
return {
|
|
240
|
-
url: url.toString(),
|
|
241
|
-
state,
|
|
242
|
-
codeVerifier,
|
|
243
|
-
codeChallenge,
|
|
244
|
-
requestExpiresAt
|
|
245
|
-
};
|
|
171
|
+
return { configuration, url: url.toString(), redirectUri, state, codeVerifier, codeChallenge };
|
|
246
172
|
}
|
|
247
|
-
var
|
|
173
|
+
var OAuthAuthorizationDeniedError = class extends Error {
|
|
248
174
|
constructor() {
|
|
249
175
|
super("Authorization was denied.");
|
|
250
|
-
this.name = "
|
|
176
|
+
this.name = "OAuthAuthorizationDeniedError";
|
|
251
177
|
}
|
|
252
178
|
};
|
|
179
|
+
async function exchangeAuthorizationCode(options) {
|
|
180
|
+
const { authorization } = options;
|
|
181
|
+
const response = await options.fetch(`${authorization.configuration.issuer}/oauth2/token`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
184
|
+
body: new URLSearchParams({
|
|
185
|
+
grant_type: "authorization_code",
|
|
186
|
+
code: options.code,
|
|
187
|
+
redirect_uri: authorization.redirectUri,
|
|
188
|
+
client_id: authorization.configuration.clientId,
|
|
189
|
+
code_verifier: authorization.codeVerifier,
|
|
190
|
+
resource: authorization.configuration.resource
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
const payload = await response.json().catch(() => null);
|
|
194
|
+
if (!response.ok || !payload || typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string") throw new Error("Rolino could not exchange the OAuth authorization code.");
|
|
195
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
|
|
196
|
+
return {
|
|
197
|
+
host: authorization.configuration.host,
|
|
198
|
+
issuer: authorization.configuration.issuer,
|
|
199
|
+
clientId: authorization.configuration.clientId,
|
|
200
|
+
resource: authorization.configuration.resource,
|
|
201
|
+
accessToken: payload.access_token,
|
|
202
|
+
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
|
|
203
|
+
refreshToken: payload.refresh_token,
|
|
204
|
+
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]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
253
208
|
async function loginWithBrowser(options) {
|
|
209
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
254
210
|
return new Promise((resolve3, reject) => {
|
|
255
211
|
let settled = false;
|
|
256
|
-
|
|
212
|
+
const authorization = createBrowserAuthorizationRequest(options.baseUrl);
|
|
257
213
|
const finish = (callback) => {
|
|
258
214
|
if (settled) return;
|
|
259
215
|
settled = true;
|
|
@@ -263,82 +219,46 @@ async function loginWithBrowser(options) {
|
|
|
263
219
|
};
|
|
264
220
|
const server = (0, import_node_http.createServer)((request, response) => {
|
|
265
221
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
266
|
-
if (request.method !== "GET" || url.pathname !==
|
|
222
|
+
if (request.method !== "GET" || url.pathname !== OAUTH_CALLBACK_PATH) {
|
|
267
223
|
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
268
224
|
response.end("Not found");
|
|
269
225
|
return;
|
|
270
226
|
}
|
|
271
227
|
void (async () => {
|
|
272
|
-
if (!authorization) {
|
|
273
|
-
writePage(response, 503, renderCliAuthorizationResultPage({
|
|
274
|
-
title: "Authorization is starting",
|
|
275
|
-
message: "Return to your terminal and try the browser link again in a moment.",
|
|
276
|
-
success: false
|
|
277
|
-
}));
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
228
|
if (!secretsMatch(url.searchParams.get("state") ?? "", authorization.state)) {
|
|
281
|
-
writePage(response, 400,
|
|
282
|
-
|
|
283
|
-
message: "The callback state did not match. Return to your terminal and try again.",
|
|
284
|
-
success: false
|
|
285
|
-
}));
|
|
286
|
-
finish(() => reject(new Error("The browser callback state did not match.")));
|
|
229
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The callback state did not match.", success: false }));
|
|
230
|
+
finish(() => reject(new Error("The OAuth callback state did not match.")));
|
|
287
231
|
return;
|
|
288
232
|
}
|
|
289
233
|
if (url.searchParams.get("error") === "access_denied") {
|
|
290
|
-
writePage(response, 200,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
234
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Access denied", message: "No OAuth token was saved.", success: false }));
|
|
235
|
+
finish(() => reject(new OAuthAuthorizationDeniedError()));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (url.searchParams.get("iss") !== authorization.configuration.issuer) {
|
|
239
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The authorization server did not match.", success: false }));
|
|
240
|
+
finish(() => reject(new Error("The OAuth callback issuer did not match.")));
|
|
296
241
|
return;
|
|
297
242
|
}
|
|
298
243
|
const code = url.searchParams.get("code");
|
|
299
|
-
if (!code ||
|
|
300
|
-
writePage(response, 400,
|
|
301
|
-
|
|
302
|
-
message: "Rolino did not return a valid one-time code. Return to your terminal and try again.",
|
|
303
|
-
success: false
|
|
304
|
-
}));
|
|
305
|
-
finish(() => reject(new Error("The browser callback did not include a valid code.")));
|
|
244
|
+
if (!code || code.length > 2048) {
|
|
245
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "Rolino did not return a valid code.", success: false }));
|
|
246
|
+
finish(() => reject(new Error("The OAuth callback did not include a valid code.")));
|
|
306
247
|
return;
|
|
307
248
|
}
|
|
308
249
|
try {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
});
|
|
313
|
-
writePage(response, 200, renderCliAuthorizationResultPage({
|
|
314
|
-
title: "Terminal connected",
|
|
315
|
-
message: "Rolino is ready in your terminal. You can close this window.",
|
|
316
|
-
success: true
|
|
317
|
-
}));
|
|
318
|
-
finish(() => resolve3(credential));
|
|
250
|
+
const tokenSet = await exchangeAuthorizationCode({ authorization, code, fetch: fetchImplementation });
|
|
251
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Terminal connected", message: "You can close this window and return to your terminal.", success: true }));
|
|
252
|
+
finish(() => resolve3(tokenSet));
|
|
319
253
|
} catch (error) {
|
|
320
|
-
writePage(response, 502,
|
|
321
|
-
title: "Authorization failed",
|
|
322
|
-
message: "The CLI could not exchange its one-time code. Return to your terminal and try again.",
|
|
323
|
-
success: false
|
|
324
|
-
}));
|
|
254
|
+
writePage(response, 502, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The CLI could not exchange the code.", success: false }));
|
|
325
255
|
finish(() => reject(error));
|
|
326
256
|
}
|
|
327
257
|
})();
|
|
328
258
|
});
|
|
329
259
|
server.once("error", (error) => finish(() => reject(error)));
|
|
330
|
-
server.listen(
|
|
331
|
-
|
|
332
|
-
if (!address || typeof address === "string") {
|
|
333
|
-
finish(() => reject(new Error("Rolino could not start the local authorization callback.")));
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
authorization = createBrowserAuthorizationRequest({
|
|
337
|
-
baseUrl: options.baseUrl,
|
|
338
|
-
redirectUri: `http://127.0.0.1:${address.port}/callback`
|
|
339
|
-
});
|
|
340
|
-
options.stderr.write(`Opening Rolino in your browser\u2026
|
|
341
|
-
`);
|
|
260
|
+
server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
|
|
261
|
+
options.stderr.write("Opening Rolino OAuth sign-in in your browser\u2026\n");
|
|
342
262
|
options.stderr.write(`If it does not open, visit:
|
|
343
263
|
${authorization.url}
|
|
344
264
|
`);
|
|
@@ -346,13 +266,13 @@ ${authorization.url}
|
|
|
346
266
|
void (0, import_open.default)(authorization.url).catch(() => void 0);
|
|
347
267
|
});
|
|
348
268
|
const timeout = setTimeout(() => {
|
|
349
|
-
finish(() => reject(new Error("
|
|
350
|
-
},
|
|
269
|
+
finish(() => reject(new Error("OAuth sign-in timed out after 5 minutes.")));
|
|
270
|
+
}, OAUTH_TIMEOUT_MS);
|
|
351
271
|
});
|
|
352
272
|
}
|
|
353
273
|
|
|
354
274
|
// src/cli.ts
|
|
355
|
-
var
|
|
275
|
+
var import_local_auth2 = require("@rolino/local-auth");
|
|
356
276
|
|
|
357
277
|
// src/exit-codes.ts
|
|
358
278
|
var EXIT_CODES = {
|
|
@@ -634,12 +554,12 @@ var import_node_child_process = require("child_process");
|
|
|
634
554
|
var import_node_fs = require("fs");
|
|
635
555
|
var import_promises = require("fs/promises");
|
|
636
556
|
var import_node_module = require("module");
|
|
637
|
-
var
|
|
557
|
+
var import_node_os = require("os");
|
|
638
558
|
var import_node_path = require("path");
|
|
639
559
|
var CODEX_BEGIN = "# BEGIN ROLINO MCP (managed by rolino setup mcp)";
|
|
640
560
|
var CODEX_END = "# END ROLINO MCP";
|
|
641
561
|
function homeDirectory(env) {
|
|
642
|
-
return env.USERPROFILE || env.HOME || (0,
|
|
562
|
+
return env.USERPROFILE || env.HOME || (0, import_node_os.homedir)();
|
|
643
563
|
}
|
|
644
564
|
function tomlString(value) {
|
|
645
565
|
return JSON.stringify(value);
|
|
@@ -729,12 +649,18 @@ function removeExistingCodexEntry(source, newline) {
|
|
|
729
649
|
return kept.join(newline).trimEnd();
|
|
730
650
|
}
|
|
731
651
|
function codexBlock(server, newline) {
|
|
652
|
+
const configuration = server.transport === "http" ? [
|
|
653
|
+
`url = ${tomlString(server.url)}`,
|
|
654
|
+
`auth = ${tomlString(server.auth)}`
|
|
655
|
+
] : [
|
|
656
|
+
`command = ${tomlString(server.command)}`,
|
|
657
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
658
|
+
`env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`
|
|
659
|
+
];
|
|
732
660
|
return [
|
|
733
661
|
CODEX_BEGIN,
|
|
734
662
|
"[mcp_servers.rolino]",
|
|
735
|
-
|
|
736
|
-
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
737
|
-
`env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`,
|
|
663
|
+
...configuration,
|
|
738
664
|
CODEX_END
|
|
739
665
|
].join(newline);
|
|
740
666
|
}
|
|
@@ -774,7 +700,12 @@ function updateClaudeProjectConfig(source, server) {
|
|
|
774
700
|
...parsed,
|
|
775
701
|
mcpServers: {
|
|
776
702
|
...currentServers,
|
|
777
|
-
rolino: { type: "
|
|
703
|
+
rolino: server.transport === "http" ? { type: "http", url: server.url } : {
|
|
704
|
+
type: "stdio",
|
|
705
|
+
command: server.command,
|
|
706
|
+
args: server.args,
|
|
707
|
+
env: server.env
|
|
708
|
+
}
|
|
778
709
|
}
|
|
779
710
|
}, null, 2)}
|
|
780
711
|
`;
|
|
@@ -806,6 +737,7 @@ async function setupCodex(options, server) {
|
|
|
806
737
|
backupPath,
|
|
807
738
|
changed,
|
|
808
739
|
dryRun: options.dryRun ?? false,
|
|
740
|
+
transport: server.transport,
|
|
809
741
|
server
|
|
810
742
|
};
|
|
811
743
|
}
|
|
@@ -826,6 +758,7 @@ async function setupClaudeCode(options, server) {
|
|
|
826
758
|
backupPath,
|
|
827
759
|
changed,
|
|
828
760
|
dryRun: options.dryRun ?? false,
|
|
761
|
+
transport: server.transport,
|
|
829
762
|
server
|
|
830
763
|
};
|
|
831
764
|
}
|
|
@@ -838,6 +771,7 @@ async function setupClaudeCode(options, server) {
|
|
|
838
771
|
backupPath: null,
|
|
839
772
|
changed: true,
|
|
840
773
|
dryRun: true,
|
|
774
|
+
transport: server.transport,
|
|
841
775
|
server
|
|
842
776
|
};
|
|
843
777
|
}
|
|
@@ -851,7 +785,12 @@ async function setupClaudeCode(options, server) {
|
|
|
851
785
|
"mcp",
|
|
852
786
|
"add-json",
|
|
853
787
|
"rolino",
|
|
854
|
-
JSON.stringify({ type: "
|
|
788
|
+
JSON.stringify(server.transport === "http" ? { type: "http", url: server.url } : {
|
|
789
|
+
type: "stdio",
|
|
790
|
+
command: server.command,
|
|
791
|
+
args: server.args,
|
|
792
|
+
env: server.env
|
|
793
|
+
}),
|
|
855
794
|
"--scope",
|
|
856
795
|
"user"
|
|
857
796
|
], options);
|
|
@@ -867,14 +806,51 @@ async function setupClaudeCode(options, server) {
|
|
|
867
806
|
backupPath: null,
|
|
868
807
|
changed: true,
|
|
869
808
|
dryRun: false,
|
|
809
|
+
transport: server.transport,
|
|
870
810
|
server
|
|
871
811
|
};
|
|
872
812
|
}
|
|
813
|
+
function remoteMcpUrl(baseUrl) {
|
|
814
|
+
return new URL("mcp", `${baseUrl.replace(/\/$/, "")}/`).toString();
|
|
815
|
+
}
|
|
816
|
+
async function advertisedRemoteMcp(options) {
|
|
817
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
818
|
+
try {
|
|
819
|
+
const response = await fetchImplementation(
|
|
820
|
+
new URL("api/v1/meta", `${options.baseUrl.replace(/\/$/, "")}/`),
|
|
821
|
+
{
|
|
822
|
+
headers: { accept: "application/json" },
|
|
823
|
+
signal: AbortSignal.timeout(5e3)
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
if (!response.ok) return false;
|
|
827
|
+
const payload = await response.json();
|
|
828
|
+
return payload.data?.mcp?.streamableHttp === true;
|
|
829
|
+
} catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function resolveTransport(options) {
|
|
834
|
+
const requested = options.transport ?? "stdio";
|
|
835
|
+
if (requested === "stdio") return "stdio";
|
|
836
|
+
if (await advertisedRemoteMcp(options)) return "http";
|
|
837
|
+
if (requested === "http") {
|
|
838
|
+
throw new TypeError(
|
|
839
|
+
"This Rolino instance does not advertise Streamable HTTP MCP. Use --transport stdio, or enable and verify remote MCP on the server."
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return "stdio";
|
|
843
|
+
}
|
|
873
844
|
async function setupMcp(options) {
|
|
874
|
-
const
|
|
875
|
-
const server = {
|
|
845
|
+
const transport = await resolveTransport(options);
|
|
846
|
+
const server = transport === "http" ? {
|
|
847
|
+
transport: "http",
|
|
848
|
+
url: remoteMcpUrl(options.baseUrl),
|
|
849
|
+
auth: "oauth"
|
|
850
|
+
} : {
|
|
851
|
+
transport: "stdio",
|
|
876
852
|
command: options.nodePath,
|
|
877
|
-
args: [
|
|
853
|
+
args: [await resolveServerPath(options)],
|
|
878
854
|
env: { ROLINO_URL: options.baseUrl }
|
|
879
855
|
};
|
|
880
856
|
return options.client === "codex" ? setupCodex(options, server) : setupClaudeCode(options, server);
|
|
@@ -905,40 +881,40 @@ function outputFormat(value) {
|
|
|
905
881
|
throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
|
|
906
882
|
}
|
|
907
883
|
function postStatus(value) {
|
|
908
|
-
const parsed =
|
|
884
|
+
const parsed = import_contracts.PostStatusSchema.safeParse(value.toUpperCase());
|
|
909
885
|
if (parsed.success) return parsed.data;
|
|
910
886
|
throw new import_commander.InvalidArgumentError(
|
|
911
|
-
`Status must be one of ${
|
|
887
|
+
`Status must be one of ${import_contracts.PostStatusSchema.options.join(", ")}.`
|
|
912
888
|
);
|
|
913
889
|
}
|
|
914
890
|
function seoOpportunityKind(value) {
|
|
915
|
-
const parsed =
|
|
891
|
+
const parsed = import_contracts.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
|
|
916
892
|
if (parsed.success) return parsed.data;
|
|
917
|
-
throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${
|
|
893
|
+
throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts.SeoOpportunityKindSchema.options.join(", ")}.`);
|
|
918
894
|
}
|
|
919
895
|
function seoExpectedImpact(value) {
|
|
920
|
-
const parsed =
|
|
896
|
+
const parsed = import_contracts.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
|
|
921
897
|
if (parsed.success) return parsed.data;
|
|
922
898
|
throw new import_commander.InvalidArgumentError("SEO impact must be HIGH, MEDIUM, or LOW.");
|
|
923
899
|
}
|
|
924
900
|
function seoReportCompleteness(value) {
|
|
925
|
-
const parsed =
|
|
901
|
+
const parsed = import_contracts.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
|
|
926
902
|
if (parsed.success) return parsed.data;
|
|
927
903
|
throw new import_commander.InvalidArgumentError("Report status must be COMPLETE or PARTIAL.");
|
|
928
904
|
}
|
|
929
905
|
function projectType(value) {
|
|
930
|
-
const parsed =
|
|
906
|
+
const parsed = import_contracts.ProjectTypeSchema.safeParse(value.toUpperCase());
|
|
931
907
|
if (parsed.success) return parsed.data;
|
|
932
908
|
throw new import_commander.InvalidArgumentError(
|
|
933
|
-
`Type must be one of ${
|
|
909
|
+
`Type must be one of ${import_contracts.ProjectTypeSchema.options.join(", ")}.`
|
|
934
910
|
);
|
|
935
911
|
}
|
|
936
912
|
function publishingProvider(value) {
|
|
937
913
|
const normalized = value.toUpperCase();
|
|
938
|
-
const parsed =
|
|
914
|
+
const parsed = import_contracts.PublishingProviderSchema.safeParse(normalized);
|
|
939
915
|
if (parsed.success) return parsed.data;
|
|
940
916
|
throw new import_commander.InvalidArgumentError(
|
|
941
|
-
`Platform must be one of ${
|
|
917
|
+
`Platform must be one of ${import_contracts.PublishingProviderSchema.options.join(", ")}.`
|
|
942
918
|
);
|
|
943
919
|
}
|
|
944
920
|
function collectPublishingProvider(value, previous) {
|
|
@@ -949,10 +925,10 @@ function collectString(value, previous) {
|
|
|
949
925
|
}
|
|
950
926
|
function deliveryOptionsProvider(value) {
|
|
951
927
|
const normalized = value.toUpperCase();
|
|
952
|
-
const parsed =
|
|
928
|
+
const parsed = import_contracts.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
|
|
953
929
|
if (parsed.success) return parsed.data;
|
|
954
930
|
throw new import_commander.InvalidArgumentError(
|
|
955
|
-
`Provider must be one of ${
|
|
931
|
+
`Provider must be one of ${import_contracts.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
|
|
956
932
|
);
|
|
957
933
|
}
|
|
958
934
|
function tiktokPostMode(value) {
|
|
@@ -990,7 +966,7 @@ function yesOrNo(value) {
|
|
|
990
966
|
function youtubeSettings(options) {
|
|
991
967
|
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;
|
|
992
968
|
if (!hasYouTubeOptions) return null;
|
|
993
|
-
return
|
|
969
|
+
return import_contracts.YouTubePostSettingsSchema.parse({
|
|
994
970
|
title: options.youtubeTitle,
|
|
995
971
|
categoryId: options.youtubeCategoryId,
|
|
996
972
|
privacyStatus: options.youtubePrivacy,
|
|
@@ -1015,6 +991,10 @@ function mcpScope(value) {
|
|
|
1015
991
|
if (value === "user" || value === "project") return value;
|
|
1016
992
|
throw new import_commander.InvalidArgumentError("MCP setup scope must be user or project.");
|
|
1017
993
|
}
|
|
994
|
+
function mcpTransport(value) {
|
|
995
|
+
if (value === "auto" || value === "http" || value === "stdio") return value;
|
|
996
|
+
throw new import_commander.InvalidArgumentError("MCP transport must be auto, http, or stdio.");
|
|
997
|
+
}
|
|
1018
998
|
function isoDateTime(value) {
|
|
1019
999
|
const date = new Date(value);
|
|
1020
1000
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -1053,18 +1033,43 @@ async function requireWriteConsent(options) {
|
|
|
1053
1033
|
);
|
|
1054
1034
|
}
|
|
1055
1035
|
}
|
|
1036
|
+
var BLOG_WEEKDAY_VALUES = {
|
|
1037
|
+
sun: 0,
|
|
1038
|
+
mon: 1,
|
|
1039
|
+
tue: 2,
|
|
1040
|
+
wed: 3,
|
|
1041
|
+
thu: 4,
|
|
1042
|
+
fri: 5,
|
|
1043
|
+
sat: 6
|
|
1044
|
+
};
|
|
1045
|
+
function parseBlogWeekdays(value) {
|
|
1046
|
+
const names = value.split(",").map((day) => day.trim().toLowerCase());
|
|
1047
|
+
const weekdays = names.map((day) => BLOG_WEEKDAY_VALUES[day]).filter((day) => day !== void 0);
|
|
1048
|
+
if (!weekdays.length || weekdays.length !== names.length || new Set(weekdays).size !== weekdays.length) {
|
|
1049
|
+
throw new TypeError("--weekdays must contain unique comma-separated sun,mon,tue,wed,thu,fri,sat values.");
|
|
1050
|
+
}
|
|
1051
|
+
return weekdays;
|
|
1052
|
+
}
|
|
1056
1053
|
function requireBlogExecutionConsent(options) {
|
|
1057
1054
|
if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
|
|
1058
1055
|
throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
|
|
1059
1056
|
}
|
|
1060
1057
|
function formatMcpSetupPreview(result) {
|
|
1058
|
+
const connection = result.server.transport === "http" ? [
|
|
1059
|
+
`Transport: Streamable HTTP`,
|
|
1060
|
+
`Endpoint: ${result.server.url}`,
|
|
1061
|
+
"Authentication: OAuth in the MCP client"
|
|
1062
|
+
] : [
|
|
1063
|
+
"Transport: local STDIO",
|
|
1064
|
+
`Command: ${result.server.command}`,
|
|
1065
|
+
`Arguments: ${result.server.args.join(" ")}`,
|
|
1066
|
+
`Rolino URL: ${result.server.env.ROLINO_URL}`
|
|
1067
|
+
];
|
|
1061
1068
|
return [
|
|
1062
1069
|
`Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
|
|
1063
1070
|
`Scope: ${result.scope}`,
|
|
1064
1071
|
`Target: ${result.target}`,
|
|
1065
|
-
|
|
1066
|
-
`Arguments: ${result.server.args.join(" ")}`,
|
|
1067
|
-
`Rolino URL: ${result.server.env.ROLINO_URL}`,
|
|
1072
|
+
...connection,
|
|
1068
1073
|
"No token will be written to MCP configuration."
|
|
1069
1074
|
].join("\n");
|
|
1070
1075
|
}
|
|
@@ -1083,16 +1088,16 @@ function baseUrlFor(options, runtime) {
|
|
|
1083
1088
|
function clientFor(options, runtime) {
|
|
1084
1089
|
const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
|
|
1085
1090
|
const baseUrl = baseUrlFor(options, runtime);
|
|
1086
|
-
const
|
|
1091
|
+
const token = runtime.env.ROLINO_TOKEN ?? (0, import_local_auth2.createOAuthAccessTokenProvider)({ baseUrl, env: runtime.env, fetch: runtime.fetch });
|
|
1087
1092
|
return new import_sdk.RolinoClient({
|
|
1088
1093
|
baseUrl,
|
|
1089
|
-
token
|
|
1094
|
+
token,
|
|
1090
1095
|
timeoutMs,
|
|
1091
1096
|
fetch: runtime.fetch
|
|
1092
1097
|
});
|
|
1093
1098
|
}
|
|
1094
1099
|
function exitCodeFor(error) {
|
|
1095
|
-
if (error instanceof
|
|
1100
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1096
1101
|
return EXIT_CODES.cancelled;
|
|
1097
1102
|
}
|
|
1098
1103
|
if (error instanceof import_sdk.RolinoNetworkError) {
|
|
@@ -1125,7 +1130,7 @@ function exitCodeFor(error) {
|
|
|
1125
1130
|
return EXIT_CODES.unexpected;
|
|
1126
1131
|
}
|
|
1127
1132
|
function errorForOutput(error) {
|
|
1128
|
-
if (error instanceof
|
|
1133
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1129
1134
|
return { code: "CANCELLED", message: error.message };
|
|
1130
1135
|
}
|
|
1131
1136
|
if (error instanceof import_sdk.RolinoApiError) {
|
|
@@ -1150,7 +1155,7 @@ function errorForOutput(error) {
|
|
|
1150
1155
|
function tiktokSettings(options) {
|
|
1151
1156
|
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;
|
|
1152
1157
|
if (!hasTikTokOptions) return void 0;
|
|
1153
|
-
return
|
|
1158
|
+
return import_contracts.TikTokDraftSettingsSchema.parse({
|
|
1154
1159
|
postMode: options.tiktokMode,
|
|
1155
1160
|
privacyLevel: options.tiktokVisibility,
|
|
1156
1161
|
allowComment: options.tiktokComments,
|
|
@@ -1294,7 +1299,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1294
1299
|
global,
|
|
1295
1300
|
runtime,
|
|
1296
1301
|
async action(context, client) {
|
|
1297
|
-
const parsed =
|
|
1302
|
+
const parsed = import_contracts.ProjectCreateInputSchema.safeParse({
|
|
1298
1303
|
name: local.name,
|
|
1299
1304
|
type: local.type,
|
|
1300
1305
|
websiteUrl: local.website,
|
|
@@ -1386,22 +1391,14 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1386
1391
|
"ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
|
|
1387
1392
|
);
|
|
1388
1393
|
}
|
|
1389
|
-
const
|
|
1390
|
-
if (!meta.authentication.cliBrowserAuthorization) {
|
|
1391
|
-
throw new TypeError(
|
|
1392
|
-
"This Rolino server does not support browser CLI authorization."
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1395
|
-
const credential = await loginWithBrowser({
|
|
1394
|
+
const tokenSet = await loginWithBrowser({
|
|
1396
1395
|
baseUrl: client.baseUrl,
|
|
1397
1396
|
stderr: runtime.stderr,
|
|
1398
|
-
|
|
1399
|
-
requestId: context.requestId
|
|
1400
|
-
})
|
|
1397
|
+
fetch: runtime.fetch
|
|
1401
1398
|
});
|
|
1402
1399
|
const authenticatedClient = new import_sdk.RolinoClient({
|
|
1403
1400
|
baseUrl: client.baseUrl,
|
|
1404
|
-
token:
|
|
1401
|
+
token: tokenSet.accessToken,
|
|
1405
1402
|
timeoutMs: client.timeoutMs,
|
|
1406
1403
|
fetch: runtime.fetch
|
|
1407
1404
|
});
|
|
@@ -1410,12 +1407,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1410
1407
|
const actor = await authenticatedClient.whoami({
|
|
1411
1408
|
requestId: context.requestId
|
|
1412
1409
|
});
|
|
1413
|
-
const path = (0,
|
|
1414
|
-
token: credential.token,
|
|
1415
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1416
|
-
expiresAt: credential.expiresAt,
|
|
1417
|
-
organization: credential.organization
|
|
1418
|
-
}, runtime.env);
|
|
1410
|
+
const path = (0, import_local_auth2.saveOAuthTokenSet)(tokenSet, runtime.env);
|
|
1419
1411
|
credentialSaved = true;
|
|
1420
1412
|
writeSuccess(
|
|
1421
1413
|
context,
|
|
@@ -1424,23 +1416,20 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1424
1416
|
user: actor.user,
|
|
1425
1417
|
organization: actor.organization,
|
|
1426
1418
|
capabilities: actor.capabilities,
|
|
1427
|
-
expiresAt:
|
|
1419
|
+
expiresAt: tokenSet.accessTokenExpiresAt
|
|
1428
1420
|
},
|
|
1429
1421
|
[
|
|
1430
1422
|
`Authenticated as ${actor.user.email}.`,
|
|
1431
1423
|
`Workspace: ${actor.organization.name}`,
|
|
1432
|
-
`
|
|
1424
|
+
`Access token expires: ${tokenSet.accessTokenExpiresAt}`,
|
|
1433
1425
|
`Credential saved in a permission-restricted file at ${path}`
|
|
1434
1426
|
].join("\n"),
|
|
1435
1427
|
["rolino whoami --agent", "rolino projects list --agent"]
|
|
1436
1428
|
);
|
|
1437
1429
|
} catch (error) {
|
|
1438
1430
|
if (credentialSaved) {
|
|
1439
|
-
(0,
|
|
1431
|
+
(0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
|
|
1440
1432
|
}
|
|
1441
|
-
await authenticatedClient.auth.revokeCurrentCredential({
|
|
1442
|
-
requestId: context.requestId
|
|
1443
|
-
}).catch(() => void 0);
|
|
1444
1433
|
throw error;
|
|
1445
1434
|
}
|
|
1446
1435
|
}
|
|
@@ -1453,7 +1442,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1453
1442
|
global,
|
|
1454
1443
|
runtime,
|
|
1455
1444
|
async action(context, client) {
|
|
1456
|
-
const credential = (0,
|
|
1445
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
1457
1446
|
if (credential.source === "none") {
|
|
1458
1447
|
writeSuccess(
|
|
1459
1448
|
context,
|
|
@@ -1472,7 +1461,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1472
1461
|
baseUrl: client.baseUrl,
|
|
1473
1462
|
user: actor.user,
|
|
1474
1463
|
organization: actor.organization,
|
|
1475
|
-
...credential.source === "
|
|
1464
|
+
...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
|
|
1476
1465
|
},
|
|
1477
1466
|
[
|
|
1478
1467
|
`Authenticated as ${actor.user.email}.`,
|
|
@@ -1490,7 +1479,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1490
1479
|
global,
|
|
1491
1480
|
runtime,
|
|
1492
1481
|
async action(context, client) {
|
|
1493
|
-
const credential = (0,
|
|
1482
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
1494
1483
|
if (credential.source === "environment") {
|
|
1495
1484
|
throw new TypeError(
|
|
1496
1485
|
"ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
|
|
@@ -1504,15 +1493,36 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1504
1493
|
);
|
|
1505
1494
|
return;
|
|
1506
1495
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1496
|
+
const tokenSet = credential.tokenSet;
|
|
1497
|
+
const fetchImplementation = runtime.fetch ?? globalThis.fetch;
|
|
1498
|
+
const accessToken = await (0, import_local_auth2.createOAuthAccessTokenProvider)({
|
|
1499
|
+
baseUrl: client.baseUrl,
|
|
1500
|
+
env: runtime.env,
|
|
1501
|
+
fetch: fetchImplementation
|
|
1502
|
+
})();
|
|
1503
|
+
if (accessToken) {
|
|
1504
|
+
const disconnect = await fetchImplementation(`${client.baseUrl}/api/v1/oauth/logout`, {
|
|
1505
|
+
method: "POST",
|
|
1506
|
+
headers: {
|
|
1507
|
+
authorization: `Bearer ${accessToken}`,
|
|
1508
|
+
accept: "application/json",
|
|
1509
|
+
"x-request-id": context.requestId
|
|
1510
|
+
}
|
|
1510
1511
|
});
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1512
|
+
if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
|
|
1513
|
+
throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
|
|
1514
|
+
}
|
|
1514
1515
|
}
|
|
1515
|
-
|
|
1516
|
+
await fetchImplementation(`${tokenSet.issuer}/oauth2/revoke`, {
|
|
1517
|
+
method: "POST",
|
|
1518
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1519
|
+
body: new URLSearchParams({
|
|
1520
|
+
token: tokenSet.refreshToken,
|
|
1521
|
+
token_type_hint: "refresh_token",
|
|
1522
|
+
client_id: tokenSet.clientId
|
|
1523
|
+
})
|
|
1524
|
+
}).catch(() => void 0);
|
|
1525
|
+
(0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
|
|
1516
1526
|
writeSuccess(
|
|
1517
1527
|
context,
|
|
1518
1528
|
{ revoked: true, baseUrl: client.baseUrl },
|
|
@@ -1522,7 +1532,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1522
1532
|
});
|
|
1523
1533
|
});
|
|
1524
1534
|
const setup = program.command("setup").description("Configure local agent tools for Rolino");
|
|
1525
|
-
setup.command("mcp").description("Configure
|
|
1535
|
+
setup.command("mcp").description("Configure Rolino MCP with Streamable HTTP or local STDIO").requiredOption("--client <client>", "codex or claude-code", mcpClient).option("--scope <scope>", "user or project", mcpScope, "user").option("--transport <transport>", "auto, http, or stdio", mcpTransport, "auto").option("--server-path <path>", "absolute or working-directory-relative MCP server path").option("--dry-run", "show the intended configuration without writing it").option("--yes", "apply without an interactive confirmation").option("--force", "replace an existing Claude Code user-scoped Rolino server").action(async (local) => {
|
|
1526
1536
|
const global = program.opts();
|
|
1527
1537
|
commandExitCode = await execute({
|
|
1528
1538
|
command: "setup mcp",
|
|
@@ -1535,7 +1545,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1535
1545
|
cwd: runtime.cwd,
|
|
1536
1546
|
env: runtime.env,
|
|
1537
1547
|
nodePath: runtime.nodePath,
|
|
1538
|
-
cliEntryPath: runtime.cliEntryPath
|
|
1548
|
+
cliEntryPath: runtime.cliEntryPath,
|
|
1549
|
+
fetch: runtime.fetch
|
|
1539
1550
|
};
|
|
1540
1551
|
const preview = await setupMcp({ ...setupOptions, dryRun: true });
|
|
1541
1552
|
let result = preview;
|
|
@@ -1562,11 +1573,12 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1562
1573
|
`${state} Rolino MCP for ${clientLabel}.`,
|
|
1563
1574
|
`Scope: ${result.scope}`,
|
|
1564
1575
|
`Target: ${result.target}`,
|
|
1576
|
+
`Transport: ${result.transport === "http" ? "Streamable HTTP" : "local STDIO"}`,
|
|
1565
1577
|
...result.backupPath && !result.dryRun ? [`Backup: ${result.backupPath}`] : [],
|
|
1566
1578
|
`Rolino URL: ${client.baseUrl}`,
|
|
1567
1579
|
"No token was written to MCP configuration."
|
|
1568
1580
|
].join("\n"),
|
|
1569
|
-
result.client === "codex" ? ["codex mcp
|
|
1581
|
+
result.client === "codex" ? result.transport === "http" ? ["codex mcp login rolino", "codex mcp list"] : ["rolino auth login", "codex mcp list"] : result.transport === "http" ? ["claude mcp get rolino", "Complete OAuth when Claude prompts you"] : ["rolino auth login", "claude mcp get rolino"]
|
|
1570
1582
|
);
|
|
1571
1583
|
}
|
|
1572
1584
|
});
|
|
@@ -1591,7 +1603,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1591
1603
|
});
|
|
1592
1604
|
});
|
|
1593
1605
|
const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
|
|
1594
|
-
posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or
|
|
1606
|
+
posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, LINKEDIN, or GOOGLE_BUSINESS_PROFILE; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "existing project media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--google-business-profile-caption <text>", "Google Business Profile update override; limited to 1,500 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "YouTube tag; repeat as needed", collectString, []).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm creation without an interactive prompt").action(async (local) => {
|
|
1595
1607
|
const global = program.opts();
|
|
1596
1608
|
commandExitCode = await execute({
|
|
1597
1609
|
command: "posts create",
|
|
@@ -1618,7 +1630,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1618
1630
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1619
1631
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1620
1632
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1621
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1633
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1634
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1622
1635
|
},
|
|
1623
1636
|
tiktokSettings: tiktokSettings(local) ?? null,
|
|
1624
1637
|
youtubeSettings: youtubeSettings(local)
|
|
@@ -1636,7 +1649,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1636
1649
|
}
|
|
1637
1650
|
});
|
|
1638
1651
|
});
|
|
1639
|
-
posts.command("update").description("Update selected draft fields using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).option("--caption <text>", "replacement draft caption; omitted fields are preserved").option("--platform <provider>", "replacement destinations; repeat as needed", collectPublishingProvider).option("--clear-platforms", "remove every draft destination").option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString).option("--clear-media", "remove every media asset from the draft").option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "replacement YouTube tag; repeat as needed", collectString).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
|
|
1652
|
+
posts.command("update").description("Update selected draft fields using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).option("--caption <text>", "replacement draft caption; omitted fields are preserved").option("--platform <provider>", "replacement destinations; repeat as needed", collectPublishingProvider).option("--clear-platforms", "remove every draft destination").option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString).option("--clear-media", "remove every media asset from the draft").option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--google-business-profile-caption <text>", "Google Business Profile update override; limited to 1,500 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "replacement YouTube tag; repeat as needed", collectString).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
|
|
1640
1653
|
const global = program.opts();
|
|
1641
1654
|
commandExitCode = await execute({
|
|
1642
1655
|
command: "posts update",
|
|
@@ -1665,7 +1678,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1665
1678
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1666
1679
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1667
1680
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1668
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1681
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1682
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1669
1683
|
};
|
|
1670
1684
|
const resolvedTikTokSettings = tiktokSettings(local);
|
|
1671
1685
|
const resolvedYouTubeSettings = youtubeSettings(local);
|
|
@@ -1813,7 +1827,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1813
1827
|
});
|
|
1814
1828
|
});
|
|
1815
1829
|
const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
|
|
1816
|
-
publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or
|
|
1830
|
+
publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, LINKEDIN, or GOOGLE_BUSINESS_PROFILE; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
|
|
1817
1831
|
const global = program.opts();
|
|
1818
1832
|
commandExitCode = await execute({
|
|
1819
1833
|
command: "posts publish preview",
|
|
@@ -1970,6 +1984,52 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1970
1984
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1971
1985
|
} });
|
|
1972
1986
|
});
|
|
1987
|
+
const blogWebhook = blogPublishing.command("webhook").description("Configure a signed custom Blog publishing webhook");
|
|
1988
|
+
const addWebhookChangeOptions = (command, executeChange) => {
|
|
1989
|
+
command.requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--name <name>", "destination name").requiredOption("--endpoint <https-url>", "public HTTPS receiver URL").requiredOption("--semantics <mode>", "DRAFT or LIVE").option("--destination <destination-id>", "saved destination to update");
|
|
1990
|
+
if (executeChange) command.requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm the exact webhook change");
|
|
1991
|
+
return command;
|
|
1992
|
+
};
|
|
1993
|
+
addWebhookChangeOptions(blogWebhook.command("preview"), false).action(async (local) => {
|
|
1994
|
+
const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
|
|
1995
|
+
const global = program.opts();
|
|
1996
|
+
commandExitCode = await execute({ command: "blog destinations webhook preview", global, runtime, async action(context, client) {
|
|
1997
|
+
const data = await client.blog.publishing.webhook.preview(local.project, input, { requestId: context.requestId });
|
|
1998
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1999
|
+
} });
|
|
2000
|
+
});
|
|
2001
|
+
addWebhookChangeOptions(blogWebhook.command("execute"), true).action(async (local) => {
|
|
2002
|
+
const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
|
|
2003
|
+
const global = program.opts();
|
|
2004
|
+
commandExitCode = await execute({ command: "blog destinations webhook execute", global, runtime, async action(context, client) {
|
|
2005
|
+
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed custom webhook change for project ${local.project}? Store a returned signing secret only in the receiver's server environment. Test the connection before publication.` });
|
|
2006
|
+
const data = await client.blog.publishing.webhook.execute(local.project, { ...input, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2007
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2008
|
+
} });
|
|
2009
|
+
});
|
|
2010
|
+
blogWebhook.command("test").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--destination <destination-id>", "saved destination ID").option("--make-primary", "make a successful LIVE destination primary").action(async (local) => {
|
|
2011
|
+
const global = program.opts();
|
|
2012
|
+
commandExitCode = await execute({ command: "blog destinations webhook test", global, runtime, async action(context, client) {
|
|
2013
|
+
const data = await client.blog.publishing.webhook.test(local.project, { siteId: local.site, destinationId: local.destination, makePrimary: local.makePrimary }, { requestId: context.requestId });
|
|
2014
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2015
|
+
} });
|
|
2016
|
+
});
|
|
2017
|
+
const blogWebhookRotate = blogWebhook.command("rotate").description("Rotate the server-only signing secret");
|
|
2018
|
+
blogWebhookRotate.command("preview").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--destination <destination-id>", "saved destination ID").action(async (local) => {
|
|
2019
|
+
const global = program.opts();
|
|
2020
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate preview", global, runtime, async action(context, client) {
|
|
2021
|
+
const data = await client.blog.publishing.webhook.previewRotation(local.project, { siteId: local.site, destinationId: local.destination }, { requestId: context.requestId });
|
|
2022
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2023
|
+
} });
|
|
2024
|
+
});
|
|
2025
|
+
blogWebhookRotate.command("execute").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--destination <destination-id>", "saved destination ID").requiredOption("--confirmation-token <token>", "short-lived token returned by rotation preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm immediate secret invalidation").action(async (local) => {
|
|
2026
|
+
const global = program.opts();
|
|
2027
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate execute", global, runtime, async action(context, client) {
|
|
2028
|
+
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Rotate the signing secret for destination ${local.destination}? The old secret stops working at once. Store the new secret only in the receiver's server environment, then test before publication.` });
|
|
2029
|
+
const data = await client.blog.publishing.webhook.executeRotation(local.project, { siteId: local.site, destinationId: local.destination, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2030
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2031
|
+
} });
|
|
2032
|
+
});
|
|
1973
2033
|
const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
|
|
1974
2034
|
blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
|
|
1975
2035
|
const global = program.opts();
|
|
@@ -2002,9 +2062,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2002
2062
|
});
|
|
2003
2063
|
const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
|
|
2004
2064
|
blogPlan.command("create").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--start <date>", "local start date as YYYY-MM-DD").requiredOption("--weekdays <days>", "comma-separated weekday names such as mon,wed,fri").option("--time-zone <zone>", "IANA time zone", "UTC").requiredOption("--idempotency-key <key>", "stable retry key").action(async (local) => {
|
|
2005
|
-
const
|
|
2006
|
-
const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
|
|
2007
|
-
if (!weekdays.length || weekdays.length !== local.weekdays.split(",").length) throw new TypeError("--weekdays must contain comma-separated sun,mon,tue,wed,thu,fri,sat values.");
|
|
2065
|
+
const weekdays = parseBlogWeekdays(local.weekdays);
|
|
2008
2066
|
const global = program.opts();
|
|
2009
2067
|
commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
|
|
2010
2068
|
const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
|
|
@@ -2018,6 +2076,25 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2018
2076
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2019
2077
|
} });
|
|
2020
2078
|
});
|
|
2079
|
+
const blogPlanCadence = blogPlan.command("cadence").description("Preview or apply editorial cadence changes without publishing");
|
|
2080
|
+
blogPlanCadence.command("preview").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").requiredOption("--weekdays <days>", "unique comma-separated weekday names such as mon,wed,fri").requiredOption("--expected-plan-version <version>", "version returned by the latest plan read", Number).action(async (local) => {
|
|
2081
|
+
const global = program.opts();
|
|
2082
|
+
commandExitCode = await execute({ command: "blog plan cadence preview", global, runtime, async action(context, client) {
|
|
2083
|
+
const data = await client.blog.plans.previewCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion }, { requestId: context.requestId });
|
|
2084
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2085
|
+
} });
|
|
2086
|
+
});
|
|
2087
|
+
blogPlanCadence.command("apply").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").requiredOption("--weekdays <days>", "unchanged weekdays from cadence preview").requiredOption("--expected-plan-version <version>", "unchanged plan version from cadence preview", Number).requiredOption("--expected-schedule-digest <digest>", "unchanged schedule digest from cadence preview").requiredOption("--confirmation-token <token>", "short-lived token returned by cadence preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm the editorial cadence change").action(async (local) => {
|
|
2088
|
+
const global = program.opts();
|
|
2089
|
+
commandExitCode = await execute({ command: "blog plan cadence apply", global, runtime, async action(context, client) {
|
|
2090
|
+
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed editorial cadence change?
|
|
2091
|
+
Project: ${local.project}
|
|
2092
|
+
Plan: ${local.plan}
|
|
2093
|
+
This moves eligible editorial dates only. It does not schedule, publish, or unpublish an article.` });
|
|
2094
|
+
const data = await client.blog.plans.executeCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion, expectedScheduleDigest: local.expectedScheduleDigest, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2095
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2096
|
+
} });
|
|
2097
|
+
});
|
|
2021
2098
|
blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
|
|
2022
2099
|
const global = program.opts();
|
|
2023
2100
|
commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
|
|
@@ -2098,7 +2175,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2098
2175
|
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) => {
|
|
2099
2176
|
const global = program.opts();
|
|
2100
2177
|
commandExitCode = await execute({ command: "blog articles update", global, runtime, async action(context, client) {
|
|
2101
|
-
const draft =
|
|
2178
|
+
const draft = import_contracts.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
|
|
2102
2179
|
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Save a new immutable Blog draft revision?
|
|
2103
2180
|
Project: ${local.project}
|
|
2104
2181
|
Article: ${articleId}
|
|
@@ -2240,6 +2317,106 @@ Revision: ${local.revision}` });
|
|
|
2240
2317
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2241
2318
|
} });
|
|
2242
2319
|
});
|
|
2320
|
+
const backlinks = program.command("backlinks").description("Review backlink prospects and public contact drafts. Rolino never sends email.");
|
|
2321
|
+
const backlinkTargets = backlinks.command("targets");
|
|
2322
|
+
backlinkTargets.command("list").requiredOption("--project <project-id>").action(async (local) => {
|
|
2323
|
+
const global = program.opts();
|
|
2324
|
+
commandExitCode = await execute({ command: "backlinks targets list", global, runtime, async action(context, client) {
|
|
2325
|
+
const data = await client.backlinks.targets.list(local.project, { requestId: context.requestId });
|
|
2326
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2327
|
+
} });
|
|
2328
|
+
});
|
|
2329
|
+
backlinkTargets.command("add").requiredOption("--project <project-id>").requiredOption("--url <url>").requiredOption("--label <label>").action(async (local) => {
|
|
2330
|
+
const global = program.opts();
|
|
2331
|
+
commandExitCode = await execute({ command: "backlinks targets add", global, runtime, async action(context, client) {
|
|
2332
|
+
const data = await client.backlinks.targets.add(local.project, { url: local.url, label: local.label }, context.requestId, { requestId: context.requestId });
|
|
2333
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2334
|
+
} });
|
|
2335
|
+
});
|
|
2336
|
+
backlinks.command("discover").requiredOption("--project <project-id>").option("--limit <number>", "maximum saved prospects", Number, 20).option("--idempotency-key <key>").action(async (local) => {
|
|
2337
|
+
const global = program.opts();
|
|
2338
|
+
commandExitCode = await execute({ command: "backlinks discover", global, runtime, async action(context, client) {
|
|
2339
|
+
const data = await client.backlinks.discoveries.start(local.project, { limit: local.limit }, local.idempotencyKey ?? context.requestId, { requestId: context.requestId });
|
|
2340
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2341
|
+
} });
|
|
2342
|
+
});
|
|
2343
|
+
const backlinkRuns = backlinks.command("runs");
|
|
2344
|
+
backlinkRuns.command("get").requiredOption("--project <project-id>").requiredOption("--run <run-id>").action(async (local) => {
|
|
2345
|
+
const global = program.opts();
|
|
2346
|
+
commandExitCode = await execute({ command: "backlinks runs get", global, runtime, async action(context, client) {
|
|
2347
|
+
const data = await client.backlinks.discoveries.get(local.project, local.run, { requestId: context.requestId });
|
|
2348
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2349
|
+
} });
|
|
2350
|
+
});
|
|
2351
|
+
const backlinkProspects = backlinks.command("prospects");
|
|
2352
|
+
backlinkProspects.command("list").requiredOption("--project <project-id>").option("--stage <stage>").action(async (local) => {
|
|
2353
|
+
const global = program.opts();
|
|
2354
|
+
commandExitCode = await execute({ command: "backlinks prospects list", global, runtime, async action(context, client) {
|
|
2355
|
+
const stage = local.stage ? import_contracts.BacklinkProspectStageSchema.parse(local.stage.toUpperCase()) : void 0;
|
|
2356
|
+
const data = await client.backlinks.prospects.list(local.project, { stage }, { requestId: context.requestId });
|
|
2357
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2358
|
+
} });
|
|
2359
|
+
});
|
|
2360
|
+
backlinkProspects.command("get").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
|
|
2361
|
+
const global = program.opts();
|
|
2362
|
+
commandExitCode = await execute({ command: "backlinks prospects get", global, runtime, async action(context, client) {
|
|
2363
|
+
const data = await client.backlinks.prospects.get(local.project, local.prospect, { requestId: context.requestId });
|
|
2364
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2365
|
+
} });
|
|
2366
|
+
});
|
|
2367
|
+
backlinkProspects.command("approve").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--expected-version <number>", "current optimistic version", Number).action(async (local) => {
|
|
2368
|
+
const global = program.opts();
|
|
2369
|
+
commandExitCode = await execute({ command: "backlinks prospects approve", global, runtime, async action(context, client) {
|
|
2370
|
+
const data = await client.backlinks.prospects.updateStage(local.project, local.prospect, { stage: "APPROVED", expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2371
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2372
|
+
} });
|
|
2373
|
+
});
|
|
2374
|
+
const backlinkContacts = backlinks.command("contacts");
|
|
2375
|
+
backlinkContacts.command("research").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
|
|
2376
|
+
const global = program.opts();
|
|
2377
|
+
commandExitCode = await execute({ command: "backlinks contacts research", global, runtime, async action(context, client) {
|
|
2378
|
+
const data = await client.backlinks.prospects.researchContact(local.project, local.prospect, context.requestId, { requestId: context.requestId });
|
|
2379
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2380
|
+
} });
|
|
2381
|
+
});
|
|
2382
|
+
backlinkContacts.command("list").requiredOption("--project <project-id>").action(async (local) => {
|
|
2383
|
+
const global = program.opts();
|
|
2384
|
+
commandExitCode = await execute({ command: "backlinks contacts list", global, runtime, async action(context, client) {
|
|
2385
|
+
const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
|
|
2386
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2387
|
+
} });
|
|
2388
|
+
});
|
|
2389
|
+
backlinkContacts.command("export").requiredOption("--project <project-id>").option("--format <format>", "json or csv", "csv").action(async (local) => {
|
|
2390
|
+
const global = program.opts();
|
|
2391
|
+
commandExitCode = await execute({ command: "backlinks contacts export", global, runtime, async action(context, client) {
|
|
2392
|
+
const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
|
|
2393
|
+
if (local.format !== "csv") return writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2394
|
+
const cell = (value) => {
|
|
2395
|
+
let text = String(value ?? "").replace(/[\r\n]+/g, " ");
|
|
2396
|
+
if (/^[=+\-@\t]/.test(text)) text = `'${text}`;
|
|
2397
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
2398
|
+
};
|
|
2399
|
+
const csv = ["prospectId,name,role,email,sourceUrl,checkedAt", ...data.items.map((item) => [item.prospectId, item.name, item.role, item.email, item.sourceUrl, item.checkedAt].map(cell).join(","))].join("\n");
|
|
2400
|
+
context.stdout.write(`${csv}
|
|
2401
|
+
`);
|
|
2402
|
+
} });
|
|
2403
|
+
});
|
|
2404
|
+
const backlinkOutreach = backlinks.command("outreach");
|
|
2405
|
+
backlinkOutreach.command("update").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--file <file>").requiredOption("--expected-version <number>", "current draft version", Number).action(async (local) => {
|
|
2406
|
+
const global = program.opts();
|
|
2407
|
+
commandExitCode = await execute({ command: "backlinks outreach update", global, runtime, async action(context, client) {
|
|
2408
|
+
const payload = JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.file), "utf8"));
|
|
2409
|
+
const data = await client.backlinks.prospects.updateOutreach(local.project, local.prospect, { ...payload, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2410
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2411
|
+
} });
|
|
2412
|
+
});
|
|
2413
|
+
backlinks.command("verify").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--url <url>").requiredOption("--expected-version <number>", "current prospect version", Number).action(async (local) => {
|
|
2414
|
+
const global = program.opts();
|
|
2415
|
+
commandExitCode = await execute({ command: "backlinks verify", global, runtime, async action(context, client) {
|
|
2416
|
+
const data = await client.backlinks.prospects.verify(local.project, local.prospect, { url: local.url, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2417
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2418
|
+
} });
|
|
2419
|
+
});
|
|
2243
2420
|
const seo = program.command("seo").description("Read authorized SEO opportunities and weekly reports");
|
|
2244
2421
|
const seoOpportunities = seo.command("opportunities").description("Read accepted SEO opportunities");
|
|
2245
2422
|
seoOpportunities.command("list").description("List bounded SEO opportunities").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum opportunities to return", (value) => {
|
|
@@ -2337,7 +2514,7 @@ Revision: ${local.revision}` });
|
|
|
2337
2514
|
message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
|
|
2338
2515
|
}
|
|
2339
2516
|
];
|
|
2340
|
-
const credential = (0,
|
|
2517
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
2341
2518
|
if (credential.source !== "none") {
|
|
2342
2519
|
const actor = await client.whoami({ requestId: context.requestId });
|
|
2343
2520
|
checks.push({
|