@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
|
@@ -123,12 +123,18 @@ function removeExistingCodexEntry(source, newline) {
|
|
|
123
123
|
return kept.join(newline).trimEnd();
|
|
124
124
|
}
|
|
125
125
|
function codexBlock(server, newline) {
|
|
126
|
+
const configuration = server.transport === "http" ? [
|
|
127
|
+
`url = ${tomlString(server.url)}`,
|
|
128
|
+
`auth = ${tomlString(server.auth)}`
|
|
129
|
+
] : [
|
|
130
|
+
`command = ${tomlString(server.command)}`,
|
|
131
|
+
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
132
|
+
`env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`
|
|
133
|
+
];
|
|
126
134
|
return [
|
|
127
135
|
CODEX_BEGIN,
|
|
128
136
|
"[mcp_servers.rolino]",
|
|
129
|
-
|
|
130
|
-
`args = [${server.args.map(tomlString).join(", ")}]`,
|
|
131
|
-
`env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`,
|
|
137
|
+
...configuration,
|
|
132
138
|
CODEX_END
|
|
133
139
|
].join(newline);
|
|
134
140
|
}
|
|
@@ -168,7 +174,12 @@ function updateClaudeProjectConfig(source, server) {
|
|
|
168
174
|
...parsed,
|
|
169
175
|
mcpServers: {
|
|
170
176
|
...currentServers,
|
|
171
|
-
rolino: { type: "
|
|
177
|
+
rolino: server.transport === "http" ? { type: "http", url: server.url } : {
|
|
178
|
+
type: "stdio",
|
|
179
|
+
command: server.command,
|
|
180
|
+
args: server.args,
|
|
181
|
+
env: server.env
|
|
182
|
+
}
|
|
172
183
|
}
|
|
173
184
|
}, null, 2)}
|
|
174
185
|
`;
|
|
@@ -200,6 +211,7 @@ async function setupCodex(options, server) {
|
|
|
200
211
|
backupPath,
|
|
201
212
|
changed,
|
|
202
213
|
dryRun: options.dryRun ?? false,
|
|
214
|
+
transport: server.transport,
|
|
203
215
|
server
|
|
204
216
|
};
|
|
205
217
|
}
|
|
@@ -220,6 +232,7 @@ async function setupClaudeCode(options, server) {
|
|
|
220
232
|
backupPath,
|
|
221
233
|
changed,
|
|
222
234
|
dryRun: options.dryRun ?? false,
|
|
235
|
+
transport: server.transport,
|
|
223
236
|
server
|
|
224
237
|
};
|
|
225
238
|
}
|
|
@@ -232,6 +245,7 @@ async function setupClaudeCode(options, server) {
|
|
|
232
245
|
backupPath: null,
|
|
233
246
|
changed: true,
|
|
234
247
|
dryRun: true,
|
|
248
|
+
transport: server.transport,
|
|
235
249
|
server
|
|
236
250
|
};
|
|
237
251
|
}
|
|
@@ -245,7 +259,12 @@ async function setupClaudeCode(options, server) {
|
|
|
245
259
|
"mcp",
|
|
246
260
|
"add-json",
|
|
247
261
|
"rolino",
|
|
248
|
-
JSON.stringify({ type: "
|
|
262
|
+
JSON.stringify(server.transport === "http" ? { type: "http", url: server.url } : {
|
|
263
|
+
type: "stdio",
|
|
264
|
+
command: server.command,
|
|
265
|
+
args: server.args,
|
|
266
|
+
env: server.env
|
|
267
|
+
}),
|
|
249
268
|
"--scope",
|
|
250
269
|
"user"
|
|
251
270
|
], options);
|
|
@@ -261,14 +280,51 @@ async function setupClaudeCode(options, server) {
|
|
|
261
280
|
backupPath: null,
|
|
262
281
|
changed: true,
|
|
263
282
|
dryRun: false,
|
|
283
|
+
transport: server.transport,
|
|
264
284
|
server
|
|
265
285
|
};
|
|
266
286
|
}
|
|
287
|
+
function remoteMcpUrl(baseUrl) {
|
|
288
|
+
return new URL("mcp", `${baseUrl.replace(/\/$/, "")}/`).toString();
|
|
289
|
+
}
|
|
290
|
+
async function advertisedRemoteMcp(options) {
|
|
291
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetchImplementation(
|
|
294
|
+
new URL("api/v1/meta", `${options.baseUrl.replace(/\/$/, "")}/`),
|
|
295
|
+
{
|
|
296
|
+
headers: { accept: "application/json" },
|
|
297
|
+
signal: AbortSignal.timeout(5e3)
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
if (!response.ok) return false;
|
|
301
|
+
const payload = await response.json();
|
|
302
|
+
return payload.data?.mcp?.streamableHttp === true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function resolveTransport(options) {
|
|
308
|
+
const requested = options.transport ?? "stdio";
|
|
309
|
+
if (requested === "stdio") return "stdio";
|
|
310
|
+
if (await advertisedRemoteMcp(options)) return "http";
|
|
311
|
+
if (requested === "http") {
|
|
312
|
+
throw new TypeError(
|
|
313
|
+
"This Rolino instance does not advertise Streamable HTTP MCP. Use --transport stdio, or enable and verify remote MCP on the server."
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return "stdio";
|
|
317
|
+
}
|
|
267
318
|
async function setupMcp(options) {
|
|
268
|
-
const
|
|
269
|
-
const server = {
|
|
319
|
+
const transport = await resolveTransport(options);
|
|
320
|
+
const server = transport === "http" ? {
|
|
321
|
+
transport: "http",
|
|
322
|
+
url: remoteMcpUrl(options.baseUrl),
|
|
323
|
+
auth: "oauth"
|
|
324
|
+
} : {
|
|
325
|
+
transport: "stdio",
|
|
270
326
|
command: options.nodePath,
|
|
271
|
-
args: [
|
|
327
|
+
args: [await resolveServerPath(options)],
|
|
272
328
|
env: { ROLINO_URL: options.baseUrl }
|
|
273
329
|
};
|
|
274
330
|
return options.client === "codex" ? setupCodex(options, server) : setupClaudeCode(options, server);
|
|
@@ -285,7 +341,7 @@ import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
|
285
341
|
// package.json
|
|
286
342
|
var package_default = {
|
|
287
343
|
name: "@rolino/cli",
|
|
288
|
-
version: "0.
|
|
344
|
+
version: "0.7.0",
|
|
289
345
|
description: "Agent-friendly command-line interface for Rolino",
|
|
290
346
|
type: "module",
|
|
291
347
|
license: "MIT",
|
|
@@ -342,14 +398,14 @@ var package_default = {
|
|
|
342
398
|
dev: "tsx src/bin.ts"
|
|
343
399
|
},
|
|
344
400
|
dependencies: {
|
|
345
|
-
"@rolino/contracts": "0.
|
|
346
|
-
"@rolino/local-auth": "0.
|
|
347
|
-
"@rolino/sdk": "0.
|
|
401
|
+
"@rolino/contracts": "0.7.0",
|
|
402
|
+
"@rolino/local-auth": "0.7.0",
|
|
403
|
+
"@rolino/sdk": "0.7.0",
|
|
348
404
|
commander: "^15.0.0",
|
|
349
405
|
open: "^11.0.0"
|
|
350
406
|
},
|
|
351
407
|
devDependencies: {
|
|
352
|
-
tsx: "^4.
|
|
408
|
+
tsx: "^4.23.12"
|
|
353
409
|
},
|
|
354
410
|
engines: {
|
|
355
411
|
node: ">=20.19.0"
|
|
@@ -360,6 +416,7 @@ var package_default = {
|
|
|
360
416
|
import {
|
|
361
417
|
PostStatusSchema,
|
|
362
418
|
AgentBlogDraftUpdateSchema,
|
|
419
|
+
BacklinkProspectStageSchema,
|
|
363
420
|
ProjectCreateInputSchema,
|
|
364
421
|
ProjectTypeSchema,
|
|
365
422
|
ProviderDeliveryOptionsProviderSchema,
|
|
@@ -381,19 +438,21 @@ import {
|
|
|
381
438
|
// src/browser-login.ts
|
|
382
439
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
383
440
|
import { createServer } from "http";
|
|
384
|
-
import { hostname } from "os";
|
|
385
441
|
import {
|
|
386
|
-
|
|
387
|
-
} from "@rolino/
|
|
442
|
+
oauthConfiguration
|
|
443
|
+
} from "@rolino/local-auth";
|
|
388
444
|
import open from "open";
|
|
389
|
-
var
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
|
|
396
|
-
|
|
445
|
+
var OAUTH_CALLBACK_PORT = 48391;
|
|
446
|
+
var OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
447
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
448
|
+
var DEFAULT_SCOPES = [
|
|
449
|
+
"offline_access",
|
|
450
|
+
"identity:read",
|
|
451
|
+
"projects:read",
|
|
452
|
+
"posts:read",
|
|
453
|
+
"integrations:read",
|
|
454
|
+
"calendar:read"
|
|
455
|
+
];
|
|
397
456
|
function escapeHtml(value) {
|
|
398
457
|
return value.replace(/[&<>"']/g, (character) => ({
|
|
399
458
|
"&": "&",
|
|
@@ -403,86 +462,18 @@ function escapeHtml(value) {
|
|
|
403
462
|
"'": "'"
|
|
404
463
|
})[character]);
|
|
405
464
|
}
|
|
406
|
-
function
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
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>';
|
|
410
|
-
return `<!doctype html>
|
|
411
|
-
<html lang="en">
|
|
412
|
-
<head>
|
|
413
|
-
<meta charset="utf-8">
|
|
414
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
415
|
-
<meta name="color-scheme" content="light">
|
|
416
|
-
<title>${escapeHtml(options.title)} \xB7 Rolino</title>
|
|
417
|
-
<style>
|
|
418
|
-
*{box-sizing:border-box}
|
|
419
|
-
html,body{height:100%;margin:0}
|
|
420
|
-
body{min-height:100svh;overflow:hidden;background:#faf9f1;color:#0f172a;font-family:"Poppins","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif}
|
|
421
|
-
.shell{position:relative;isolation:isolate;display:grid;min-height:100svh;grid-template-rows:auto 1fr auto;padding:clamp(18px,3vw,38px);overflow:hidden}
|
|
422
|
-
.shape{position:absolute;z-index:-1;border-radius:999px;pointer-events:none}
|
|
423
|
-
.shape-a{top:-160px;left:-130px;width:390px;height:390px;background:#facc1530;filter:blur(2px)}
|
|
424
|
-
.shape-b{right:-100px;bottom:-160px;width:360px;height:360px;background:#34c78a20}
|
|
425
|
-
.shape-c{right:9%;top:14%;width:22px;height:22px;background:#ef6a5b;transform:rotate(18deg);border-radius:7px}
|
|
426
|
-
.topbar,.footer{display:flex;width:min(100%,1100px);margin:auto;align-items:center;justify-content:space-between}
|
|
427
|
-
.brand{display:flex;align-items:center;gap:10px;color:#0f172a;font-size:22px;font-weight:800;letter-spacing:-.04em}
|
|
428
|
-
.brand svg{display:block;width:36px;height:36px}
|
|
429
|
-
.secure{display:flex;align-items:center;gap:8px;color:#52607a;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
|
|
430
|
-
.secure-dot{display:block;width:8px;height:8px;border-radius:50%;background:#34c78a;box-shadow:0 0 0 5px #34c78a1f}
|
|
431
|
-
.stage{display:grid;min-height:0;place-items:center;padding:12px 0}
|
|
432
|
-
.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}
|
|
433
|
-
.mascot-wrap{position:relative;display:grid;width:112px;height:96px;margin:0 auto 12px;place-items:center}
|
|
434
|
-
.mascot-wrap:before{position:absolute;width:88px;height:88px;border-radius:50%;background:#fff4a8;content:"";box-shadow:0 0 0 14px #fff9d8}
|
|
435
|
-
.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}
|
|
436
|
-
.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}
|
|
437
|
-
.eyebrow:before{width:7px;height:7px;border-radius:50%;background:#34c78a;content:""}
|
|
438
|
-
.failure .eyebrow{background:#fee6df;color:#9f2f24}
|
|
439
|
-
.failure .eyebrow:before{background:#ef6a5b}
|
|
440
|
-
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}
|
|
441
|
-
.message{max-width:450px;margin:14px auto 0;color:#52607a;font-size:15px;line-height:1.65}
|
|
442
|
-
.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}
|
|
443
|
-
.terminal-bar{display:flex;align-items:center;gap:6px;padding-bottom:12px;color:#8f9bb1;font-size:9px;font-weight:800;letter-spacing:.13em}
|
|
444
|
-
.terminal-bar i{display:block;width:7px;height:7px;border-radius:50%;background:#ef6a5b}
|
|
445
|
-
.terminal-bar i:nth-child(2){background:#facc15}
|
|
446
|
-
.terminal-bar i:nth-child(3){margin-right:5px;background:#34c78a}
|
|
447
|
-
.command{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
|
|
448
|
-
.command b{color:#facc15}
|
|
449
|
-
.terminal-result{display:flex;align-items:center;gap:8px;margin-top:9px;color:#aeb9ca;font-size:12px}
|
|
450
|
-
.terminal-result span{color:#52d49e;font-weight:900}
|
|
451
|
-
.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}
|
|
452
|
-
.return-note>span{font-size:24px}
|
|
453
|
-
.return-note strong,.return-note small{display:block}
|
|
454
|
-
.return-note small{margin-top:3px;color:#875b56}
|
|
455
|
-
.footer{justify-content:center;color:#6f788a;font-size:11px}
|
|
456
|
-
@keyframes arrive{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
|
|
457
|
-
@keyframes mascot-arrive{from{opacity:0;transform:translateY(10px) rotate(-4deg) scale(.88)}to{opacity:1;transform:none}}
|
|
458
|
-
@media(max-width:600px){.secure{display:none}.panel{border-radius:24px}.shape-c{display:none}}
|
|
459
|
-
@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}}
|
|
460
|
-
@media(prefers-reduced-motion:reduce){.panel,.mascot-wrap svg{animation:none}}
|
|
461
|
-
</style>
|
|
462
|
-
</head>
|
|
463
|
-
<body class="${tone}">
|
|
464
|
-
<main class="shell">
|
|
465
|
-
<span class="shape shape-a"></span><span class="shape shape-b"></span><span class="shape shape-c"></span>
|
|
466
|
-
<header class="topbar">
|
|
467
|
-
<div class="brand">${ROLINO_MASCOT_SVG}<span>Rolino</span></div>
|
|
468
|
-
<div class="secure"><span class="secure-dot"></span>Secure CLI authorization</div>
|
|
469
|
-
</header>
|
|
470
|
-
<section class="stage">
|
|
471
|
-
<div class="panel">
|
|
472
|
-
<div class="mascot-wrap">${ROLINO_MASCOT_SVG}</div>
|
|
473
|
-
<p class="eyebrow">${eyebrow}</p>
|
|
474
|
-
<h1>${escapeHtml(options.title)}</h1>
|
|
475
|
-
<p class="message">${escapeHtml(options.message)}</p>
|
|
476
|
-
${detail}
|
|
477
|
-
</div>
|
|
478
|
-
</section>
|
|
479
|
-
<footer class="footer">Secure browser handoff \xB7 no password shared</footer>
|
|
480
|
-
</main>
|
|
481
|
-
</body>
|
|
482
|
-
</html>`;
|
|
465
|
+
function renderOAuthAuthorizationResultPage(options) {
|
|
466
|
+
const color = options.success ? "#087f5b" : "#b42318";
|
|
467
|
+
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>`;
|
|
483
468
|
}
|
|
484
469
|
function writePage(response, status, html) {
|
|
485
|
-
response.writeHead(status,
|
|
470
|
+
response.writeHead(status, {
|
|
471
|
+
"cache-control": "no-store",
|
|
472
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
|
473
|
+
"content-type": "text/html; charset=utf-8",
|
|
474
|
+
"referrer-policy": "no-referrer",
|
|
475
|
+
"x-content-type-options": "nosniff"
|
|
476
|
+
});
|
|
486
477
|
response.end(html);
|
|
487
478
|
}
|
|
488
479
|
function secretsMatch(left, right) {
|
|
@@ -490,41 +481,63 @@ function secretsMatch(left, right) {
|
|
|
490
481
|
const rightBytes = Buffer.from(right);
|
|
491
482
|
return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
|
|
492
483
|
}
|
|
493
|
-
function createBrowserAuthorizationRequest(
|
|
484
|
+
function createBrowserAuthorizationRequest(baseUrl) {
|
|
485
|
+
const configuration = oauthConfiguration(baseUrl);
|
|
494
486
|
const state = randomBytes(32).toString("base64url");
|
|
495
487
|
const codeVerifier = randomBytes(32).toString("base64url");
|
|
496
488
|
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
url.searchParams.set("redirect_uri",
|
|
489
|
+
const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
|
|
490
|
+
const url = new URL(`${configuration.issuer}/oauth2/authorize`);
|
|
491
|
+
url.searchParams.set("response_type", "code");
|
|
492
|
+
url.searchParams.set("client_id", configuration.clientId);
|
|
493
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
494
|
+
url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
|
|
495
|
+
url.searchParams.set("resource", configuration.resource);
|
|
502
496
|
url.searchParams.set("state", state);
|
|
503
497
|
url.searchParams.set("code_challenge", codeChallenge);
|
|
504
498
|
url.searchParams.set("code_challenge_method", "S256");
|
|
505
|
-
url.
|
|
506
|
-
"client_name",
|
|
507
|
-
(options.clientName ?? `Rolino CLI on ${hostname()}`).slice(0, 32)
|
|
508
|
-
);
|
|
509
|
-
url.searchParams.set("request_expires_at", requestExpiresAt);
|
|
510
|
-
return {
|
|
511
|
-
url: url.toString(),
|
|
512
|
-
state,
|
|
513
|
-
codeVerifier,
|
|
514
|
-
codeChallenge,
|
|
515
|
-
requestExpiresAt
|
|
516
|
-
};
|
|
499
|
+
return { configuration, url: url.toString(), redirectUri, state, codeVerifier, codeChallenge };
|
|
517
500
|
}
|
|
518
|
-
var
|
|
501
|
+
var OAuthAuthorizationDeniedError = class extends Error {
|
|
519
502
|
constructor() {
|
|
520
503
|
super("Authorization was denied.");
|
|
521
|
-
this.name = "
|
|
504
|
+
this.name = "OAuthAuthorizationDeniedError";
|
|
522
505
|
}
|
|
523
506
|
};
|
|
507
|
+
async function exchangeAuthorizationCode(options) {
|
|
508
|
+
const { authorization } = options;
|
|
509
|
+
const response = await options.fetch(`${authorization.configuration.issuer}/oauth2/token`, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
512
|
+
body: new URLSearchParams({
|
|
513
|
+
grant_type: "authorization_code",
|
|
514
|
+
code: options.code,
|
|
515
|
+
redirect_uri: authorization.redirectUri,
|
|
516
|
+
client_id: authorization.configuration.clientId,
|
|
517
|
+
code_verifier: authorization.codeVerifier,
|
|
518
|
+
resource: authorization.configuration.resource
|
|
519
|
+
})
|
|
520
|
+
});
|
|
521
|
+
const payload = await response.json().catch(() => null);
|
|
522
|
+
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.");
|
|
523
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
|
|
524
|
+
return {
|
|
525
|
+
host: authorization.configuration.host,
|
|
526
|
+
issuer: authorization.configuration.issuer,
|
|
527
|
+
clientId: authorization.configuration.clientId,
|
|
528
|
+
resource: authorization.configuration.resource,
|
|
529
|
+
accessToken: payload.access_token,
|
|
530
|
+
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
|
|
531
|
+
refreshToken: payload.refresh_token,
|
|
532
|
+
refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
533
|
+
scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...DEFAULT_SCOPES]
|
|
534
|
+
};
|
|
535
|
+
}
|
|
524
536
|
async function loginWithBrowser(options) {
|
|
537
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
525
538
|
return new Promise((resolve3, reject) => {
|
|
526
539
|
let settled = false;
|
|
527
|
-
|
|
540
|
+
const authorization = createBrowserAuthorizationRequest(options.baseUrl);
|
|
528
541
|
const finish = (callback) => {
|
|
529
542
|
if (settled) return;
|
|
530
543
|
settled = true;
|
|
@@ -534,82 +547,46 @@ async function loginWithBrowser(options) {
|
|
|
534
547
|
};
|
|
535
548
|
const server = createServer((request, response) => {
|
|
536
549
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
537
|
-
if (request.method !== "GET" || url.pathname !==
|
|
550
|
+
if (request.method !== "GET" || url.pathname !== OAUTH_CALLBACK_PATH) {
|
|
538
551
|
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
539
552
|
response.end("Not found");
|
|
540
553
|
return;
|
|
541
554
|
}
|
|
542
555
|
void (async () => {
|
|
543
|
-
if (!authorization) {
|
|
544
|
-
writePage(response, 503, renderCliAuthorizationResultPage({
|
|
545
|
-
title: "Authorization is starting",
|
|
546
|
-
message: "Return to your terminal and try the browser link again in a moment.",
|
|
547
|
-
success: false
|
|
548
|
-
}));
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
556
|
if (!secretsMatch(url.searchParams.get("state") ?? "", authorization.state)) {
|
|
552
|
-
writePage(response, 400,
|
|
553
|
-
|
|
554
|
-
message: "The callback state did not match. Return to your terminal and try again.",
|
|
555
|
-
success: false
|
|
556
|
-
}));
|
|
557
|
-
finish(() => reject(new Error("The browser callback state did not match.")));
|
|
557
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The callback state did not match.", success: false }));
|
|
558
|
+
finish(() => reject(new Error("The OAuth callback state did not match.")));
|
|
558
559
|
return;
|
|
559
560
|
}
|
|
560
561
|
if (url.searchParams.get("error") === "access_denied") {
|
|
561
|
-
writePage(response, 200,
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
562
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Access denied", message: "No OAuth token was saved.", success: false }));
|
|
563
|
+
finish(() => reject(new OAuthAuthorizationDeniedError()));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (url.searchParams.get("iss") !== authorization.configuration.issuer) {
|
|
567
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The authorization server did not match.", success: false }));
|
|
568
|
+
finish(() => reject(new Error("The OAuth callback issuer did not match.")));
|
|
567
569
|
return;
|
|
568
570
|
}
|
|
569
571
|
const code = url.searchParams.get("code");
|
|
570
|
-
if (!code ||
|
|
571
|
-
writePage(response, 400,
|
|
572
|
-
|
|
573
|
-
message: "Rolino did not return a valid one-time code. Return to your terminal and try again.",
|
|
574
|
-
success: false
|
|
575
|
-
}));
|
|
576
|
-
finish(() => reject(new Error("The browser callback did not include a valid code.")));
|
|
572
|
+
if (!code || code.length > 2048) {
|
|
573
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "Rolino did not return a valid code.", success: false }));
|
|
574
|
+
finish(() => reject(new Error("The OAuth callback did not include a valid code.")));
|
|
577
575
|
return;
|
|
578
576
|
}
|
|
579
577
|
try {
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
});
|
|
584
|
-
writePage(response, 200, renderCliAuthorizationResultPage({
|
|
585
|
-
title: "Terminal connected",
|
|
586
|
-
message: "Rolino is ready in your terminal. You can close this window.",
|
|
587
|
-
success: true
|
|
588
|
-
}));
|
|
589
|
-
finish(() => resolve3(credential));
|
|
578
|
+
const tokenSet = await exchangeAuthorizationCode({ authorization, code, fetch: fetchImplementation });
|
|
579
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Terminal connected", message: "You can close this window and return to your terminal.", success: true }));
|
|
580
|
+
finish(() => resolve3(tokenSet));
|
|
590
581
|
} catch (error) {
|
|
591
|
-
writePage(response, 502,
|
|
592
|
-
title: "Authorization failed",
|
|
593
|
-
message: "The CLI could not exchange its one-time code. Return to your terminal and try again.",
|
|
594
|
-
success: false
|
|
595
|
-
}));
|
|
582
|
+
writePage(response, 502, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The CLI could not exchange the code.", success: false }));
|
|
596
583
|
finish(() => reject(error));
|
|
597
584
|
}
|
|
598
585
|
})();
|
|
599
586
|
});
|
|
600
587
|
server.once("error", (error) => finish(() => reject(error)));
|
|
601
|
-
server.listen(
|
|
602
|
-
|
|
603
|
-
if (!address || typeof address === "string") {
|
|
604
|
-
finish(() => reject(new Error("Rolino could not start the local authorization callback.")));
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
authorization = createBrowserAuthorizationRequest({
|
|
608
|
-
baseUrl: options.baseUrl,
|
|
609
|
-
redirectUri: `http://127.0.0.1:${address.port}/callback`
|
|
610
|
-
});
|
|
611
|
-
options.stderr.write(`Opening Rolino in your browser\u2026
|
|
612
|
-
`);
|
|
588
|
+
server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
|
|
589
|
+
options.stderr.write("Opening Rolino OAuth sign-in in your browser\u2026\n");
|
|
613
590
|
options.stderr.write(`If it does not open, visit:
|
|
614
591
|
${authorization.url}
|
|
615
592
|
`);
|
|
@@ -617,16 +594,17 @@ ${authorization.url}
|
|
|
617
594
|
void open(authorization.url).catch(() => void 0);
|
|
618
595
|
});
|
|
619
596
|
const timeout = setTimeout(() => {
|
|
620
|
-
finish(() => reject(new Error("
|
|
621
|
-
},
|
|
597
|
+
finish(() => reject(new Error("OAuth sign-in timed out after 5 minutes.")));
|
|
598
|
+
}, OAUTH_TIMEOUT_MS);
|
|
622
599
|
});
|
|
623
600
|
}
|
|
624
601
|
|
|
625
602
|
// src/cli.ts
|
|
626
603
|
import {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
604
|
+
clearOAuthTokenSet,
|
|
605
|
+
createOAuthAccessTokenProvider,
|
|
606
|
+
resolveLocalAuthentication,
|
|
607
|
+
saveOAuthTokenSet
|
|
630
608
|
} from "@rolino/local-auth";
|
|
631
609
|
|
|
632
610
|
// src/output.ts
|
|
@@ -1022,6 +1000,10 @@ function mcpScope(value) {
|
|
|
1022
1000
|
if (value === "user" || value === "project") return value;
|
|
1023
1001
|
throw new InvalidArgumentError("MCP setup scope must be user or project.");
|
|
1024
1002
|
}
|
|
1003
|
+
function mcpTransport(value) {
|
|
1004
|
+
if (value === "auto" || value === "http" || value === "stdio") return value;
|
|
1005
|
+
throw new InvalidArgumentError("MCP transport must be auto, http, or stdio.");
|
|
1006
|
+
}
|
|
1025
1007
|
function isoDateTime(value) {
|
|
1026
1008
|
const date = new Date(value);
|
|
1027
1009
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -1060,18 +1042,43 @@ async function requireWriteConsent(options) {
|
|
|
1060
1042
|
);
|
|
1061
1043
|
}
|
|
1062
1044
|
}
|
|
1045
|
+
var BLOG_WEEKDAY_VALUES = {
|
|
1046
|
+
sun: 0,
|
|
1047
|
+
mon: 1,
|
|
1048
|
+
tue: 2,
|
|
1049
|
+
wed: 3,
|
|
1050
|
+
thu: 4,
|
|
1051
|
+
fri: 5,
|
|
1052
|
+
sat: 6
|
|
1053
|
+
};
|
|
1054
|
+
function parseBlogWeekdays(value) {
|
|
1055
|
+
const names = value.split(",").map((day) => day.trim().toLowerCase());
|
|
1056
|
+
const weekdays = names.map((day) => BLOG_WEEKDAY_VALUES[day]).filter((day) => day !== void 0);
|
|
1057
|
+
if (!weekdays.length || weekdays.length !== names.length || new Set(weekdays).size !== weekdays.length) {
|
|
1058
|
+
throw new TypeError("--weekdays must contain unique comma-separated sun,mon,tue,wed,thu,fri,sat values.");
|
|
1059
|
+
}
|
|
1060
|
+
return weekdays;
|
|
1061
|
+
}
|
|
1063
1062
|
function requireBlogExecutionConsent(options) {
|
|
1064
1063
|
if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
|
|
1065
1064
|
throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
|
|
1066
1065
|
}
|
|
1067
1066
|
function formatMcpSetupPreview(result) {
|
|
1067
|
+
const connection = result.server.transport === "http" ? [
|
|
1068
|
+
`Transport: Streamable HTTP`,
|
|
1069
|
+
`Endpoint: ${result.server.url}`,
|
|
1070
|
+
"Authentication: OAuth in the MCP client"
|
|
1071
|
+
] : [
|
|
1072
|
+
"Transport: local STDIO",
|
|
1073
|
+
`Command: ${result.server.command}`,
|
|
1074
|
+
`Arguments: ${result.server.args.join(" ")}`,
|
|
1075
|
+
`Rolino URL: ${result.server.env.ROLINO_URL}`
|
|
1076
|
+
];
|
|
1068
1077
|
return [
|
|
1069
1078
|
`Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
|
|
1070
1079
|
`Scope: ${result.scope}`,
|
|
1071
1080
|
`Target: ${result.target}`,
|
|
1072
|
-
|
|
1073
|
-
`Arguments: ${result.server.args.join(" ")}`,
|
|
1074
|
-
`Rolino URL: ${result.server.env.ROLINO_URL}`,
|
|
1081
|
+
...connection,
|
|
1075
1082
|
"No token will be written to MCP configuration."
|
|
1076
1083
|
].join("\n");
|
|
1077
1084
|
}
|
|
@@ -1090,16 +1097,16 @@ function baseUrlFor(options, runtime) {
|
|
|
1090
1097
|
function clientFor(options, runtime) {
|
|
1091
1098
|
const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
|
|
1092
1099
|
const baseUrl = baseUrlFor(options, runtime);
|
|
1093
|
-
const
|
|
1100
|
+
const token = runtime.env.ROLINO_TOKEN ?? createOAuthAccessTokenProvider({ baseUrl, env: runtime.env, fetch: runtime.fetch });
|
|
1094
1101
|
return new RolinoClient({
|
|
1095
1102
|
baseUrl,
|
|
1096
|
-
token
|
|
1103
|
+
token,
|
|
1097
1104
|
timeoutMs,
|
|
1098
1105
|
fetch: runtime.fetch
|
|
1099
1106
|
});
|
|
1100
1107
|
}
|
|
1101
1108
|
function exitCodeFor(error) {
|
|
1102
|
-
if (error instanceof
|
|
1109
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1103
1110
|
return EXIT_CODES.cancelled;
|
|
1104
1111
|
}
|
|
1105
1112
|
if (error instanceof RolinoNetworkError) {
|
|
@@ -1132,7 +1139,7 @@ function exitCodeFor(error) {
|
|
|
1132
1139
|
return EXIT_CODES.unexpected;
|
|
1133
1140
|
}
|
|
1134
1141
|
function errorForOutput(error) {
|
|
1135
|
-
if (error instanceof
|
|
1142
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1136
1143
|
return { code: "CANCELLED", message: error.message };
|
|
1137
1144
|
}
|
|
1138
1145
|
if (error instanceof RolinoApiError) {
|
|
@@ -1393,22 +1400,14 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1393
1400
|
"ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
|
|
1394
1401
|
);
|
|
1395
1402
|
}
|
|
1396
|
-
const
|
|
1397
|
-
if (!meta.authentication.cliBrowserAuthorization) {
|
|
1398
|
-
throw new TypeError(
|
|
1399
|
-
"This Rolino server does not support browser CLI authorization."
|
|
1400
|
-
);
|
|
1401
|
-
}
|
|
1402
|
-
const credential = await loginWithBrowser({
|
|
1403
|
+
const tokenSet = await loginWithBrowser({
|
|
1403
1404
|
baseUrl: client.baseUrl,
|
|
1404
1405
|
stderr: runtime.stderr,
|
|
1405
|
-
|
|
1406
|
-
requestId: context.requestId
|
|
1407
|
-
})
|
|
1406
|
+
fetch: runtime.fetch
|
|
1408
1407
|
});
|
|
1409
1408
|
const authenticatedClient = new RolinoClient({
|
|
1410
1409
|
baseUrl: client.baseUrl,
|
|
1411
|
-
token:
|
|
1410
|
+
token: tokenSet.accessToken,
|
|
1412
1411
|
timeoutMs: client.timeoutMs,
|
|
1413
1412
|
fetch: runtime.fetch
|
|
1414
1413
|
});
|
|
@@ -1417,12 +1416,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1417
1416
|
const actor = await authenticatedClient.whoami({
|
|
1418
1417
|
requestId: context.requestId
|
|
1419
1418
|
});
|
|
1420
|
-
const path =
|
|
1421
|
-
token: credential.token,
|
|
1422
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1423
|
-
expiresAt: credential.expiresAt,
|
|
1424
|
-
organization: credential.organization
|
|
1425
|
-
}, runtime.env);
|
|
1419
|
+
const path = saveOAuthTokenSet(tokenSet, runtime.env);
|
|
1426
1420
|
credentialSaved = true;
|
|
1427
1421
|
writeSuccess(
|
|
1428
1422
|
context,
|
|
@@ -1431,23 +1425,20 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1431
1425
|
user: actor.user,
|
|
1432
1426
|
organization: actor.organization,
|
|
1433
1427
|
capabilities: actor.capabilities,
|
|
1434
|
-
expiresAt:
|
|
1428
|
+
expiresAt: tokenSet.accessTokenExpiresAt
|
|
1435
1429
|
},
|
|
1436
1430
|
[
|
|
1437
1431
|
`Authenticated as ${actor.user.email}.`,
|
|
1438
1432
|
`Workspace: ${actor.organization.name}`,
|
|
1439
|
-
`
|
|
1433
|
+
`Access token expires: ${tokenSet.accessTokenExpiresAt}`,
|
|
1440
1434
|
`Credential saved in a permission-restricted file at ${path}`
|
|
1441
1435
|
].join("\n"),
|
|
1442
1436
|
["rolino whoami --agent", "rolino projects list --agent"]
|
|
1443
1437
|
);
|
|
1444
1438
|
} catch (error) {
|
|
1445
1439
|
if (credentialSaved) {
|
|
1446
|
-
|
|
1440
|
+
clearOAuthTokenSet(client.baseUrl, runtime.env);
|
|
1447
1441
|
}
|
|
1448
|
-
await authenticatedClient.auth.revokeCurrentCredential({
|
|
1449
|
-
requestId: context.requestId
|
|
1450
|
-
}).catch(() => void 0);
|
|
1451
1442
|
throw error;
|
|
1452
1443
|
}
|
|
1453
1444
|
}
|
|
@@ -1460,7 +1451,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1460
1451
|
global,
|
|
1461
1452
|
runtime,
|
|
1462
1453
|
async action(context, client) {
|
|
1463
|
-
const credential =
|
|
1454
|
+
const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
|
|
1464
1455
|
if (credential.source === "none") {
|
|
1465
1456
|
writeSuccess(
|
|
1466
1457
|
context,
|
|
@@ -1479,7 +1470,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1479
1470
|
baseUrl: client.baseUrl,
|
|
1480
1471
|
user: actor.user,
|
|
1481
1472
|
organization: actor.organization,
|
|
1482
|
-
...credential.source === "
|
|
1473
|
+
...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
|
|
1483
1474
|
},
|
|
1484
1475
|
[
|
|
1485
1476
|
`Authenticated as ${actor.user.email}.`,
|
|
@@ -1497,7 +1488,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1497
1488
|
global,
|
|
1498
1489
|
runtime,
|
|
1499
1490
|
async action(context, client) {
|
|
1500
|
-
const credential =
|
|
1491
|
+
const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
|
|
1501
1492
|
if (credential.source === "environment") {
|
|
1502
1493
|
throw new TypeError(
|
|
1503
1494
|
"ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
|
|
@@ -1511,15 +1502,36 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1511
1502
|
);
|
|
1512
1503
|
return;
|
|
1513
1504
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1505
|
+
const tokenSet = credential.tokenSet;
|
|
1506
|
+
const fetchImplementation = runtime.fetch ?? globalThis.fetch;
|
|
1507
|
+
const accessToken = await createOAuthAccessTokenProvider({
|
|
1508
|
+
baseUrl: client.baseUrl,
|
|
1509
|
+
env: runtime.env,
|
|
1510
|
+
fetch: fetchImplementation
|
|
1511
|
+
})();
|
|
1512
|
+
if (accessToken) {
|
|
1513
|
+
const disconnect = await fetchImplementation(`${client.baseUrl}/api/v1/oauth/logout`, {
|
|
1514
|
+
method: "POST",
|
|
1515
|
+
headers: {
|
|
1516
|
+
authorization: `Bearer ${accessToken}`,
|
|
1517
|
+
accept: "application/json",
|
|
1518
|
+
"x-request-id": context.requestId
|
|
1519
|
+
}
|
|
1517
1520
|
});
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
+
if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
|
|
1522
|
+
throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
|
|
1523
|
+
}
|
|
1521
1524
|
}
|
|
1522
|
-
|
|
1525
|
+
await fetchImplementation(`${tokenSet.issuer}/oauth2/revoke`, {
|
|
1526
|
+
method: "POST",
|
|
1527
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1528
|
+
body: new URLSearchParams({
|
|
1529
|
+
token: tokenSet.refreshToken,
|
|
1530
|
+
token_type_hint: "refresh_token",
|
|
1531
|
+
client_id: tokenSet.clientId
|
|
1532
|
+
})
|
|
1533
|
+
}).catch(() => void 0);
|
|
1534
|
+
clearOAuthTokenSet(client.baseUrl, runtime.env);
|
|
1523
1535
|
writeSuccess(
|
|
1524
1536
|
context,
|
|
1525
1537
|
{ revoked: true, baseUrl: client.baseUrl },
|
|
@@ -1529,7 +1541,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1529
1541
|
});
|
|
1530
1542
|
});
|
|
1531
1543
|
const setup = program.command("setup").description("Configure local agent tools for Rolino");
|
|
1532
|
-
setup.command("mcp").description("Configure
|
|
1544
|
+
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) => {
|
|
1533
1545
|
const global = program.opts();
|
|
1534
1546
|
commandExitCode = await execute({
|
|
1535
1547
|
command: "setup mcp",
|
|
@@ -1542,7 +1554,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1542
1554
|
cwd: runtime.cwd,
|
|
1543
1555
|
env: runtime.env,
|
|
1544
1556
|
nodePath: runtime.nodePath,
|
|
1545
|
-
cliEntryPath: runtime.cliEntryPath
|
|
1557
|
+
cliEntryPath: runtime.cliEntryPath,
|
|
1558
|
+
fetch: runtime.fetch
|
|
1546
1559
|
};
|
|
1547
1560
|
const preview = await setupMcp({ ...setupOptions, dryRun: true });
|
|
1548
1561
|
let result = preview;
|
|
@@ -1569,11 +1582,12 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1569
1582
|
`${state} Rolino MCP for ${clientLabel}.`,
|
|
1570
1583
|
`Scope: ${result.scope}`,
|
|
1571
1584
|
`Target: ${result.target}`,
|
|
1585
|
+
`Transport: ${result.transport === "http" ? "Streamable HTTP" : "local STDIO"}`,
|
|
1572
1586
|
...result.backupPath && !result.dryRun ? [`Backup: ${result.backupPath}`] : [],
|
|
1573
1587
|
`Rolino URL: ${client.baseUrl}`,
|
|
1574
1588
|
"No token was written to MCP configuration."
|
|
1575
1589
|
].join("\n"),
|
|
1576
|
-
result.client === "codex" ? ["codex mcp
|
|
1590
|
+
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"]
|
|
1577
1591
|
);
|
|
1578
1592
|
}
|
|
1579
1593
|
});
|
|
@@ -1598,7 +1612,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1598
1612
|
});
|
|
1599
1613
|
});
|
|
1600
1614
|
const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
|
|
1601
|
-
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
|
|
1615
|
+
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) => {
|
|
1602
1616
|
const global = program.opts();
|
|
1603
1617
|
commandExitCode = await execute({
|
|
1604
1618
|
command: "posts create",
|
|
@@ -1625,7 +1639,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1625
1639
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1626
1640
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1627
1641
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1628
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1642
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1643
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1629
1644
|
},
|
|
1630
1645
|
tiktokSettings: tiktokSettings(local) ?? null,
|
|
1631
1646
|
youtubeSettings: youtubeSettings(local)
|
|
@@ -1643,7 +1658,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1643
1658
|
}
|
|
1644
1659
|
});
|
|
1645
1660
|
});
|
|
1646
|
-
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) => {
|
|
1661
|
+
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) => {
|
|
1647
1662
|
const global = program.opts();
|
|
1648
1663
|
commandExitCode = await execute({
|
|
1649
1664
|
command: "posts update",
|
|
@@ -1672,7 +1687,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1672
1687
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1673
1688
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1674
1689
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1675
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1690
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1691
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1676
1692
|
};
|
|
1677
1693
|
const resolvedTikTokSettings = tiktokSettings(local);
|
|
1678
1694
|
const resolvedYouTubeSettings = youtubeSettings(local);
|
|
@@ -1820,7 +1836,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1820
1836
|
});
|
|
1821
1837
|
});
|
|
1822
1838
|
const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
|
|
1823
|
-
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
|
|
1839
|
+
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) => {
|
|
1824
1840
|
const global = program.opts();
|
|
1825
1841
|
commandExitCode = await execute({
|
|
1826
1842
|
command: "posts publish preview",
|
|
@@ -1977,6 +1993,52 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1977
1993
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1978
1994
|
} });
|
|
1979
1995
|
});
|
|
1996
|
+
const blogWebhook = blogPublishing.command("webhook").description("Configure a signed custom Blog publishing webhook");
|
|
1997
|
+
const addWebhookChangeOptions = (command, executeChange) => {
|
|
1998
|
+
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");
|
|
1999
|
+
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");
|
|
2000
|
+
return command;
|
|
2001
|
+
};
|
|
2002
|
+
addWebhookChangeOptions(blogWebhook.command("preview"), false).action(async (local) => {
|
|
2003
|
+
const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
|
|
2004
|
+
const global = program.opts();
|
|
2005
|
+
commandExitCode = await execute({ command: "blog destinations webhook preview", global, runtime, async action(context, client) {
|
|
2006
|
+
const data = await client.blog.publishing.webhook.preview(local.project, input, { requestId: context.requestId });
|
|
2007
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2008
|
+
} });
|
|
2009
|
+
});
|
|
2010
|
+
addWebhookChangeOptions(blogWebhook.command("execute"), true).action(async (local) => {
|
|
2011
|
+
const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
|
|
2012
|
+
const global = program.opts();
|
|
2013
|
+
commandExitCode = await execute({ command: "blog destinations webhook execute", global, runtime, async action(context, client) {
|
|
2014
|
+
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.` });
|
|
2015
|
+
const data = await client.blog.publishing.webhook.execute(local.project, { ...input, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2016
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2017
|
+
} });
|
|
2018
|
+
});
|
|
2019
|
+
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) => {
|
|
2020
|
+
const global = program.opts();
|
|
2021
|
+
commandExitCode = await execute({ command: "blog destinations webhook test", global, runtime, async action(context, client) {
|
|
2022
|
+
const data = await client.blog.publishing.webhook.test(local.project, { siteId: local.site, destinationId: local.destination, makePrimary: local.makePrimary }, { requestId: context.requestId });
|
|
2023
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2024
|
+
} });
|
|
2025
|
+
});
|
|
2026
|
+
const blogWebhookRotate = blogWebhook.command("rotate").description("Rotate the server-only signing secret");
|
|
2027
|
+
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) => {
|
|
2028
|
+
const global = program.opts();
|
|
2029
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate preview", global, runtime, async action(context, client) {
|
|
2030
|
+
const data = await client.blog.publishing.webhook.previewRotation(local.project, { siteId: local.site, destinationId: local.destination }, { requestId: context.requestId });
|
|
2031
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2032
|
+
} });
|
|
2033
|
+
});
|
|
2034
|
+
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) => {
|
|
2035
|
+
const global = program.opts();
|
|
2036
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate execute", global, runtime, async action(context, client) {
|
|
2037
|
+
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.` });
|
|
2038
|
+
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 });
|
|
2039
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2040
|
+
} });
|
|
2041
|
+
});
|
|
1980
2042
|
const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
|
|
1981
2043
|
blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
|
|
1982
2044
|
const global = program.opts();
|
|
@@ -2009,9 +2071,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2009
2071
|
});
|
|
2010
2072
|
const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
|
|
2011
2073
|
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) => {
|
|
2012
|
-
const
|
|
2013
|
-
const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
|
|
2014
|
-
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.");
|
|
2074
|
+
const weekdays = parseBlogWeekdays(local.weekdays);
|
|
2015
2075
|
const global = program.opts();
|
|
2016
2076
|
commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
|
|
2017
2077
|
const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
|
|
@@ -2025,6 +2085,25 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2025
2085
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2026
2086
|
} });
|
|
2027
2087
|
});
|
|
2088
|
+
const blogPlanCadence = blogPlan.command("cadence").description("Preview or apply editorial cadence changes without publishing");
|
|
2089
|
+
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) => {
|
|
2090
|
+
const global = program.opts();
|
|
2091
|
+
commandExitCode = await execute({ command: "blog plan cadence preview", global, runtime, async action(context, client) {
|
|
2092
|
+
const data = await client.blog.plans.previewCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion }, { requestId: context.requestId });
|
|
2093
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2094
|
+
} });
|
|
2095
|
+
});
|
|
2096
|
+
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) => {
|
|
2097
|
+
const global = program.opts();
|
|
2098
|
+
commandExitCode = await execute({ command: "blog plan cadence apply", global, runtime, async action(context, client) {
|
|
2099
|
+
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed editorial cadence change?
|
|
2100
|
+
Project: ${local.project}
|
|
2101
|
+
Plan: ${local.plan}
|
|
2102
|
+
This moves eligible editorial dates only. It does not schedule, publish, or unpublish an article.` });
|
|
2103
|
+
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 });
|
|
2104
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2105
|
+
} });
|
|
2106
|
+
});
|
|
2028
2107
|
blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
|
|
2029
2108
|
const global = program.opts();
|
|
2030
2109
|
commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
|
|
@@ -2247,6 +2326,106 @@ Revision: ${local.revision}` });
|
|
|
2247
2326
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2248
2327
|
} });
|
|
2249
2328
|
});
|
|
2329
|
+
const backlinks = program.command("backlinks").description("Review backlink prospects and public contact drafts. Rolino never sends email.");
|
|
2330
|
+
const backlinkTargets = backlinks.command("targets");
|
|
2331
|
+
backlinkTargets.command("list").requiredOption("--project <project-id>").action(async (local) => {
|
|
2332
|
+
const global = program.opts();
|
|
2333
|
+
commandExitCode = await execute({ command: "backlinks targets list", global, runtime, async action(context, client) {
|
|
2334
|
+
const data = await client.backlinks.targets.list(local.project, { requestId: context.requestId });
|
|
2335
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2336
|
+
} });
|
|
2337
|
+
});
|
|
2338
|
+
backlinkTargets.command("add").requiredOption("--project <project-id>").requiredOption("--url <url>").requiredOption("--label <label>").action(async (local) => {
|
|
2339
|
+
const global = program.opts();
|
|
2340
|
+
commandExitCode = await execute({ command: "backlinks targets add", global, runtime, async action(context, client) {
|
|
2341
|
+
const data = await client.backlinks.targets.add(local.project, { url: local.url, label: local.label }, context.requestId, { requestId: context.requestId });
|
|
2342
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2343
|
+
} });
|
|
2344
|
+
});
|
|
2345
|
+
backlinks.command("discover").requiredOption("--project <project-id>").option("--limit <number>", "maximum saved prospects", Number, 20).option("--idempotency-key <key>").action(async (local) => {
|
|
2346
|
+
const global = program.opts();
|
|
2347
|
+
commandExitCode = await execute({ command: "backlinks discover", global, runtime, async action(context, client) {
|
|
2348
|
+
const data = await client.backlinks.discoveries.start(local.project, { limit: local.limit }, local.idempotencyKey ?? context.requestId, { requestId: context.requestId });
|
|
2349
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2350
|
+
} });
|
|
2351
|
+
});
|
|
2352
|
+
const backlinkRuns = backlinks.command("runs");
|
|
2353
|
+
backlinkRuns.command("get").requiredOption("--project <project-id>").requiredOption("--run <run-id>").action(async (local) => {
|
|
2354
|
+
const global = program.opts();
|
|
2355
|
+
commandExitCode = await execute({ command: "backlinks runs get", global, runtime, async action(context, client) {
|
|
2356
|
+
const data = await client.backlinks.discoveries.get(local.project, local.run, { requestId: context.requestId });
|
|
2357
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2358
|
+
} });
|
|
2359
|
+
});
|
|
2360
|
+
const backlinkProspects = backlinks.command("prospects");
|
|
2361
|
+
backlinkProspects.command("list").requiredOption("--project <project-id>").option("--stage <stage>").action(async (local) => {
|
|
2362
|
+
const global = program.opts();
|
|
2363
|
+
commandExitCode = await execute({ command: "backlinks prospects list", global, runtime, async action(context, client) {
|
|
2364
|
+
const stage = local.stage ? BacklinkProspectStageSchema.parse(local.stage.toUpperCase()) : void 0;
|
|
2365
|
+
const data = await client.backlinks.prospects.list(local.project, { stage }, { requestId: context.requestId });
|
|
2366
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2367
|
+
} });
|
|
2368
|
+
});
|
|
2369
|
+
backlinkProspects.command("get").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
|
|
2370
|
+
const global = program.opts();
|
|
2371
|
+
commandExitCode = await execute({ command: "backlinks prospects get", global, runtime, async action(context, client) {
|
|
2372
|
+
const data = await client.backlinks.prospects.get(local.project, local.prospect, { requestId: context.requestId });
|
|
2373
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2374
|
+
} });
|
|
2375
|
+
});
|
|
2376
|
+
backlinkProspects.command("approve").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").requiredOption("--expected-version <number>", "current optimistic version", Number).action(async (local) => {
|
|
2377
|
+
const global = program.opts();
|
|
2378
|
+
commandExitCode = await execute({ command: "backlinks prospects approve", global, runtime, async action(context, client) {
|
|
2379
|
+
const data = await client.backlinks.prospects.updateStage(local.project, local.prospect, { stage: "APPROVED", expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2380
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2381
|
+
} });
|
|
2382
|
+
});
|
|
2383
|
+
const backlinkContacts = backlinks.command("contacts");
|
|
2384
|
+
backlinkContacts.command("research").requiredOption("--project <project-id>").requiredOption("--prospect <prospect-id>").action(async (local) => {
|
|
2385
|
+
const global = program.opts();
|
|
2386
|
+
commandExitCode = await execute({ command: "backlinks contacts research", global, runtime, async action(context, client) {
|
|
2387
|
+
const data = await client.backlinks.prospects.researchContact(local.project, local.prospect, context.requestId, { requestId: context.requestId });
|
|
2388
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2389
|
+
} });
|
|
2390
|
+
});
|
|
2391
|
+
backlinkContacts.command("list").requiredOption("--project <project-id>").action(async (local) => {
|
|
2392
|
+
const global = program.opts();
|
|
2393
|
+
commandExitCode = await execute({ command: "backlinks contacts list", global, runtime, async action(context, client) {
|
|
2394
|
+
const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
|
|
2395
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2396
|
+
} });
|
|
2397
|
+
});
|
|
2398
|
+
backlinkContacts.command("export").requiredOption("--project <project-id>").option("--format <format>", "json or csv", "csv").action(async (local) => {
|
|
2399
|
+
const global = program.opts();
|
|
2400
|
+
commandExitCode = await execute({ command: "backlinks contacts export", global, runtime, async action(context, client) {
|
|
2401
|
+
const data = await client.backlinks.contacts.list(local.project, { limit: 50 }, { requestId: context.requestId });
|
|
2402
|
+
if (local.format !== "csv") return writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2403
|
+
const cell = (value) => {
|
|
2404
|
+
let text = String(value ?? "").replace(/[\r\n]+/g, " ");
|
|
2405
|
+
if (/^[=+\-@\t]/.test(text)) text = `'${text}`;
|
|
2406
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
2407
|
+
};
|
|
2408
|
+
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");
|
|
2409
|
+
context.stdout.write(`${csv}
|
|
2410
|
+
`);
|
|
2411
|
+
} });
|
|
2412
|
+
});
|
|
2413
|
+
const backlinkOutreach = backlinks.command("outreach");
|
|
2414
|
+
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) => {
|
|
2415
|
+
const global = program.opts();
|
|
2416
|
+
commandExitCode = await execute({ command: "backlinks outreach update", global, runtime, async action(context, client) {
|
|
2417
|
+
const payload = JSON.parse(await readFile2(resolve2(runtime.cwd, local.file), "utf8"));
|
|
2418
|
+
const data = await client.backlinks.prospects.updateOutreach(local.project, local.prospect, { ...payload, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2419
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2420
|
+
} });
|
|
2421
|
+
});
|
|
2422
|
+
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) => {
|
|
2423
|
+
const global = program.opts();
|
|
2424
|
+
commandExitCode = await execute({ command: "backlinks verify", global, runtime, async action(context, client) {
|
|
2425
|
+
const data = await client.backlinks.prospects.verify(local.project, local.prospect, { url: local.url, expectedVersion: local.expectedVersion }, context.requestId, { requestId: context.requestId });
|
|
2426
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2427
|
+
} });
|
|
2428
|
+
});
|
|
2250
2429
|
const seo = program.command("seo").description("Read authorized SEO opportunities and weekly reports");
|
|
2251
2430
|
const seoOpportunities = seo.command("opportunities").description("Read accepted SEO opportunities");
|
|
2252
2431
|
seoOpportunities.command("list").description("List bounded SEO opportunities").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum opportunities to return", (value) => {
|
|
@@ -2344,7 +2523,7 @@ Revision: ${local.revision}` });
|
|
|
2344
2523
|
message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
|
|
2345
2524
|
}
|
|
2346
2525
|
];
|
|
2347
|
-
const credential =
|
|
2526
|
+
const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
|
|
2348
2527
|
if (credential.source !== "none") {
|
|
2349
2528
|
const actor = await client.whoami({ requestId: context.requestId });
|
|
2350
2529
|
checks.push({
|
|
@@ -2412,4 +2591,4 @@ export {
|
|
|
2412
2591
|
ROLINO_CLI_VERSION,
|
|
2413
2592
|
runCli
|
|
2414
2593
|
};
|
|
2415
|
-
//# sourceMappingURL=chunk-
|
|
2594
|
+
//# sourceMappingURL=chunk-PEJ2WA66.js.map
|