@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/index.cjs
CHANGED
|
@@ -50,7 +50,7 @@ var import_commander = require("commander");
|
|
|
50
50
|
// package.json
|
|
51
51
|
var package_default = {
|
|
52
52
|
name: "@rolino/cli",
|
|
53
|
-
version: "0.
|
|
53
|
+
version: "0.7.0",
|
|
54
54
|
description: "Agent-friendly command-line interface for Rolino",
|
|
55
55
|
type: "module",
|
|
56
56
|
license: "MIT",
|
|
@@ -107,14 +107,14 @@ var package_default = {
|
|
|
107
107
|
dev: "tsx src/bin.ts"
|
|
108
108
|
},
|
|
109
109
|
dependencies: {
|
|
110
|
-
"@rolino/contracts": "0.
|
|
111
|
-
"@rolino/local-auth": "0.
|
|
112
|
-
"@rolino/sdk": "0.
|
|
110
|
+
"@rolino/contracts": "0.7.0",
|
|
111
|
+
"@rolino/local-auth": "0.7.0",
|
|
112
|
+
"@rolino/sdk": "0.7.0",
|
|
113
113
|
commander: "^15.0.0",
|
|
114
114
|
open: "^11.0.0"
|
|
115
115
|
},
|
|
116
116
|
devDependencies: {
|
|
117
|
-
tsx: "^4.
|
|
117
|
+
tsx: "^4.23.12"
|
|
118
118
|
},
|
|
119
119
|
engines: {
|
|
120
120
|
node: ">=20.19.0"
|
|
@@ -122,23 +122,25 @@ var package_default = {
|
|
|
122
122
|
};
|
|
123
123
|
|
|
124
124
|
// src/cli.ts
|
|
125
|
-
var
|
|
125
|
+
var import_contracts = require("@rolino/contracts");
|
|
126
126
|
var import_sdk = require("@rolino/sdk");
|
|
127
127
|
|
|
128
128
|
// src/browser-login.ts
|
|
129
129
|
var import_node_crypto = require("crypto");
|
|
130
130
|
var import_node_http = require("http");
|
|
131
|
-
var
|
|
132
|
-
var import_contracts = require("@rolino/contracts");
|
|
131
|
+
var import_local_auth = require("@rolino/local-auth");
|
|
133
132
|
var import_open = __toESM(require("open"), 1);
|
|
134
|
-
var
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
var OAUTH_CALLBACK_PORT = 48391;
|
|
134
|
+
var OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
135
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
136
|
+
var DEFAULT_SCOPES = [
|
|
137
|
+
"offline_access",
|
|
138
|
+
"identity:read",
|
|
139
|
+
"projects:read",
|
|
140
|
+
"posts:read",
|
|
141
|
+
"integrations:read",
|
|
142
|
+
"calendar:read"
|
|
143
|
+
];
|
|
142
144
|
function escapeHtml(value) {
|
|
143
145
|
return value.replace(/[&<>"']/g, (character) => ({
|
|
144
146
|
"&": "&",
|
|
@@ -148,86 +150,18 @@ function escapeHtml(value) {
|
|
|
148
150
|
"'": "'"
|
|
149
151
|
})[character]);
|
|
150
152
|
}
|
|
151
|
-
function
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
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>';
|
|
155
|
-
return `<!doctype html>
|
|
156
|
-
<html lang="en">
|
|
157
|
-
<head>
|
|
158
|
-
<meta charset="utf-8">
|
|
159
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
160
|
-
<meta name="color-scheme" content="light">
|
|
161
|
-
<title>${escapeHtml(options.title)} \xB7 Rolino</title>
|
|
162
|
-
<style>
|
|
163
|
-
*{box-sizing:border-box}
|
|
164
|
-
html,body{height:100%;margin:0}
|
|
165
|
-
body{min-height:100svh;overflow:hidden;background:#faf9f1;color:#0f172a;font-family:"Poppins","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif}
|
|
166
|
-
.shell{position:relative;isolation:isolate;display:grid;min-height:100svh;grid-template-rows:auto 1fr auto;padding:clamp(18px,3vw,38px);overflow:hidden}
|
|
167
|
-
.shape{position:absolute;z-index:-1;border-radius:999px;pointer-events:none}
|
|
168
|
-
.shape-a{top:-160px;left:-130px;width:390px;height:390px;background:#facc1530;filter:blur(2px)}
|
|
169
|
-
.shape-b{right:-100px;bottom:-160px;width:360px;height:360px;background:#34c78a20}
|
|
170
|
-
.shape-c{right:9%;top:14%;width:22px;height:22px;background:#ef6a5b;transform:rotate(18deg);border-radius:7px}
|
|
171
|
-
.topbar,.footer{display:flex;width:min(100%,1100px);margin:auto;align-items:center;justify-content:space-between}
|
|
172
|
-
.brand{display:flex;align-items:center;gap:10px;color:#0f172a;font-size:22px;font-weight:800;letter-spacing:-.04em}
|
|
173
|
-
.brand svg{display:block;width:36px;height:36px}
|
|
174
|
-
.secure{display:flex;align-items:center;gap:8px;color:#52607a;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
|
|
175
|
-
.secure-dot{display:block;width:8px;height:8px;border-radius:50%;background:#34c78a;box-shadow:0 0 0 5px #34c78a1f}
|
|
176
|
-
.stage{display:grid;min-height:0;place-items:center;padding:12px 0}
|
|
177
|
-
.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}
|
|
178
|
-
.mascot-wrap{position:relative;display:grid;width:112px;height:96px;margin:0 auto 12px;place-items:center}
|
|
179
|
-
.mascot-wrap:before{position:absolute;width:88px;height:88px;border-radius:50%;background:#fff4a8;content:"";box-shadow:0 0 0 14px #fff9d8}
|
|
180
|
-
.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}
|
|
181
|
-
.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}
|
|
182
|
-
.eyebrow:before{width:7px;height:7px;border-radius:50%;background:#34c78a;content:""}
|
|
183
|
-
.failure .eyebrow{background:#fee6df;color:#9f2f24}
|
|
184
|
-
.failure .eyebrow:before{background:#ef6a5b}
|
|
185
|
-
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}
|
|
186
|
-
.message{max-width:450px;margin:14px auto 0;color:#52607a;font-size:15px;line-height:1.65}
|
|
187
|
-
.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}
|
|
188
|
-
.terminal-bar{display:flex;align-items:center;gap:6px;padding-bottom:12px;color:#8f9bb1;font-size:9px;font-weight:800;letter-spacing:.13em}
|
|
189
|
-
.terminal-bar i{display:block;width:7px;height:7px;border-radius:50%;background:#ef6a5b}
|
|
190
|
-
.terminal-bar i:nth-child(2){background:#facc15}
|
|
191
|
-
.terminal-bar i:nth-child(3){margin-right:5px;background:#34c78a}
|
|
192
|
-
.command{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
|
|
193
|
-
.command b{color:#facc15}
|
|
194
|
-
.terminal-result{display:flex;align-items:center;gap:8px;margin-top:9px;color:#aeb9ca;font-size:12px}
|
|
195
|
-
.terminal-result span{color:#52d49e;font-weight:900}
|
|
196
|
-
.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}
|
|
197
|
-
.return-note>span{font-size:24px}
|
|
198
|
-
.return-note strong,.return-note small{display:block}
|
|
199
|
-
.return-note small{margin-top:3px;color:#875b56}
|
|
200
|
-
.footer{justify-content:center;color:#6f788a;font-size:11px}
|
|
201
|
-
@keyframes arrive{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
|
|
202
|
-
@keyframes mascot-arrive{from{opacity:0;transform:translateY(10px) rotate(-4deg) scale(.88)}to{opacity:1;transform:none}}
|
|
203
|
-
@media(max-width:600px){.secure{display:none}.panel{border-radius:24px}.shape-c{display:none}}
|
|
204
|
-
@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}}
|
|
205
|
-
@media(prefers-reduced-motion:reduce){.panel,.mascot-wrap svg{animation:none}}
|
|
206
|
-
</style>
|
|
207
|
-
</head>
|
|
208
|
-
<body class="${tone}">
|
|
209
|
-
<main class="shell">
|
|
210
|
-
<span class="shape shape-a"></span><span class="shape shape-b"></span><span class="shape shape-c"></span>
|
|
211
|
-
<header class="topbar">
|
|
212
|
-
<div class="brand">${ROLINO_MASCOT_SVG}<span>Rolino</span></div>
|
|
213
|
-
<div class="secure"><span class="secure-dot"></span>Secure CLI authorization</div>
|
|
214
|
-
</header>
|
|
215
|
-
<section class="stage">
|
|
216
|
-
<div class="panel">
|
|
217
|
-
<div class="mascot-wrap">${ROLINO_MASCOT_SVG}</div>
|
|
218
|
-
<p class="eyebrow">${eyebrow}</p>
|
|
219
|
-
<h1>${escapeHtml(options.title)}</h1>
|
|
220
|
-
<p class="message">${escapeHtml(options.message)}</p>
|
|
221
|
-
${detail}
|
|
222
|
-
</div>
|
|
223
|
-
</section>
|
|
224
|
-
<footer class="footer">Secure browser handoff \xB7 no password shared</footer>
|
|
225
|
-
</main>
|
|
226
|
-
</body>
|
|
227
|
-
</html>`;
|
|
153
|
+
function renderOAuthAuthorizationResultPage(options) {
|
|
154
|
+
const color = options.success ? "#087f5b" : "#b42318";
|
|
155
|
+
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>`;
|
|
228
156
|
}
|
|
229
157
|
function writePage(response, status, html) {
|
|
230
|
-
response.writeHead(status,
|
|
158
|
+
response.writeHead(status, {
|
|
159
|
+
"cache-control": "no-store",
|
|
160
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
|
161
|
+
"content-type": "text/html; charset=utf-8",
|
|
162
|
+
"referrer-policy": "no-referrer",
|
|
163
|
+
"x-content-type-options": "nosniff"
|
|
164
|
+
});
|
|
231
165
|
response.end(html);
|
|
232
166
|
}
|
|
233
167
|
function secretsMatch(left, right) {
|
|
@@ -235,41 +169,63 @@ function secretsMatch(left, right) {
|
|
|
235
169
|
const rightBytes = Buffer.from(right);
|
|
236
170
|
return leftBytes.length === rightBytes.length && (0, import_node_crypto.timingSafeEqual)(leftBytes, rightBytes);
|
|
237
171
|
}
|
|
238
|
-
function createBrowserAuthorizationRequest(
|
|
172
|
+
function createBrowserAuthorizationRequest(baseUrl) {
|
|
173
|
+
const configuration = (0, import_local_auth.oauthConfiguration)(baseUrl);
|
|
239
174
|
const state = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
240
175
|
const codeVerifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
241
176
|
const codeChallenge = (0, import_node_crypto.createHash)("sha256").update(codeVerifier).digest("base64url");
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
url.searchParams.set("redirect_uri",
|
|
177
|
+
const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
|
|
178
|
+
const url = new URL(`${configuration.issuer}/oauth2/authorize`);
|
|
179
|
+
url.searchParams.set("response_type", "code");
|
|
180
|
+
url.searchParams.set("client_id", configuration.clientId);
|
|
181
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
182
|
+
url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
|
|
183
|
+
url.searchParams.set("resource", configuration.resource);
|
|
247
184
|
url.searchParams.set("state", state);
|
|
248
185
|
url.searchParams.set("code_challenge", codeChallenge);
|
|
249
186
|
url.searchParams.set("code_challenge_method", "S256");
|
|
250
|
-
url.
|
|
251
|
-
"client_name",
|
|
252
|
-
(options.clientName ?? `Rolino CLI on ${(0, import_node_os.hostname)()}`).slice(0, 32)
|
|
253
|
-
);
|
|
254
|
-
url.searchParams.set("request_expires_at", requestExpiresAt);
|
|
255
|
-
return {
|
|
256
|
-
url: url.toString(),
|
|
257
|
-
state,
|
|
258
|
-
codeVerifier,
|
|
259
|
-
codeChallenge,
|
|
260
|
-
requestExpiresAt
|
|
261
|
-
};
|
|
187
|
+
return { configuration, url: url.toString(), redirectUri, state, codeVerifier, codeChallenge };
|
|
262
188
|
}
|
|
263
|
-
var
|
|
189
|
+
var OAuthAuthorizationDeniedError = class extends Error {
|
|
264
190
|
constructor() {
|
|
265
191
|
super("Authorization was denied.");
|
|
266
|
-
this.name = "
|
|
192
|
+
this.name = "OAuthAuthorizationDeniedError";
|
|
267
193
|
}
|
|
268
194
|
};
|
|
195
|
+
async function exchangeAuthorizationCode(options) {
|
|
196
|
+
const { authorization } = options;
|
|
197
|
+
const response = await options.fetch(`${authorization.configuration.issuer}/oauth2/token`, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
200
|
+
body: new URLSearchParams({
|
|
201
|
+
grant_type: "authorization_code",
|
|
202
|
+
code: options.code,
|
|
203
|
+
redirect_uri: authorization.redirectUri,
|
|
204
|
+
client_id: authorization.configuration.clientId,
|
|
205
|
+
code_verifier: authorization.codeVerifier,
|
|
206
|
+
resource: authorization.configuration.resource
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
const payload = await response.json().catch(() => null);
|
|
210
|
+
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.");
|
|
211
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
|
|
212
|
+
return {
|
|
213
|
+
host: authorization.configuration.host,
|
|
214
|
+
issuer: authorization.configuration.issuer,
|
|
215
|
+
clientId: authorization.configuration.clientId,
|
|
216
|
+
resource: authorization.configuration.resource,
|
|
217
|
+
accessToken: payload.access_token,
|
|
218
|
+
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
|
|
219
|
+
refreshToken: payload.refresh_token,
|
|
220
|
+
refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
221
|
+
scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...DEFAULT_SCOPES]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
269
224
|
async function loginWithBrowser(options) {
|
|
225
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
270
226
|
return new Promise((resolve3, reject) => {
|
|
271
227
|
let settled = false;
|
|
272
|
-
|
|
228
|
+
const authorization = createBrowserAuthorizationRequest(options.baseUrl);
|
|
273
229
|
const finish = (callback) => {
|
|
274
230
|
if (settled) return;
|
|
275
231
|
settled = true;
|
|
@@ -279,82 +235,46 @@ async function loginWithBrowser(options) {
|
|
|
279
235
|
};
|
|
280
236
|
const server = (0, import_node_http.createServer)((request, response) => {
|
|
281
237
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
282
|
-
if (request.method !== "GET" || url.pathname !==
|
|
238
|
+
if (request.method !== "GET" || url.pathname !== OAUTH_CALLBACK_PATH) {
|
|
283
239
|
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
284
240
|
response.end("Not found");
|
|
285
241
|
return;
|
|
286
242
|
}
|
|
287
243
|
void (async () => {
|
|
288
|
-
if (!authorization) {
|
|
289
|
-
writePage(response, 503, renderCliAuthorizationResultPage({
|
|
290
|
-
title: "Authorization is starting",
|
|
291
|
-
message: "Return to your terminal and try the browser link again in a moment.",
|
|
292
|
-
success: false
|
|
293
|
-
}));
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
244
|
if (!secretsMatch(url.searchParams.get("state") ?? "", authorization.state)) {
|
|
297
|
-
writePage(response, 400,
|
|
298
|
-
|
|
299
|
-
message: "The callback state did not match. Return to your terminal and try again.",
|
|
300
|
-
success: false
|
|
301
|
-
}));
|
|
302
|
-
finish(() => reject(new Error("The browser callback state did not match.")));
|
|
245
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The callback state did not match.", success: false }));
|
|
246
|
+
finish(() => reject(new Error("The OAuth callback state did not match.")));
|
|
303
247
|
return;
|
|
304
248
|
}
|
|
305
249
|
if (url.searchParams.get("error") === "access_denied") {
|
|
306
|
-
writePage(response, 200,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
250
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Access denied", message: "No OAuth token was saved.", success: false }));
|
|
251
|
+
finish(() => reject(new OAuthAuthorizationDeniedError()));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (url.searchParams.get("iss") !== authorization.configuration.issuer) {
|
|
255
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The authorization server did not match.", success: false }));
|
|
256
|
+
finish(() => reject(new Error("The OAuth callback issuer did not match.")));
|
|
312
257
|
return;
|
|
313
258
|
}
|
|
314
259
|
const code = url.searchParams.get("code");
|
|
315
|
-
if (!code ||
|
|
316
|
-
writePage(response, 400,
|
|
317
|
-
|
|
318
|
-
message: "Rolino did not return a valid one-time code. Return to your terminal and try again.",
|
|
319
|
-
success: false
|
|
320
|
-
}));
|
|
321
|
-
finish(() => reject(new Error("The browser callback did not include a valid code.")));
|
|
260
|
+
if (!code || code.length > 2048) {
|
|
261
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "Rolino did not return a valid code.", success: false }));
|
|
262
|
+
finish(() => reject(new Error("The OAuth callback did not include a valid code.")));
|
|
322
263
|
return;
|
|
323
264
|
}
|
|
324
265
|
try {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
writePage(response, 200, renderCliAuthorizationResultPage({
|
|
330
|
-
title: "Terminal connected",
|
|
331
|
-
message: "Rolino is ready in your terminal. You can close this window.",
|
|
332
|
-
success: true
|
|
333
|
-
}));
|
|
334
|
-
finish(() => resolve3(credential));
|
|
266
|
+
const tokenSet = await exchangeAuthorizationCode({ authorization, code, fetch: fetchImplementation });
|
|
267
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Terminal connected", message: "You can close this window and return to your terminal.", success: true }));
|
|
268
|
+
finish(() => resolve3(tokenSet));
|
|
335
269
|
} catch (error) {
|
|
336
|
-
writePage(response, 502,
|
|
337
|
-
title: "Authorization failed",
|
|
338
|
-
message: "The CLI could not exchange its one-time code. Return to your terminal and try again.",
|
|
339
|
-
success: false
|
|
340
|
-
}));
|
|
270
|
+
writePage(response, 502, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The CLI could not exchange the code.", success: false }));
|
|
341
271
|
finish(() => reject(error));
|
|
342
272
|
}
|
|
343
273
|
})();
|
|
344
274
|
});
|
|
345
275
|
server.once("error", (error) => finish(() => reject(error)));
|
|
346
|
-
server.listen(
|
|
347
|
-
|
|
348
|
-
if (!address || typeof address === "string") {
|
|
349
|
-
finish(() => reject(new Error("Rolino could not start the local authorization callback.")));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
authorization = createBrowserAuthorizationRequest({
|
|
353
|
-
baseUrl: options.baseUrl,
|
|
354
|
-
redirectUri: `http://127.0.0.1:${address.port}/callback`
|
|
355
|
-
});
|
|
356
|
-
options.stderr.write(`Opening Rolino in your browser\u2026
|
|
357
|
-
`);
|
|
276
|
+
server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
|
|
277
|
+
options.stderr.write("Opening Rolino OAuth sign-in in your browser\u2026\n");
|
|
358
278
|
options.stderr.write(`If it does not open, visit:
|
|
359
279
|
${authorization.url}
|
|
360
280
|
`);
|
|
@@ -362,13 +282,13 @@ ${authorization.url}
|
|
|
362
282
|
void (0, import_open.default)(authorization.url).catch(() => void 0);
|
|
363
283
|
});
|
|
364
284
|
const timeout = setTimeout(() => {
|
|
365
|
-
finish(() => reject(new Error("
|
|
366
|
-
},
|
|
285
|
+
finish(() => reject(new Error("OAuth sign-in timed out after 5 minutes.")));
|
|
286
|
+
}, OAUTH_TIMEOUT_MS);
|
|
367
287
|
});
|
|
368
288
|
}
|
|
369
289
|
|
|
370
290
|
// src/cli.ts
|
|
371
|
-
var
|
|
291
|
+
var import_local_auth2 = require("@rolino/local-auth");
|
|
372
292
|
|
|
373
293
|
// src/exit-codes.ts
|
|
374
294
|
var EXIT_CODES = {
|
|
@@ -650,12 +570,12 @@ var import_node_child_process = require("child_process");
|
|
|
650
570
|
var import_node_fs = require("fs");
|
|
651
571
|
var import_promises = require("fs/promises");
|
|
652
572
|
var import_node_module = require("module");
|
|
653
|
-
var
|
|
573
|
+
var import_node_os = require("os");
|
|
654
574
|
var import_node_path = require("path");
|
|
655
575
|
var CODEX_BEGIN = "# BEGIN ROLINO MCP (managed by rolino setup mcp)";
|
|
656
576
|
var CODEX_END = "# END ROLINO MCP";
|
|
657
577
|
function homeDirectory(env) {
|
|
658
|
-
return env.USERPROFILE || env.HOME || (0,
|
|
578
|
+
return env.USERPROFILE || env.HOME || (0, import_node_os.homedir)();
|
|
659
579
|
}
|
|
660
580
|
function tomlString(value) {
|
|
661
581
|
return JSON.stringify(value);
|
|
@@ -745,12 +665,18 @@ function removeExistingCodexEntry(source, newline) {
|
|
|
745
665
|
return kept.join(newline).trimEnd();
|
|
746
666
|
}
|
|
747
667
|
function codexBlock(server, newline) {
|
|
668
|
+
const configuration = server.transport === "http" ? [
|
|
669
|
+
`url = ${tomlString(server.url)}`,
|
|
670
|
+
`auth = ${tomlString(server.auth)}`
|
|
671
|
+
] : [
|
|
672
|
+
`command = ${tomlString(server.command)}`,
|
|
673
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
674
|
+
`env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`
|
|
675
|
+
];
|
|
748
676
|
return [
|
|
749
677
|
CODEX_BEGIN,
|
|
750
678
|
"[mcp_servers.rolino]",
|
|
751
|
-
|
|
752
|
-
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
753
|
-
`env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`,
|
|
679
|
+
...configuration,
|
|
754
680
|
CODEX_END
|
|
755
681
|
].join(newline);
|
|
756
682
|
}
|
|
@@ -790,7 +716,12 @@ function updateClaudeProjectConfig(source, server) {
|
|
|
790
716
|
...parsed,
|
|
791
717
|
mcpServers: {
|
|
792
718
|
...currentServers,
|
|
793
|
-
rolino: { type: "
|
|
719
|
+
rolino: server.transport === "http" ? { type: "http", url: server.url } : {
|
|
720
|
+
type: "stdio",
|
|
721
|
+
command: server.command,
|
|
722
|
+
args: server.args,
|
|
723
|
+
env: server.env
|
|
724
|
+
}
|
|
794
725
|
}
|
|
795
726
|
}, null, 2)}
|
|
796
727
|
`;
|
|
@@ -822,6 +753,7 @@ async function setupCodex(options, server) {
|
|
|
822
753
|
backupPath,
|
|
823
754
|
changed,
|
|
824
755
|
dryRun: options.dryRun ?? false,
|
|
756
|
+
transport: server.transport,
|
|
825
757
|
server
|
|
826
758
|
};
|
|
827
759
|
}
|
|
@@ -842,6 +774,7 @@ async function setupClaudeCode(options, server) {
|
|
|
842
774
|
backupPath,
|
|
843
775
|
changed,
|
|
844
776
|
dryRun: options.dryRun ?? false,
|
|
777
|
+
transport: server.transport,
|
|
845
778
|
server
|
|
846
779
|
};
|
|
847
780
|
}
|
|
@@ -854,6 +787,7 @@ async function setupClaudeCode(options, server) {
|
|
|
854
787
|
backupPath: null,
|
|
855
788
|
changed: true,
|
|
856
789
|
dryRun: true,
|
|
790
|
+
transport: server.transport,
|
|
857
791
|
server
|
|
858
792
|
};
|
|
859
793
|
}
|
|
@@ -867,7 +801,12 @@ async function setupClaudeCode(options, server) {
|
|
|
867
801
|
"mcp",
|
|
868
802
|
"add-json",
|
|
869
803
|
"rolino",
|
|
870
|
-
JSON.stringify({ type: "
|
|
804
|
+
JSON.stringify(server.transport === "http" ? { type: "http", url: server.url } : {
|
|
805
|
+
type: "stdio",
|
|
806
|
+
command: server.command,
|
|
807
|
+
args: server.args,
|
|
808
|
+
env: server.env
|
|
809
|
+
}),
|
|
871
810
|
"--scope",
|
|
872
811
|
"user"
|
|
873
812
|
], options);
|
|
@@ -883,14 +822,51 @@ async function setupClaudeCode(options, server) {
|
|
|
883
822
|
backupPath: null,
|
|
884
823
|
changed: true,
|
|
885
824
|
dryRun: false,
|
|
825
|
+
transport: server.transport,
|
|
886
826
|
server
|
|
887
827
|
};
|
|
888
828
|
}
|
|
829
|
+
function remoteMcpUrl(baseUrl) {
|
|
830
|
+
return new URL("mcp", `${baseUrl.replace(/\/$/, "")}/`).toString();
|
|
831
|
+
}
|
|
832
|
+
async function advertisedRemoteMcp(options) {
|
|
833
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
834
|
+
try {
|
|
835
|
+
const response = await fetchImplementation(
|
|
836
|
+
new URL("api/v1/meta", `${options.baseUrl.replace(/\/$/, "")}/`),
|
|
837
|
+
{
|
|
838
|
+
headers: { accept: "application/json" },
|
|
839
|
+
signal: AbortSignal.timeout(5e3)
|
|
840
|
+
}
|
|
841
|
+
);
|
|
842
|
+
if (!response.ok) return false;
|
|
843
|
+
const payload = await response.json();
|
|
844
|
+
return payload.data?.mcp?.streamableHttp === true;
|
|
845
|
+
} catch {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function resolveTransport(options) {
|
|
850
|
+
const requested = options.transport ?? "stdio";
|
|
851
|
+
if (requested === "stdio") return "stdio";
|
|
852
|
+
if (await advertisedRemoteMcp(options)) return "http";
|
|
853
|
+
if (requested === "http") {
|
|
854
|
+
throw new TypeError(
|
|
855
|
+
"This Rolino instance does not advertise Streamable HTTP MCP. Use --transport stdio, or enable and verify remote MCP on the server."
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
return "stdio";
|
|
859
|
+
}
|
|
889
860
|
async function setupMcp(options) {
|
|
890
|
-
const
|
|
891
|
-
const server = {
|
|
861
|
+
const transport = await resolveTransport(options);
|
|
862
|
+
const server = transport === "http" ? {
|
|
863
|
+
transport: "http",
|
|
864
|
+
url: remoteMcpUrl(options.baseUrl),
|
|
865
|
+
auth: "oauth"
|
|
866
|
+
} : {
|
|
867
|
+
transport: "stdio",
|
|
892
868
|
command: options.nodePath,
|
|
893
|
-
args: [
|
|
869
|
+
args: [await resolveServerPath(options)],
|
|
894
870
|
env: { ROLINO_URL: options.baseUrl }
|
|
895
871
|
};
|
|
896
872
|
return options.client === "codex" ? setupCodex(options, server) : setupClaudeCode(options, server);
|
|
@@ -921,40 +897,40 @@ function outputFormat(value) {
|
|
|
921
897
|
throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
|
|
922
898
|
}
|
|
923
899
|
function postStatus(value) {
|
|
924
|
-
const parsed =
|
|
900
|
+
const parsed = import_contracts.PostStatusSchema.safeParse(value.toUpperCase());
|
|
925
901
|
if (parsed.success) return parsed.data;
|
|
926
902
|
throw new import_commander.InvalidArgumentError(
|
|
927
|
-
`Status must be one of ${
|
|
903
|
+
`Status must be one of ${import_contracts.PostStatusSchema.options.join(", ")}.`
|
|
928
904
|
);
|
|
929
905
|
}
|
|
930
906
|
function seoOpportunityKind(value) {
|
|
931
|
-
const parsed =
|
|
907
|
+
const parsed = import_contracts.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
|
|
932
908
|
if (parsed.success) return parsed.data;
|
|
933
|
-
throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${
|
|
909
|
+
throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts.SeoOpportunityKindSchema.options.join(", ")}.`);
|
|
934
910
|
}
|
|
935
911
|
function seoExpectedImpact(value) {
|
|
936
|
-
const parsed =
|
|
912
|
+
const parsed = import_contracts.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
|
|
937
913
|
if (parsed.success) return parsed.data;
|
|
938
914
|
throw new import_commander.InvalidArgumentError("SEO impact must be HIGH, MEDIUM, or LOW.");
|
|
939
915
|
}
|
|
940
916
|
function seoReportCompleteness(value) {
|
|
941
|
-
const parsed =
|
|
917
|
+
const parsed = import_contracts.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
|
|
942
918
|
if (parsed.success) return parsed.data;
|
|
943
919
|
throw new import_commander.InvalidArgumentError("Report status must be COMPLETE or PARTIAL.");
|
|
944
920
|
}
|
|
945
921
|
function projectType(value) {
|
|
946
|
-
const parsed =
|
|
922
|
+
const parsed = import_contracts.ProjectTypeSchema.safeParse(value.toUpperCase());
|
|
947
923
|
if (parsed.success) return parsed.data;
|
|
948
924
|
throw new import_commander.InvalidArgumentError(
|
|
949
|
-
`Type must be one of ${
|
|
925
|
+
`Type must be one of ${import_contracts.ProjectTypeSchema.options.join(", ")}.`
|
|
950
926
|
);
|
|
951
927
|
}
|
|
952
928
|
function publishingProvider(value) {
|
|
953
929
|
const normalized = value.toUpperCase();
|
|
954
|
-
const parsed =
|
|
930
|
+
const parsed = import_contracts.PublishingProviderSchema.safeParse(normalized);
|
|
955
931
|
if (parsed.success) return parsed.data;
|
|
956
932
|
throw new import_commander.InvalidArgumentError(
|
|
957
|
-
`Platform must be one of ${
|
|
933
|
+
`Platform must be one of ${import_contracts.PublishingProviderSchema.options.join(", ")}.`
|
|
958
934
|
);
|
|
959
935
|
}
|
|
960
936
|
function collectPublishingProvider(value, previous) {
|
|
@@ -965,10 +941,10 @@ function collectString(value, previous) {
|
|
|
965
941
|
}
|
|
966
942
|
function deliveryOptionsProvider(value) {
|
|
967
943
|
const normalized = value.toUpperCase();
|
|
968
|
-
const parsed =
|
|
944
|
+
const parsed = import_contracts.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
|
|
969
945
|
if (parsed.success) return parsed.data;
|
|
970
946
|
throw new import_commander.InvalidArgumentError(
|
|
971
|
-
`Provider must be one of ${
|
|
947
|
+
`Provider must be one of ${import_contracts.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
|
|
972
948
|
);
|
|
973
949
|
}
|
|
974
950
|
function tiktokPostMode(value) {
|
|
@@ -1006,7 +982,7 @@ function yesOrNo(value) {
|
|
|
1006
982
|
function youtubeSettings(options) {
|
|
1007
983
|
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;
|
|
1008
984
|
if (!hasYouTubeOptions) return null;
|
|
1009
|
-
return
|
|
985
|
+
return import_contracts.YouTubePostSettingsSchema.parse({
|
|
1010
986
|
title: options.youtubeTitle,
|
|
1011
987
|
categoryId: options.youtubeCategoryId,
|
|
1012
988
|
privacyStatus: options.youtubePrivacy,
|
|
@@ -1031,6 +1007,10 @@ function mcpScope(value) {
|
|
|
1031
1007
|
if (value === "user" || value === "project") return value;
|
|
1032
1008
|
throw new import_commander.InvalidArgumentError("MCP setup scope must be user or project.");
|
|
1033
1009
|
}
|
|
1010
|
+
function mcpTransport(value) {
|
|
1011
|
+
if (value === "auto" || value === "http" || value === "stdio") return value;
|
|
1012
|
+
throw new import_commander.InvalidArgumentError("MCP transport must be auto, http, or stdio.");
|
|
1013
|
+
}
|
|
1034
1014
|
function isoDateTime(value) {
|
|
1035
1015
|
const date = new Date(value);
|
|
1036
1016
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -1069,18 +1049,43 @@ async function requireWriteConsent(options) {
|
|
|
1069
1049
|
);
|
|
1070
1050
|
}
|
|
1071
1051
|
}
|
|
1052
|
+
var BLOG_WEEKDAY_VALUES = {
|
|
1053
|
+
sun: 0,
|
|
1054
|
+
mon: 1,
|
|
1055
|
+
tue: 2,
|
|
1056
|
+
wed: 3,
|
|
1057
|
+
thu: 4,
|
|
1058
|
+
fri: 5,
|
|
1059
|
+
sat: 6
|
|
1060
|
+
};
|
|
1061
|
+
function parseBlogWeekdays(value) {
|
|
1062
|
+
const names = value.split(",").map((day) => day.trim().toLowerCase());
|
|
1063
|
+
const weekdays = names.map((day) => BLOG_WEEKDAY_VALUES[day]).filter((day) => day !== void 0);
|
|
1064
|
+
if (!weekdays.length || weekdays.length !== names.length || new Set(weekdays).size !== weekdays.length) {
|
|
1065
|
+
throw new TypeError("--weekdays must contain unique comma-separated sun,mon,tue,wed,thu,fri,sat values.");
|
|
1066
|
+
}
|
|
1067
|
+
return weekdays;
|
|
1068
|
+
}
|
|
1072
1069
|
function requireBlogExecutionConsent(options) {
|
|
1073
1070
|
if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
|
|
1074
1071
|
throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
|
|
1075
1072
|
}
|
|
1076
1073
|
function formatMcpSetupPreview(result) {
|
|
1074
|
+
const connection = result.server.transport === "http" ? [
|
|
1075
|
+
`Transport: Streamable HTTP`,
|
|
1076
|
+
`Endpoint: ${result.server.url}`,
|
|
1077
|
+
"Authentication: OAuth in the MCP client"
|
|
1078
|
+
] : [
|
|
1079
|
+
"Transport: local STDIO",
|
|
1080
|
+
`Command: ${result.server.command}`,
|
|
1081
|
+
`Arguments: ${result.server.args.join(" ")}`,
|
|
1082
|
+
`Rolino URL: ${result.server.env.ROLINO_URL}`
|
|
1083
|
+
];
|
|
1077
1084
|
return [
|
|
1078
1085
|
`Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
|
|
1079
1086
|
`Scope: ${result.scope}`,
|
|
1080
1087
|
`Target: ${result.target}`,
|
|
1081
|
-
|
|
1082
|
-
`Arguments: ${result.server.args.join(" ")}`,
|
|
1083
|
-
`Rolino URL: ${result.server.env.ROLINO_URL}`,
|
|
1088
|
+
...connection,
|
|
1084
1089
|
"No token will be written to MCP configuration."
|
|
1085
1090
|
].join("\n");
|
|
1086
1091
|
}
|
|
@@ -1099,16 +1104,16 @@ function baseUrlFor(options, runtime) {
|
|
|
1099
1104
|
function clientFor(options, runtime) {
|
|
1100
1105
|
const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
|
|
1101
1106
|
const baseUrl = baseUrlFor(options, runtime);
|
|
1102
|
-
const
|
|
1107
|
+
const token = runtime.env.ROLINO_TOKEN ?? (0, import_local_auth2.createOAuthAccessTokenProvider)({ baseUrl, env: runtime.env, fetch: runtime.fetch });
|
|
1103
1108
|
return new import_sdk.RolinoClient({
|
|
1104
1109
|
baseUrl,
|
|
1105
|
-
token
|
|
1110
|
+
token,
|
|
1106
1111
|
timeoutMs,
|
|
1107
1112
|
fetch: runtime.fetch
|
|
1108
1113
|
});
|
|
1109
1114
|
}
|
|
1110
1115
|
function exitCodeFor(error) {
|
|
1111
|
-
if (error instanceof
|
|
1116
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1112
1117
|
return EXIT_CODES.cancelled;
|
|
1113
1118
|
}
|
|
1114
1119
|
if (error instanceof import_sdk.RolinoNetworkError) {
|
|
@@ -1141,7 +1146,7 @@ function exitCodeFor(error) {
|
|
|
1141
1146
|
return EXIT_CODES.unexpected;
|
|
1142
1147
|
}
|
|
1143
1148
|
function errorForOutput(error) {
|
|
1144
|
-
if (error instanceof
|
|
1149
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1145
1150
|
return { code: "CANCELLED", message: error.message };
|
|
1146
1151
|
}
|
|
1147
1152
|
if (error instanceof import_sdk.RolinoApiError) {
|
|
@@ -1166,7 +1171,7 @@ function errorForOutput(error) {
|
|
|
1166
1171
|
function tiktokSettings(options) {
|
|
1167
1172
|
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;
|
|
1168
1173
|
if (!hasTikTokOptions) return void 0;
|
|
1169
|
-
return
|
|
1174
|
+
return import_contracts.TikTokDraftSettingsSchema.parse({
|
|
1170
1175
|
postMode: options.tiktokMode,
|
|
1171
1176
|
privacyLevel: options.tiktokVisibility,
|
|
1172
1177
|
allowComment: options.tiktokComments,
|
|
@@ -1310,7 +1315,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1310
1315
|
global,
|
|
1311
1316
|
runtime,
|
|
1312
1317
|
async action(context, client) {
|
|
1313
|
-
const parsed =
|
|
1318
|
+
const parsed = import_contracts.ProjectCreateInputSchema.safeParse({
|
|
1314
1319
|
name: local.name,
|
|
1315
1320
|
type: local.type,
|
|
1316
1321
|
websiteUrl: local.website,
|
|
@@ -1402,22 +1407,14 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1402
1407
|
"ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
|
|
1403
1408
|
);
|
|
1404
1409
|
}
|
|
1405
|
-
const
|
|
1406
|
-
if (!meta.authentication.cliBrowserAuthorization) {
|
|
1407
|
-
throw new TypeError(
|
|
1408
|
-
"This Rolino server does not support browser CLI authorization."
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
const credential = await loginWithBrowser({
|
|
1410
|
+
const tokenSet = await loginWithBrowser({
|
|
1412
1411
|
baseUrl: client.baseUrl,
|
|
1413
1412
|
stderr: runtime.stderr,
|
|
1414
|
-
|
|
1415
|
-
requestId: context.requestId
|
|
1416
|
-
})
|
|
1413
|
+
fetch: runtime.fetch
|
|
1417
1414
|
});
|
|
1418
1415
|
const authenticatedClient = new import_sdk.RolinoClient({
|
|
1419
1416
|
baseUrl: client.baseUrl,
|
|
1420
|
-
token:
|
|
1417
|
+
token: tokenSet.accessToken,
|
|
1421
1418
|
timeoutMs: client.timeoutMs,
|
|
1422
1419
|
fetch: runtime.fetch
|
|
1423
1420
|
});
|
|
@@ -1426,12 +1423,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1426
1423
|
const actor = await authenticatedClient.whoami({
|
|
1427
1424
|
requestId: context.requestId
|
|
1428
1425
|
});
|
|
1429
|
-
const path = (0,
|
|
1430
|
-
token: credential.token,
|
|
1431
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1432
|
-
expiresAt: credential.expiresAt,
|
|
1433
|
-
organization: credential.organization
|
|
1434
|
-
}, runtime.env);
|
|
1426
|
+
const path = (0, import_local_auth2.saveOAuthTokenSet)(tokenSet, runtime.env);
|
|
1435
1427
|
credentialSaved = true;
|
|
1436
1428
|
writeSuccess(
|
|
1437
1429
|
context,
|
|
@@ -1440,23 +1432,20 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1440
1432
|
user: actor.user,
|
|
1441
1433
|
organization: actor.organization,
|
|
1442
1434
|
capabilities: actor.capabilities,
|
|
1443
|
-
expiresAt:
|
|
1435
|
+
expiresAt: tokenSet.accessTokenExpiresAt
|
|
1444
1436
|
},
|
|
1445
1437
|
[
|
|
1446
1438
|
`Authenticated as ${actor.user.email}.`,
|
|
1447
1439
|
`Workspace: ${actor.organization.name}`,
|
|
1448
|
-
`
|
|
1440
|
+
`Access token expires: ${tokenSet.accessTokenExpiresAt}`,
|
|
1449
1441
|
`Credential saved in a permission-restricted file at ${path}`
|
|
1450
1442
|
].join("\n"),
|
|
1451
1443
|
["rolino whoami --agent", "rolino projects list --agent"]
|
|
1452
1444
|
);
|
|
1453
1445
|
} catch (error) {
|
|
1454
1446
|
if (credentialSaved) {
|
|
1455
|
-
(0,
|
|
1447
|
+
(0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
|
|
1456
1448
|
}
|
|
1457
|
-
await authenticatedClient.auth.revokeCurrentCredential({
|
|
1458
|
-
requestId: context.requestId
|
|
1459
|
-
}).catch(() => void 0);
|
|
1460
1449
|
throw error;
|
|
1461
1450
|
}
|
|
1462
1451
|
}
|
|
@@ -1469,7 +1458,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1469
1458
|
global,
|
|
1470
1459
|
runtime,
|
|
1471
1460
|
async action(context, client) {
|
|
1472
|
-
const credential = (0,
|
|
1461
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
1473
1462
|
if (credential.source === "none") {
|
|
1474
1463
|
writeSuccess(
|
|
1475
1464
|
context,
|
|
@@ -1488,7 +1477,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1488
1477
|
baseUrl: client.baseUrl,
|
|
1489
1478
|
user: actor.user,
|
|
1490
1479
|
organization: actor.organization,
|
|
1491
|
-
...credential.source === "
|
|
1480
|
+
...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
|
|
1492
1481
|
},
|
|
1493
1482
|
[
|
|
1494
1483
|
`Authenticated as ${actor.user.email}.`,
|
|
@@ -1506,7 +1495,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1506
1495
|
global,
|
|
1507
1496
|
runtime,
|
|
1508
1497
|
async action(context, client) {
|
|
1509
|
-
const credential = (0,
|
|
1498
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
1510
1499
|
if (credential.source === "environment") {
|
|
1511
1500
|
throw new TypeError(
|
|
1512
1501
|
"ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
|
|
@@ -1520,15 +1509,36 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1520
1509
|
);
|
|
1521
1510
|
return;
|
|
1522
1511
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1512
|
+
const tokenSet = credential.tokenSet;
|
|
1513
|
+
const fetchImplementation = runtime.fetch ?? globalThis.fetch;
|
|
1514
|
+
const accessToken = await (0, import_local_auth2.createOAuthAccessTokenProvider)({
|
|
1515
|
+
baseUrl: client.baseUrl,
|
|
1516
|
+
env: runtime.env,
|
|
1517
|
+
fetch: fetchImplementation
|
|
1518
|
+
})();
|
|
1519
|
+
if (accessToken) {
|
|
1520
|
+
const disconnect = await fetchImplementation(`${client.baseUrl}/api/v1/oauth/logout`, {
|
|
1521
|
+
method: "POST",
|
|
1522
|
+
headers: {
|
|
1523
|
+
authorization: `Bearer ${accessToken}`,
|
|
1524
|
+
accept: "application/json",
|
|
1525
|
+
"x-request-id": context.requestId
|
|
1526
|
+
}
|
|
1526
1527
|
});
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1528
|
+
if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
|
|
1529
|
+
throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
|
|
1530
|
+
}
|
|
1530
1531
|
}
|
|
1531
|
-
|
|
1532
|
+
await fetchImplementation(`${tokenSet.issuer}/oauth2/revoke`, {
|
|
1533
|
+
method: "POST",
|
|
1534
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1535
|
+
body: new URLSearchParams({
|
|
1536
|
+
token: tokenSet.refreshToken,
|
|
1537
|
+
token_type_hint: "refresh_token",
|
|
1538
|
+
client_id: tokenSet.clientId
|
|
1539
|
+
})
|
|
1540
|
+
}).catch(() => void 0);
|
|
1541
|
+
(0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
|
|
1532
1542
|
writeSuccess(
|
|
1533
1543
|
context,
|
|
1534
1544
|
{ revoked: true, baseUrl: client.baseUrl },
|
|
@@ -1538,7 +1548,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1538
1548
|
});
|
|
1539
1549
|
});
|
|
1540
1550
|
const setup = program.command("setup").description("Configure local agent tools for Rolino");
|
|
1541
|
-
setup.command("mcp").description("Configure
|
|
1551
|
+
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) => {
|
|
1542
1552
|
const global = program.opts();
|
|
1543
1553
|
commandExitCode = await execute({
|
|
1544
1554
|
command: "setup mcp",
|
|
@@ -1551,7 +1561,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1551
1561
|
cwd: runtime.cwd,
|
|
1552
1562
|
env: runtime.env,
|
|
1553
1563
|
nodePath: runtime.nodePath,
|
|
1554
|
-
cliEntryPath: runtime.cliEntryPath
|
|
1564
|
+
cliEntryPath: runtime.cliEntryPath,
|
|
1565
|
+
fetch: runtime.fetch
|
|
1555
1566
|
};
|
|
1556
1567
|
const preview = await setupMcp({ ...setupOptions, dryRun: true });
|
|
1557
1568
|
let result = preview;
|
|
@@ -1578,11 +1589,12 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1578
1589
|
`${state} Rolino MCP for ${clientLabel}.`,
|
|
1579
1590
|
`Scope: ${result.scope}`,
|
|
1580
1591
|
`Target: ${result.target}`,
|
|
1592
|
+
`Transport: ${result.transport === "http" ? "Streamable HTTP" : "local STDIO"}`,
|
|
1581
1593
|
...result.backupPath && !result.dryRun ? [`Backup: ${result.backupPath}`] : [],
|
|
1582
1594
|
`Rolino URL: ${client.baseUrl}`,
|
|
1583
1595
|
"No token was written to MCP configuration."
|
|
1584
1596
|
].join("\n"),
|
|
1585
|
-
result.client === "codex" ? ["codex mcp
|
|
1597
|
+
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"]
|
|
1586
1598
|
);
|
|
1587
1599
|
}
|
|
1588
1600
|
});
|
|
@@ -1607,7 +1619,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1607
1619
|
});
|
|
1608
1620
|
});
|
|
1609
1621
|
const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
|
|
1610
|
-
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
|
|
1622
|
+
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) => {
|
|
1611
1623
|
const global = program.opts();
|
|
1612
1624
|
commandExitCode = await execute({
|
|
1613
1625
|
command: "posts create",
|
|
@@ -1634,7 +1646,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1634
1646
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1635
1647
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1636
1648
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1637
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1649
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1650
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1638
1651
|
},
|
|
1639
1652
|
tiktokSettings: tiktokSettings(local) ?? null,
|
|
1640
1653
|
youtubeSettings: youtubeSettings(local)
|
|
@@ -1652,7 +1665,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1652
1665
|
}
|
|
1653
1666
|
});
|
|
1654
1667
|
});
|
|
1655
|
-
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) => {
|
|
1668
|
+
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) => {
|
|
1656
1669
|
const global = program.opts();
|
|
1657
1670
|
commandExitCode = await execute({
|
|
1658
1671
|
command: "posts update",
|
|
@@ -1681,7 +1694,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1681
1694
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1682
1695
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1683
1696
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1684
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1697
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1698
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1685
1699
|
};
|
|
1686
1700
|
const resolvedTikTokSettings = tiktokSettings(local);
|
|
1687
1701
|
const resolvedYouTubeSettings = youtubeSettings(local);
|
|
@@ -1829,7 +1843,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1829
1843
|
});
|
|
1830
1844
|
});
|
|
1831
1845
|
const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
|
|
1832
|
-
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
|
|
1846
|
+
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) => {
|
|
1833
1847
|
const global = program.opts();
|
|
1834
1848
|
commandExitCode = await execute({
|
|
1835
1849
|
command: "posts publish preview",
|
|
@@ -1986,6 +2000,52 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1986
2000
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1987
2001
|
} });
|
|
1988
2002
|
});
|
|
2003
|
+
const blogWebhook = blogPublishing.command("webhook").description("Configure a signed custom Blog publishing webhook");
|
|
2004
|
+
const addWebhookChangeOptions = (command, executeChange) => {
|
|
2005
|
+
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");
|
|
2006
|
+
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");
|
|
2007
|
+
return command;
|
|
2008
|
+
};
|
|
2009
|
+
addWebhookChangeOptions(blogWebhook.command("preview"), false).action(async (local) => {
|
|
2010
|
+
const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
|
|
2011
|
+
const global = program.opts();
|
|
2012
|
+
commandExitCode = await execute({ command: "blog destinations webhook preview", global, runtime, async action(context, client) {
|
|
2013
|
+
const data = await client.blog.publishing.webhook.preview(local.project, input, { requestId: context.requestId });
|
|
2014
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2015
|
+
} });
|
|
2016
|
+
});
|
|
2017
|
+
addWebhookChangeOptions(blogWebhook.command("execute"), true).action(async (local) => {
|
|
2018
|
+
const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
|
|
2019
|
+
const global = program.opts();
|
|
2020
|
+
commandExitCode = await execute({ command: "blog destinations webhook execute", global, runtime, async action(context, client) {
|
|
2021
|
+
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.` });
|
|
2022
|
+
const data = await client.blog.publishing.webhook.execute(local.project, { ...input, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2023
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2024
|
+
} });
|
|
2025
|
+
});
|
|
2026
|
+
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) => {
|
|
2027
|
+
const global = program.opts();
|
|
2028
|
+
commandExitCode = await execute({ command: "blog destinations webhook test", global, runtime, async action(context, client) {
|
|
2029
|
+
const data = await client.blog.publishing.webhook.test(local.project, { siteId: local.site, destinationId: local.destination, makePrimary: local.makePrimary }, { requestId: context.requestId });
|
|
2030
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2031
|
+
} });
|
|
2032
|
+
});
|
|
2033
|
+
const blogWebhookRotate = blogWebhook.command("rotate").description("Rotate the server-only signing secret");
|
|
2034
|
+
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) => {
|
|
2035
|
+
const global = program.opts();
|
|
2036
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate preview", global, runtime, async action(context, client) {
|
|
2037
|
+
const data = await client.blog.publishing.webhook.previewRotation(local.project, { siteId: local.site, destinationId: local.destination }, { requestId: context.requestId });
|
|
2038
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2039
|
+
} });
|
|
2040
|
+
});
|
|
2041
|
+
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) => {
|
|
2042
|
+
const global = program.opts();
|
|
2043
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate execute", global, runtime, async action(context, client) {
|
|
2044
|
+
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.` });
|
|
2045
|
+
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 });
|
|
2046
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2047
|
+
} });
|
|
2048
|
+
});
|
|
1989
2049
|
const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
|
|
1990
2050
|
blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
|
|
1991
2051
|
const global = program.opts();
|
|
@@ -2018,9 +2078,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2018
2078
|
});
|
|
2019
2079
|
const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
|
|
2020
2080
|
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) => {
|
|
2021
|
-
const
|
|
2022
|
-
const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
|
|
2023
|
-
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.");
|
|
2081
|
+
const weekdays = parseBlogWeekdays(local.weekdays);
|
|
2024
2082
|
const global = program.opts();
|
|
2025
2083
|
commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
|
|
2026
2084
|
const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
|
|
@@ -2034,6 +2092,25 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2034
2092
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2035
2093
|
} });
|
|
2036
2094
|
});
|
|
2095
|
+
const blogPlanCadence = blogPlan.command("cadence").description("Preview or apply editorial cadence changes without publishing");
|
|
2096
|
+
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) => {
|
|
2097
|
+
const global = program.opts();
|
|
2098
|
+
commandExitCode = await execute({ command: "blog plan cadence preview", global, runtime, async action(context, client) {
|
|
2099
|
+
const data = await client.blog.plans.previewCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion }, { requestId: context.requestId });
|
|
2100
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2101
|
+
} });
|
|
2102
|
+
});
|
|
2103
|
+
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) => {
|
|
2104
|
+
const global = program.opts();
|
|
2105
|
+
commandExitCode = await execute({ command: "blog plan cadence apply", global, runtime, async action(context, client) {
|
|
2106
|
+
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed editorial cadence change?
|
|
2107
|
+
Project: ${local.project}
|
|
2108
|
+
Plan: ${local.plan}
|
|
2109
|
+
This moves eligible editorial dates only. It does not schedule, publish, or unpublish an article.` });
|
|
2110
|
+
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 });
|
|
2111
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2112
|
+
} });
|
|
2113
|
+
});
|
|
2037
2114
|
blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
|
|
2038
2115
|
const global = program.opts();
|
|
2039
2116
|
commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
|
|
@@ -2114,7 +2191,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2114
2191
|
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) => {
|
|
2115
2192
|
const global = program.opts();
|
|
2116
2193
|
commandExitCode = await execute({ command: "blog articles update", global, runtime, async action(context, client) {
|
|
2117
|
-
const draft =
|
|
2194
|
+
const draft = import_contracts.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
|
|
2118
2195
|
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Save a new immutable Blog draft revision?
|
|
2119
2196
|
Project: ${local.project}
|
|
2120
2197
|
Article: ${articleId}
|
|
@@ -2256,6 +2333,106 @@ Revision: ${local.revision}` });
|
|
|
2256
2333
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2257
2334
|
} });
|
|
2258
2335
|
});
|
|
2336
|
+
const backlinks = program.command("backlinks").description("Review backlink prospects and public contact drafts. Rolino never sends email.");
|
|
2337
|
+
const backlinkTargets = backlinks.command("targets");
|
|
2338
|
+
backlinkTargets.command("list").requiredOption("--project <project-id>").action(async (local) => {
|
|
2339
|
+
const global = program.opts();
|
|
2340
|
+
commandExitCode = await execute({ command: "backlinks targets list", global, runtime, async action(context, client) {
|
|
2341
|
+
const data = await client.backlinks.targets.list(local.project, { requestId: context.requestId });
|
|
2342
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2343
|
+
} });
|
|
2344
|
+
});
|
|
2345
|
+
backlinkTargets.command("add").requiredOption("--project <project-id>").requiredOption("--url <url>").requiredOption("--label <label>").action(async (local) => {
|
|
2346
|
+
const global = program.opts();
|
|
2347
|
+
commandExitCode = await execute({ command: "backlinks targets add", global, runtime, async action(context, client) {
|
|
2348
|
+
const data = await client.backlinks.targets.add(local.project, { url: local.url, label: local.label }, context.requestId, { requestId: context.requestId });
|
|
2349
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2350
|
+
} });
|
|
2351
|
+
});
|
|
2352
|
+
backlinks.command("discover").requiredOption("--project <project-id>").option("--limit <number>", "maximum saved prospects", Number, 20).option("--idempotency-key <key>").action(async (local) => {
|
|
2353
|
+
const global = program.opts();
|
|
2354
|
+
commandExitCode = await execute({ command: "backlinks discover", global, runtime, async action(context, client) {
|
|
2355
|
+
const data = await client.backlinks.discoveries.start(local.project, { limit: local.limit }, local.idempotencyKey ?? context.requestId, { requestId: context.requestId });
|
|
2356
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2357
|
+
} });
|
|
2358
|
+
});
|
|
2359
|
+
const backlinkRuns = backlinks.command("runs");
|
|
2360
|
+
backlinkRuns.command("get").requiredOption("--project <project-id>").requiredOption("--run <run-id>").action(async (local) => {
|
|
2361
|
+
const global = program.opts();
|
|
2362
|
+
commandExitCode = await execute({ command: "backlinks runs get", global, runtime, async action(context, client) {
|
|
2363
|
+
const data = await client.backlinks.discoveries.get(local.project, local.run, { requestId: context.requestId });
|
|
2364
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2365
|
+
} });
|
|
2366
|
+
});
|
|
2367
|
+
const backlinkProspects = backlinks.command("prospects");
|
|
2368
|
+
backlinkProspects.command("list").requiredOption("--project <project-id>").option("--stage <stage>").action(async (local) => {
|
|
2369
|
+
const global = program.opts();
|
|
2370
|
+
commandExitCode = await execute({ command: "backlinks prospects list", global, runtime, async action(context, client) {
|
|
2371
|
+
const stage = local.stage ? import_contracts.BacklinkProspectStageSchema.parse(local.stage.toUpperCase()) : void 0;
|
|
2372
|
+
const data = await client.backlinks.prospects.list(local.project, { stage }, { requestId: context.requestId });
|
|
2373
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2374
|
+
} });
|
|
2375
|
+
});
|
|
2376
|
+
backlinkProspects.command("get").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
|
|
2377
|
+
const global = program.opts();
|
|
2378
|
+
commandExitCode = await execute({ command: "backlinks prospects get", global, runtime, async action(context, client) {
|
|
2379
|
+
const data = await client.backlinks.prospects.get(local.project, local.prospect, { requestId: context.requestId });
|
|
2380
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2381
|
+
} });
|
|
2382
|
+
});
|
|
2383
|
+
backlinkProspects.command("approve").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--expected-version <number>", "current optimistic version", Number).action(async (local) => {
|
|
2384
|
+
const global = program.opts();
|
|
2385
|
+
commandExitCode = await execute({ command: "backlinks prospects approve", global, runtime, async action(context, client) {
|
|
2386
|
+
const data = await client.backlinks.prospects.updateStage(local.project, local.prospect, { stage: "APPROVED", expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2387
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2388
|
+
} });
|
|
2389
|
+
});
|
|
2390
|
+
const backlinkContacts = backlinks.command("contacts");
|
|
2391
|
+
backlinkContacts.command("research").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
|
|
2392
|
+
const global = program.opts();
|
|
2393
|
+
commandExitCode = await execute({ command: "backlinks contacts research", global, runtime, async action(context, client) {
|
|
2394
|
+
const data = await client.backlinks.prospects.researchContact(local.project, local.prospect, context.requestId, { requestId: context.requestId });
|
|
2395
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2396
|
+
} });
|
|
2397
|
+
});
|
|
2398
|
+
backlinkContacts.command("list").requiredOption("--project <project-id>").action(async (local) => {
|
|
2399
|
+
const global = program.opts();
|
|
2400
|
+
commandExitCode = await execute({ command: "backlinks contacts list", global, runtime, async action(context, client) {
|
|
2401
|
+
const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
|
|
2402
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2403
|
+
} });
|
|
2404
|
+
});
|
|
2405
|
+
backlinkContacts.command("export").requiredOption("--project <project-id>").option("--format <format>", "json or csv", "csv").action(async (local) => {
|
|
2406
|
+
const global = program.opts();
|
|
2407
|
+
commandExitCode = await execute({ command: "backlinks contacts export", global, runtime, async action(context, client) {
|
|
2408
|
+
const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
|
|
2409
|
+
if (local.format !== "csv") return writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2410
|
+
const cell = (value) => {
|
|
2411
|
+
let text = String(value ?? "").replace(/[\r\n]+/g, " ");
|
|
2412
|
+
if (/^[=+\-@\t]/.test(text)) text = `'${text}`;
|
|
2413
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
2414
|
+
};
|
|
2415
|
+
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");
|
|
2416
|
+
context.stdout.write(`${csv}
|
|
2417
|
+
`);
|
|
2418
|
+
} });
|
|
2419
|
+
});
|
|
2420
|
+
const backlinkOutreach = backlinks.command("outreach");
|
|
2421
|
+
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) => {
|
|
2422
|
+
const global = program.opts();
|
|
2423
|
+
commandExitCode = await execute({ command: "backlinks outreach update", global, runtime, async action(context, client) {
|
|
2424
|
+
const payload = JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.file), "utf8"));
|
|
2425
|
+
const data = await client.backlinks.prospects.updateOutreach(local.project, local.prospect, { ...payload, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2426
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2427
|
+
} });
|
|
2428
|
+
});
|
|
2429
|
+
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) => {
|
|
2430
|
+
const global = program.opts();
|
|
2431
|
+
commandExitCode = await execute({ command: "backlinks verify", global, runtime, async action(context, client) {
|
|
2432
|
+
const data = await client.backlinks.prospects.verify(local.project, local.prospect, { url: local.url, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2433
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2434
|
+
} });
|
|
2435
|
+
});
|
|
2259
2436
|
const seo = program.command("seo").description("Read authorized SEO opportunities and weekly reports");
|
|
2260
2437
|
const seoOpportunities = seo.command("opportunities").description("Read accepted SEO opportunities");
|
|
2261
2438
|
seoOpportunities.command("list").description("List bounded SEO opportunities").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum opportunities to return", (value) => {
|
|
@@ -2353,7 +2530,7 @@ Revision: ${local.revision}` });
|
|
|
2353
2530
|
message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
|
|
2354
2531
|
}
|
|
2355
2532
|
];
|
|
2356
|
-
const credential = (0,
|
|
2533
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
2357
2534
|
if (credential.source !== "none") {
|
|
2358
2535
|
const actor = await client.whoami({ requestId: context.requestId });
|
|
2359
2536
|
checks.push({
|