@rolino/cli 0.5.0-beta.0 → 0.6.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/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.5.0-beta.0",
53
+ version: "0.6.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.5.0-beta.0",
111
- "@rolino/local-auth": "0.5.0-beta.0",
112
- "@rolino/sdk": "0.5.0-beta.0",
110
+ "@rolino/contracts": "0.6.0",
111
+ "@rolino/local-auth": "0.6.0",
112
+ "@rolino/sdk": "0.6.0",
113
113
  commander: "^15.0.0",
114
114
  open: "^11.0.0"
115
115
  },
116
116
  devDependencies: {
117
- tsx: "^4.21.0"
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 import_contracts2 = require("@rolino/contracts");
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 import_node_os = require("os");
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 PAGE_HEADERS = {
135
- "cache-control": "no-store",
136
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
137
- "content-type": "text/html; charset=utf-8",
138
- "referrer-policy": "no-referrer",
139
- "x-content-type-options": "nosniff"
140
- };
141
- var ROLINO_MASCOT_SVG = '<svg viewBox="0 0 512 512" aria-hidden="true" focusable="false"><path fill="#34C78A" d="M256 142c-48-29-71-58-62-84 28-4 52 11 62 36 10-25 34-40 62-36 9 26-14 55-62 84Z"/><path fill="#FACC15" stroke="#0F172A" stroke-width="12" stroke-linejoin="round" d="M256 112c-89 0-156 74-156 176v57c0 31 19 54 50 54h212c31 0 50-23 50-54v-57c0-102-67-176-156-176Z"/><circle cx="204" cy="272" r="18" fill="#0F172A"/><circle cx="308" cy="272" r="18" fill="#0F172A"/><path fill="none" stroke="#0F172A" stroke-linecap="round" stroke-width="16" d="M226 318c18 25 42 25 60 0"/></svg>';
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
  "&": "&amp;",
@@ -148,86 +150,18 @@ function escapeHtml(value) {
148
150
  "'": "&#039;"
149
151
  })[character]);
150
152
  }
151
- function renderCliAuthorizationResultPage(options) {
152
- const tone = options.success ? "success" : "failure";
153
- const eyebrow = options.success ? "CLI handoff complete" : "CLI handoff paused";
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, PAGE_HEADERS);
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(options) {
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 url = new URL("/cli/authorize", options.baseUrl);
243
- const requestExpiresAt = new Date(
244
- Date.now() + import_contracts.CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS
245
- ).toISOString();
246
- url.searchParams.set("redirect_uri", options.redirectUri);
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.searchParams.set(
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 CliAuthorizationDeniedError = class extends Error {
189
+ var OAuthAuthorizationDeniedError = class extends Error {
264
190
  constructor() {
265
191
  super("Authorization was denied.");
266
- this.name = "CliAuthorizationDeniedError";
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
- let authorization = null;
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 !== "/callback") {
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, renderCliAuthorizationResultPage({
298
- title: "Authorization failed",
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, renderCliAuthorizationResultPage({
307
- title: "Access denied",
308
- message: "No credential was created. You can close this window and return to your terminal.",
309
- success: false
310
- }));
311
- finish(() => reject(new CliAuthorizationDeniedError()));
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 || !/^[A-Za-z0-9_-]{43,128}$/.test(code)) {
316
- writePage(response, 400, renderCliAuthorizationResultPage({
317
- title: "Authorization failed",
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 credential = await options.exchange({
326
- code,
327
- codeVerifier: authorization.codeVerifier
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, renderCliAuthorizationResultPage({
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(0, "127.0.0.1", () => {
347
- const address = server.address();
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("Browser authorization timed out after 5 minutes.")));
366
- }, import_contracts.CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS);
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 import_local_auth = require("@rolino/local-auth");
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 import_node_os2 = require("os");
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, import_node_os2.homedir)();
578
+ return env.USERPROFILE || env.HOME || (0, import_node_os.homedir)();
659
579
  }
660
580
  function tomlString(value) {
661
581
  return JSON.stringify(value);
@@ -921,40 +841,40 @@ function outputFormat(value) {
921
841
  throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
922
842
  }
923
843
  function postStatus(value) {
924
- const parsed = import_contracts2.PostStatusSchema.safeParse(value.toUpperCase());
844
+ const parsed = import_contracts.PostStatusSchema.safeParse(value.toUpperCase());
925
845
  if (parsed.success) return parsed.data;
926
846
  throw new import_commander.InvalidArgumentError(
927
- `Status must be one of ${import_contracts2.PostStatusSchema.options.join(", ")}.`
847
+ `Status must be one of ${import_contracts.PostStatusSchema.options.join(", ")}.`
928
848
  );
929
849
  }
930
850
  function seoOpportunityKind(value) {
931
- const parsed = import_contracts2.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
851
+ const parsed = import_contracts.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
932
852
  if (parsed.success) return parsed.data;
933
- throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts2.SeoOpportunityKindSchema.options.join(", ")}.`);
853
+ throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts.SeoOpportunityKindSchema.options.join(", ")}.`);
934
854
  }
935
855
  function seoExpectedImpact(value) {
936
- const parsed = import_contracts2.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
856
+ const parsed = import_contracts.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
937
857
  if (parsed.success) return parsed.data;
938
858
  throw new import_commander.InvalidArgumentError("SEO impact must be HIGH, MEDIUM, or LOW.");
939
859
  }
940
860
  function seoReportCompleteness(value) {
941
- const parsed = import_contracts2.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
861
+ const parsed = import_contracts.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
942
862
  if (parsed.success) return parsed.data;
943
863
  throw new import_commander.InvalidArgumentError("Report status must be COMPLETE or PARTIAL.");
944
864
  }
945
865
  function projectType(value) {
946
- const parsed = import_contracts2.ProjectTypeSchema.safeParse(value.toUpperCase());
866
+ const parsed = import_contracts.ProjectTypeSchema.safeParse(value.toUpperCase());
947
867
  if (parsed.success) return parsed.data;
948
868
  throw new import_commander.InvalidArgumentError(
949
- `Type must be one of ${import_contracts2.ProjectTypeSchema.options.join(", ")}.`
869
+ `Type must be one of ${import_contracts.ProjectTypeSchema.options.join(", ")}.`
950
870
  );
951
871
  }
952
872
  function publishingProvider(value) {
953
873
  const normalized = value.toUpperCase();
954
- const parsed = import_contracts2.PublishingProviderSchema.safeParse(normalized);
874
+ const parsed = import_contracts.PublishingProviderSchema.safeParse(normalized);
955
875
  if (parsed.success) return parsed.data;
956
876
  throw new import_commander.InvalidArgumentError(
957
- `Platform must be one of ${import_contracts2.PublishingProviderSchema.options.join(", ")}.`
877
+ `Platform must be one of ${import_contracts.PublishingProviderSchema.options.join(", ")}.`
958
878
  );
959
879
  }
960
880
  function collectPublishingProvider(value, previous) {
@@ -965,10 +885,10 @@ function collectString(value, previous) {
965
885
  }
966
886
  function deliveryOptionsProvider(value) {
967
887
  const normalized = value.toUpperCase();
968
- const parsed = import_contracts2.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
888
+ const parsed = import_contracts.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
969
889
  if (parsed.success) return parsed.data;
970
890
  throw new import_commander.InvalidArgumentError(
971
- `Provider must be one of ${import_contracts2.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
891
+ `Provider must be one of ${import_contracts.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
972
892
  );
973
893
  }
974
894
  function tiktokPostMode(value) {
@@ -1006,7 +926,7 @@ function yesOrNo(value) {
1006
926
  function youtubeSettings(options) {
1007
927
  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
928
  if (!hasYouTubeOptions) return null;
1009
- return import_contracts2.YouTubePostSettingsSchema.parse({
929
+ return import_contracts.YouTubePostSettingsSchema.parse({
1010
930
  title: options.youtubeTitle,
1011
931
  categoryId: options.youtubeCategoryId,
1012
932
  privacyStatus: options.youtubePrivacy,
@@ -1069,6 +989,27 @@ async function requireWriteConsent(options) {
1069
989
  );
1070
990
  }
1071
991
  }
992
+ var BLOG_WEEKDAY_VALUES = {
993
+ sun: 0,
994
+ mon: 1,
995
+ tue: 2,
996
+ wed: 3,
997
+ thu: 4,
998
+ fri: 5,
999
+ sat: 6
1000
+ };
1001
+ function parseBlogWeekdays(value) {
1002
+ const names = value.split(",").map((day) => day.trim().toLowerCase());
1003
+ const weekdays = names.map((day) => BLOG_WEEKDAY_VALUES[day]).filter((day) => day !== void 0);
1004
+ if (!weekdays.length || weekdays.length !== names.length || new Set(weekdays).size !== weekdays.length) {
1005
+ throw new TypeError("--weekdays must contain unique comma-separated sun,mon,tue,wed,thu,fri,sat values.");
1006
+ }
1007
+ return weekdays;
1008
+ }
1009
+ function requireBlogExecutionConsent(options) {
1010
+ if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
1011
+ throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
1012
+ }
1072
1013
  function formatMcpSetupPreview(result) {
1073
1014
  return [
1074
1015
  `Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
@@ -1095,16 +1036,16 @@ function baseUrlFor(options, runtime) {
1095
1036
  function clientFor(options, runtime) {
1096
1037
  const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
1097
1038
  const baseUrl = baseUrlFor(options, runtime);
1098
- const credential = (0, import_local_auth.resolveCredential)(baseUrl, runtime.env);
1039
+ const token = runtime.env.ROLINO_TOKEN ?? (0, import_local_auth2.createOAuthAccessTokenProvider)({ baseUrl, env: runtime.env, fetch: runtime.fetch });
1099
1040
  return new import_sdk.RolinoClient({
1100
1041
  baseUrl,
1101
- token: credential.token ?? void 0,
1042
+ token,
1102
1043
  timeoutMs,
1103
1044
  fetch: runtime.fetch
1104
1045
  });
1105
1046
  }
1106
1047
  function exitCodeFor(error) {
1107
- if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1048
+ if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1108
1049
  return EXIT_CODES.cancelled;
1109
1050
  }
1110
1051
  if (error instanceof import_sdk.RolinoNetworkError) {
@@ -1137,7 +1078,7 @@ function exitCodeFor(error) {
1137
1078
  return EXIT_CODES.unexpected;
1138
1079
  }
1139
1080
  function errorForOutput(error) {
1140
- if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1081
+ if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1141
1082
  return { code: "CANCELLED", message: error.message };
1142
1083
  }
1143
1084
  if (error instanceof import_sdk.RolinoApiError) {
@@ -1162,7 +1103,7 @@ function errorForOutput(error) {
1162
1103
  function tiktokSettings(options) {
1163
1104
  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;
1164
1105
  if (!hasTikTokOptions) return void 0;
1165
- return import_contracts2.TikTokDraftSettingsSchema.parse({
1106
+ return import_contracts.TikTokDraftSettingsSchema.parse({
1166
1107
  postMode: options.tiktokMode,
1167
1108
  privacyLevel: options.tiktokVisibility,
1168
1109
  allowComment: options.tiktokComments,
@@ -1306,7 +1247,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1306
1247
  global,
1307
1248
  runtime,
1308
1249
  async action(context, client) {
1309
- const parsed = import_contracts2.ProjectCreateInputSchema.safeParse({
1250
+ const parsed = import_contracts.ProjectCreateInputSchema.safeParse({
1310
1251
  name: local.name,
1311
1252
  type: local.type,
1312
1253
  websiteUrl: local.website,
@@ -1398,22 +1339,14 @@ async function runCli(argv = process.argv, overrides = {}) {
1398
1339
  "ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
1399
1340
  );
1400
1341
  }
1401
- const meta = await client.meta({ requestId: context.requestId });
1402
- if (!meta.authentication.cliBrowserAuthorization) {
1403
- throw new TypeError(
1404
- "This Rolino server does not support browser CLI authorization."
1405
- );
1406
- }
1407
- const credential = await loginWithBrowser({
1342
+ const tokenSet = await loginWithBrowser({
1408
1343
  baseUrl: client.baseUrl,
1409
1344
  stderr: runtime.stderr,
1410
- exchange: (input) => client.auth.exchangeCliAuthorization(input, {
1411
- requestId: context.requestId
1412
- })
1345
+ fetch: runtime.fetch
1413
1346
  });
1414
1347
  const authenticatedClient = new import_sdk.RolinoClient({
1415
1348
  baseUrl: client.baseUrl,
1416
- token: credential.token,
1349
+ token: tokenSet.accessToken,
1417
1350
  timeoutMs: client.timeoutMs,
1418
1351
  fetch: runtime.fetch
1419
1352
  });
@@ -1422,12 +1355,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1422
1355
  const actor = await authenticatedClient.whoami({
1423
1356
  requestId: context.requestId
1424
1357
  });
1425
- const path = (0, import_local_auth.saveStoredCredential)(client.baseUrl, {
1426
- token: credential.token,
1427
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1428
- expiresAt: credential.expiresAt,
1429
- organization: credential.organization
1430
- }, runtime.env);
1358
+ const path = (0, import_local_auth2.saveOAuthTokenSet)(tokenSet, runtime.env);
1431
1359
  credentialSaved = true;
1432
1360
  writeSuccess(
1433
1361
  context,
@@ -1436,23 +1364,20 @@ async function runCli(argv = process.argv, overrides = {}) {
1436
1364
  user: actor.user,
1437
1365
  organization: actor.organization,
1438
1366
  capabilities: actor.capabilities,
1439
- expiresAt: credential.expiresAt
1367
+ expiresAt: tokenSet.accessTokenExpiresAt
1440
1368
  },
1441
1369
  [
1442
1370
  `Authenticated as ${actor.user.email}.`,
1443
1371
  `Workspace: ${actor.organization.name}`,
1444
- `Expires: ${credential.expiresAt}`,
1372
+ `Access token expires: ${tokenSet.accessTokenExpiresAt}`,
1445
1373
  `Credential saved in a permission-restricted file at ${path}`
1446
1374
  ].join("\n"),
1447
1375
  ["rolino whoami --agent", "rolino projects list --agent"]
1448
1376
  );
1449
1377
  } catch (error) {
1450
1378
  if (credentialSaved) {
1451
- (0, import_local_auth.clearStoredCredential)(client.baseUrl, runtime.env);
1379
+ (0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
1452
1380
  }
1453
- await authenticatedClient.auth.revokeCurrentCredential({
1454
- requestId: context.requestId
1455
- }).catch(() => void 0);
1456
1381
  throw error;
1457
1382
  }
1458
1383
  }
@@ -1465,7 +1390,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1465
1390
  global,
1466
1391
  runtime,
1467
1392
  async action(context, client) {
1468
- const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
1393
+ const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
1469
1394
  if (credential.source === "none") {
1470
1395
  writeSuccess(
1471
1396
  context,
@@ -1484,7 +1409,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1484
1409
  baseUrl: client.baseUrl,
1485
1410
  user: actor.user,
1486
1411
  organization: actor.organization,
1487
- ...credential.source === "stored" ? { expiresAt: credential.credential.expiresAt } : {}
1412
+ ...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
1488
1413
  },
1489
1414
  [
1490
1415
  `Authenticated as ${actor.user.email}.`,
@@ -1502,7 +1427,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1502
1427
  global,
1503
1428
  runtime,
1504
1429
  async action(context, client) {
1505
- const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
1430
+ const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
1506
1431
  if (credential.source === "environment") {
1507
1432
  throw new TypeError(
1508
1433
  "ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
@@ -1516,15 +1441,36 @@ async function runCli(argv = process.argv, overrides = {}) {
1516
1441
  );
1517
1442
  return;
1518
1443
  }
1519
- try {
1520
- await client.auth.revokeCurrentCredential({
1521
- requestId: context.requestId
1444
+ const tokenSet = credential.tokenSet;
1445
+ const fetchImplementation = runtime.fetch ?? globalThis.fetch;
1446
+ const accessToken = await (0, import_local_auth2.createOAuthAccessTokenProvider)({
1447
+ baseUrl: client.baseUrl,
1448
+ env: runtime.env,
1449
+ fetch: fetchImplementation
1450
+ })();
1451
+ if (accessToken) {
1452
+ const disconnect = await fetchImplementation(`${client.baseUrl}/api/v1/oauth/logout`, {
1453
+ method: "POST",
1454
+ headers: {
1455
+ authorization: `Bearer ${accessToken}`,
1456
+ accept: "application/json",
1457
+ "x-request-id": context.requestId
1458
+ }
1522
1459
  });
1523
- } catch (error) {
1524
- const alreadyUnavailable = error instanceof import_sdk.RolinoApiError && (error.code === "AUTH_REQUIRED" || error.code === "NOT_FOUND");
1525
- if (!alreadyUnavailable) throw error;
1460
+ if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
1461
+ throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
1462
+ }
1526
1463
  }
1527
- (0, import_local_auth.clearStoredCredential)(client.baseUrl, runtime.env);
1464
+ await fetchImplementation(`${tokenSet.issuer}/oauth2/revoke`, {
1465
+ method: "POST",
1466
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1467
+ body: new URLSearchParams({
1468
+ token: tokenSet.refreshToken,
1469
+ token_type_hint: "refresh_token",
1470
+ client_id: tokenSet.clientId
1471
+ })
1472
+ }).catch(() => void 0);
1473
+ (0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
1528
1474
  writeSuccess(
1529
1475
  context,
1530
1476
  { revoked: true, baseUrl: client.baseUrl },
@@ -1603,7 +1549,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1603
1549
  });
1604
1550
  });
1605
1551
  const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
1606
- posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or LINKEDIN; 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("--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) => {
1552
+ 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) => {
1607
1553
  const global = program.opts();
1608
1554
  commandExitCode = await execute({
1609
1555
  command: "posts create",
@@ -1630,7 +1576,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1630
1576
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1631
1577
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1632
1578
  ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1633
- ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1579
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
1580
+ ...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
1634
1581
  },
1635
1582
  tiktokSettings: tiktokSettings(local) ?? null,
1636
1583
  youtubeSettings: youtubeSettings(local)
@@ -1648,7 +1595,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1648
1595
  }
1649
1596
  });
1650
1597
  });
1651
- 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) => {
1598
+ 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) => {
1652
1599
  const global = program.opts();
1653
1600
  commandExitCode = await execute({
1654
1601
  command: "posts update",
@@ -1677,7 +1624,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1677
1624
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1678
1625
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1679
1626
  ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1680
- ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1627
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
1628
+ ...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
1681
1629
  };
1682
1630
  const resolvedTikTokSettings = tiktokSettings(local);
1683
1631
  const resolvedYouTubeSettings = youtubeSettings(local);
@@ -1825,7 +1773,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1825
1773
  });
1826
1774
  });
1827
1775
  const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
1828
- 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 LINKEDIN; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1776
+ 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) => {
1829
1777
  const global = program.opts();
1830
1778
  commandExitCode = await execute({
1831
1779
  command: "posts publish preview",
@@ -1982,6 +1930,52 @@ async function runCli(argv = process.argv, overrides = {}) {
1982
1930
  writeSuccess(context, data, JSON.stringify(data, null, 2));
1983
1931
  } });
1984
1932
  });
1933
+ const blogWebhook = blogPublishing.command("webhook").description("Configure a signed custom Blog publishing webhook");
1934
+ const addWebhookChangeOptions = (command, executeChange) => {
1935
+ 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");
1936
+ 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");
1937
+ return command;
1938
+ };
1939
+ addWebhookChangeOptions(blogWebhook.command("preview"), false).action(async (local) => {
1940
+ const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
1941
+ const global = program.opts();
1942
+ commandExitCode = await execute({ command: "blog destinations webhook preview", global, runtime, async action(context, client) {
1943
+ const data = await client.blog.publishing.webhook.preview(local.project, input, { requestId: context.requestId });
1944
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1945
+ } });
1946
+ });
1947
+ addWebhookChangeOptions(blogWebhook.command("execute"), true).action(async (local) => {
1948
+ const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
1949
+ const global = program.opts();
1950
+ commandExitCode = await execute({ command: "blog destinations webhook execute", global, runtime, async action(context, client) {
1951
+ 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.` });
1952
+ const data = await client.blog.publishing.webhook.execute(local.project, { ...input, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
1953
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1954
+ } });
1955
+ });
1956
+ 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) => {
1957
+ const global = program.opts();
1958
+ commandExitCode = await execute({ command: "blog destinations webhook test", global, runtime, async action(context, client) {
1959
+ const data = await client.blog.publishing.webhook.test(local.project, { siteId: local.site, destinationId: local.destination, makePrimary: local.makePrimary }, { requestId: context.requestId });
1960
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1961
+ } });
1962
+ });
1963
+ const blogWebhookRotate = blogWebhook.command("rotate").description("Rotate the server-only signing secret");
1964
+ 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) => {
1965
+ const global = program.opts();
1966
+ commandExitCode = await execute({ command: "blog destinations webhook rotate preview", global, runtime, async action(context, client) {
1967
+ const data = await client.blog.publishing.webhook.previewRotation(local.project, { siteId: local.site, destinationId: local.destination }, { requestId: context.requestId });
1968
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1969
+ } });
1970
+ });
1971
+ 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) => {
1972
+ const global = program.opts();
1973
+ commandExitCode = await execute({ command: "blog destinations webhook rotate execute", global, runtime, async action(context, client) {
1974
+ 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.` });
1975
+ 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 });
1976
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1977
+ } });
1978
+ });
1985
1979
  const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
1986
1980
  blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
1987
1981
  const global = program.opts();
@@ -2014,9 +2008,7 @@ async function runCli(argv = process.argv, overrides = {}) {
2014
2008
  });
2015
2009
  const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
2016
2010
  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) => {
2017
- const weekdayMap = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
2018
- const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
2019
- 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.");
2011
+ const weekdays = parseBlogWeekdays(local.weekdays);
2020
2012
  const global = program.opts();
2021
2013
  commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
2022
2014
  const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
@@ -2030,6 +2022,25 @@ async function runCli(argv = process.argv, overrides = {}) {
2030
2022
  writeSuccess(context, data, JSON.stringify(data, null, 2));
2031
2023
  } });
2032
2024
  });
2025
+ const blogPlanCadence = blogPlan.command("cadence").description("Preview or apply editorial cadence changes without publishing");
2026
+ 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) => {
2027
+ const global = program.opts();
2028
+ commandExitCode = await execute({ command: "blog plan cadence preview", global, runtime, async action(context, client) {
2029
+ const data = await client.blog.plans.previewCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion }, { requestId: context.requestId });
2030
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2031
+ } });
2032
+ });
2033
+ 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) => {
2034
+ const global = program.opts();
2035
+ commandExitCode = await execute({ command: "blog plan cadence apply", global, runtime, async action(context, client) {
2036
+ await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed editorial cadence change?
2037
+ Project: ${local.project}
2038
+ Plan: ${local.plan}
2039
+ This moves eligible editorial dates only. It does not schedule, publish, or unpublish an article.` });
2040
+ 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 });
2041
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2042
+ } });
2043
+ });
2033
2044
  blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
2034
2045
  const global = program.opts();
2035
2046
  commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
@@ -2110,7 +2121,7 @@ async function runCli(argv = process.argv, overrides = {}) {
2110
2121
  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) => {
2111
2122
  const global = program.opts();
2112
2123
  commandExitCode = await execute({ command: "blog articles update", global, runtime, async action(context, client) {
2113
- const draft = import_contracts2.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2124
+ const draft = import_contracts.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2114
2125
  await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Save a new immutable Blog draft revision?
2115
2126
  Project: ${local.project}
2116
2127
  Article: ${articleId}
@@ -2129,6 +2140,103 @@ Article: ${articleId}` });
2129
2140
  writeSuccess(context, data, JSON.stringify(data, null, 2));
2130
2141
  } });
2131
2142
  });
2143
+ const blogImages = blog.command("images").description("Create and review Blog images with blog:write; cannot approve or publish");
2144
+ blogImages.command("generate").description("Queue one exact-revision featured image with blog:write; cannot approve or publish").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact Blog revision ID").option("--editorial-brief <text>", "bounded visual direction; provider controls stay on the server").option("--idempotency-key <key>", "stable retry key; defaults to request ID").action(async (articleId, local) => {
2145
+ const global = program.opts();
2146
+ commandExitCode = await execute({ command: "blog images generate", global, runtime, async action(context, client) {
2147
+ const data = await client.blog.articles.generateImage(local.project, articleId, { revisionId: local.revision, idempotencyKey: local.idempotencyKey ?? context.requestId, editorialBrief: local.editorialBrief }, { requestId: context.requestId });
2148
+ writeSuccess(context, data, `Blog image generation queued.
2149
+ Image: ${data.image.id}
2150
+ Job: ${data.job.id}`);
2151
+ } });
2152
+ });
2153
+ blogImages.command("upload").description("Upload one JPEG, PNG, or WebP for an exact Blog revision with blog:write; cannot approve or publish").argument("<article-id>").argument("<file-path>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact Blog revision ID").requiredOption("--alt-text <text>", "reviewable image alt text").action(async (articleId, filePath, local) => {
2154
+ const global = program.opts();
2155
+ commandExitCode = await execute({ command: "blog images upload", global, runtime, async action(context, client) {
2156
+ const absolutePath = (0, import_node_path2.resolve)(runtime.cwd, filePath);
2157
+ const details = await (0, import_promises2.stat)(absolutePath).catch(() => null);
2158
+ if (!details?.isFile()) throw new TypeError("The Blog image path must point to a readable file.");
2159
+ const contentTypes = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp" };
2160
+ const extension = (0, import_node_path2.extname)(absolutePath).toLowerCase();
2161
+ const contentType = contentTypes[extension];
2162
+ if (!contentType) throw new TypeError("Use a JPEG, PNG, or WebP Blog image.");
2163
+ const body = await (0, import_node_fs2.openAsBlob)(absolutePath, { type: contentType });
2164
+ const data = await client.blog.articles.uploadImage(local.project, articleId, { revisionId: local.revision, fileName: (0, import_node_path2.basename)(absolutePath), contentType, fileSize: details.size, altText: local.altText, body }, { requestId: context.requestId });
2165
+ writeSuccess(context, data, `Blog image uploaded for review.
2166
+ Image: ${data.id}
2167
+ State: ${data.state}`);
2168
+ } });
2169
+ });
2170
+ blogImages.command("review").description("Review one exact Blog image with blog:write; does not approve the article or publish").argument("<article-id>").argument("<image-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--decision <decision>", "APPROVED or REJECTED").requiredOption("--expected-version <version>", "current image version", positiveInteger).option("--alt-text <text>", "final image alt text; required by the server for approval").option("--idempotency-key <key>", "stable retry key; defaults to request ID").action(async (articleId, imageId, local) => {
2171
+ const decision = local.decision.toUpperCase();
2172
+ if (decision !== "APPROVED" && decision !== "REJECTED") throw new TypeError("--decision must be APPROVED or REJECTED.");
2173
+ const global = program.opts();
2174
+ commandExitCode = await execute({ command: "blog images review", global, runtime, async action(context, client) {
2175
+ const data = await client.blog.articles.reviewImage(local.project, articleId, imageId, { decision, expectedVersion: local.expectedVersion, altText: local.altText ?? "", idempotencyKey: local.idempotencyKey ?? context.requestId }, { requestId: context.requestId });
2176
+ writeSuccess(context, data, `Blog image review saved.
2177
+ Image: ${data.id}
2178
+ State: ${data.state}`);
2179
+ } });
2180
+ });
2181
+ const blogApprove = blog.command("approve").description("Approve one exact Blog bundle with blog:approve; cannot publish");
2182
+ blogApprove.command("preview").description("Preview one exact approval bundle with blog:approve; creates a confirmation but cannot publish").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact Blog revision ID").action(async (articleId, local) => {
2183
+ const global = program.opts();
2184
+ commandExitCode = await execute({ command: "blog approve preview", global, runtime, async action(context, client) {
2185
+ const data = await client.blog.articles.previewApproval(local.project, articleId, { revisionId: local.revision }, { requestId: context.requestId });
2186
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2187
+ } });
2188
+ });
2189
+ blogApprove.command("execute").description("Approve the confirmed exact bundle with blog:approve; separate blog:publish access is still required").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "same exact revision used for preview").requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm exact bundle approval").action(async (articleId, local) => {
2190
+ const global = program.opts();
2191
+ commandExitCode = await execute({ command: "blog approve execute", global, runtime, async action(context, client) {
2192
+ requireBlogExecutionConsent({ yes: local.yes, global, runtime });
2193
+ const data = await client.blog.articles.executeApproval(local.project, articleId, { revisionId: local.revision, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
2194
+ writeSuccess(context, data, `Exact Blog bundle approved.
2195
+ Revision: ${data.revisionId}
2196
+ Publishing still requires blog:publish.`);
2197
+ } });
2198
+ });
2199
+ const blogSchedule = blog.command("schedule").description("Schedule exact approved Blog bundles with blog:publish; does not grant approval");
2200
+ const addScheduleOptions = (command, executeSchedule) => {
2201
+ command.argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact approved Blog revision ID").requiredOption("--at <iso>", "future ISO date-time with UTC offset").requiredOption("--timezone <iana>", "explicit IANA timezone").option("--destination <destination-id...>", "exact destination IDs; defaults to the current primary");
2202
+ if (executeSchedule) command.requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm future external publication");
2203
+ return command;
2204
+ };
2205
+ addScheduleOptions(blogSchedule.command("preview").description("Preview an exact future Blog schedule with blog:publish; does not publish now"), false).action(async (articleId, local) => {
2206
+ const global = program.opts();
2207
+ commandExitCode = await execute({ command: "blog schedule preview", global, runtime, async action(context, client) {
2208
+ const data = await client.blog.articles.previewSchedule(local.project, articleId, { revisionId: local.revision, scheduledAt: local.at, timezone: local.timezone, destinationIds: local.destination }, { requestId: context.requestId });
2209
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2210
+ } });
2211
+ });
2212
+ addScheduleOptions(blogSchedule.command("execute").description("Execute a confirmed future Blog schedule with blog:publish; causes future external publication"), true).action(async (articleId, local) => {
2213
+ const global = program.opts();
2214
+ commandExitCode = await execute({ command: "blog schedule execute", global, runtime, async action(context, client) {
2215
+ requireBlogExecutionConsent({ yes: local.yes, global, runtime });
2216
+ const data = await client.blog.articles.executeSchedule(local.project, articleId, { revisionId: local.revision, scheduledAt: local.at, timezone: local.timezone, destinationIds: local.destination, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
2217
+ writeSuccess(context, data, `Blog article scheduled.
2218
+ Time: ${data.scheduledAt}
2219
+ Timezone: ${data.timezone}`);
2220
+ } });
2221
+ });
2222
+ const blogScheduleCancel = blogSchedule.command("cancel").description("Cancel one future Blog schedule with blog:publish; never unpublishes a live article");
2223
+ blogScheduleCancel.command("preview").description("Preview exact schedule cancellation with blog:publish; does not unpublish").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (articleId, local) => {
2224
+ const global = program.opts();
2225
+ commandExitCode = await execute({ command: "blog schedule cancel preview", global, runtime, async action(context, client) {
2226
+ const data = await client.blog.articles.previewScheduleCancellation(local.project, articleId, { requestId: context.requestId });
2227
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2228
+ } });
2229
+ });
2230
+ blogScheduleCancel.command("execute").description("Cancel the confirmed future schedule with blog:publish; never unpublishes a live article").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm schedule cancellation").action(async (articleId, local) => {
2231
+ const global = program.opts();
2232
+ commandExitCode = await execute({ command: "blog schedule cancel execute", global, runtime, async action(context, client) {
2233
+ requireBlogExecutionConsent({ yes: local.yes, global, runtime });
2234
+ const data = await client.blog.articles.executeScheduleCancellation(local.project, articleId, { confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
2235
+ writeSuccess(context, data, `Future Blog schedule canceled.
2236
+ Article: ${data.articleId}
2237
+ A live article was not unpublished.`);
2238
+ } });
2239
+ });
2132
2240
  blog.command("job").argument("<job-id>").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (jobId, local) => {
2133
2241
  const global = program.opts();
2134
2242
  commandExitCode = await execute({ command: "blog job", global, runtime, async action(context, client) {
@@ -2252,7 +2360,7 @@ Revision: ${local.revision}` });
2252
2360
  message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
2253
2361
  }
2254
2362
  ];
2255
- const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
2363
+ const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
2256
2364
  if (credential.source !== "none") {
2257
2365
  const actor = await client.whoami({ requestId: context.requestId });
2258
2366
  checks.push({