@rolino/cli 0.5.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/bin.cjs CHANGED
@@ -34,7 +34,7 @@ var import_commander = require("commander");
34
34
  // package.json
35
35
  var package_default = {
36
36
  name: "@rolino/cli",
37
- version: "0.5.0",
37
+ version: "0.6.0",
38
38
  description: "Agent-friendly command-line interface for Rolino",
39
39
  type: "module",
40
40
  license: "MIT",
@@ -91,14 +91,14 @@ var package_default = {
91
91
  dev: "tsx src/bin.ts"
92
92
  },
93
93
  dependencies: {
94
- "@rolino/contracts": "0.5.0",
95
- "@rolino/local-auth": "0.5.0",
96
- "@rolino/sdk": "0.5.0",
94
+ "@rolino/contracts": "0.6.0",
95
+ "@rolino/local-auth": "0.6.0",
96
+ "@rolino/sdk": "0.6.0",
97
97
  commander: "^15.0.0",
98
98
  open: "^11.0.0"
99
99
  },
100
100
  devDependencies: {
101
- tsx: "^4.21.0"
101
+ tsx: "^4.23.12"
102
102
  },
103
103
  engines: {
104
104
  node: ">=20.19.0"
@@ -106,23 +106,25 @@ var package_default = {
106
106
  };
107
107
 
108
108
  // src/cli.ts
109
- var import_contracts2 = require("@rolino/contracts");
109
+ var import_contracts = require("@rolino/contracts");
110
110
  var import_sdk = require("@rolino/sdk");
111
111
 
112
112
  // src/browser-login.ts
113
113
  var import_node_crypto = require("crypto");
114
114
  var import_node_http = require("http");
115
- var import_node_os = require("os");
116
- var import_contracts = require("@rolino/contracts");
115
+ var import_local_auth = require("@rolino/local-auth");
117
116
  var import_open = __toESM(require("open"), 1);
118
- var PAGE_HEADERS = {
119
- "cache-control": "no-store",
120
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
121
- "content-type": "text/html; charset=utf-8",
122
- "referrer-policy": "no-referrer",
123
- "x-content-type-options": "nosniff"
124
- };
125
- 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>';
117
+ var OAUTH_CALLBACK_PORT = 48391;
118
+ var OAUTH_CALLBACK_PATH = "/oauth/callback";
119
+ var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
120
+ var DEFAULT_SCOPES = [
121
+ "offline_access",
122
+ "identity:read",
123
+ "projects:read",
124
+ "posts:read",
125
+ "integrations:read",
126
+ "calendar:read"
127
+ ];
126
128
  function escapeHtml(value) {
127
129
  return value.replace(/[&<>"']/g, (character) => ({
128
130
  "&": "&amp;",
@@ -132,86 +134,18 @@ function escapeHtml(value) {
132
134
  "'": "&#039;"
133
135
  })[character]);
134
136
  }
135
- function renderCliAuthorizationResultPage(options) {
136
- const tone = options.success ? "success" : "failure";
137
- const eyebrow = options.success ? "CLI handoff complete" : "CLI handoff paused";
138
- const detail = options.success ? '<div class="terminal" aria-label="Terminal authentication status"><div class="terminal-bar"><i></i><i></i><i></i><span>ROLINO CLI</span></div><div class="command"><b>$</b> rolino whoami</div><div class="terminal-result"><span>\u2713</span> Authenticated. Ready for your next command.</div></div>' : '<div class="return-note"><span>\u2192</span><div><strong>Return to your terminal</strong><small>Start the login flow again when you are ready.</small></div></div>';
139
- return `<!doctype html>
140
- <html lang="en">
141
- <head>
142
- <meta charset="utf-8">
143
- <meta name="viewport" content="width=device-width,initial-scale=1">
144
- <meta name="color-scheme" content="light">
145
- <title>${escapeHtml(options.title)} \xB7 Rolino</title>
146
- <style>
147
- *{box-sizing:border-box}
148
- html,body{height:100%;margin:0}
149
- body{min-height:100svh;overflow:hidden;background:#faf9f1;color:#0f172a;font-family:"Poppins","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif}
150
- .shell{position:relative;isolation:isolate;display:grid;min-height:100svh;grid-template-rows:auto 1fr auto;padding:clamp(18px,3vw,38px);overflow:hidden}
151
- .shape{position:absolute;z-index:-1;border-radius:999px;pointer-events:none}
152
- .shape-a{top:-160px;left:-130px;width:390px;height:390px;background:#facc1530;filter:blur(2px)}
153
- .shape-b{right:-100px;bottom:-160px;width:360px;height:360px;background:#34c78a20}
154
- .shape-c{right:9%;top:14%;width:22px;height:22px;background:#ef6a5b;transform:rotate(18deg);border-radius:7px}
155
- .topbar,.footer{display:flex;width:min(100%,1100px);margin:auto;align-items:center;justify-content:space-between}
156
- .brand{display:flex;align-items:center;gap:10px;color:#0f172a;font-size:22px;font-weight:800;letter-spacing:-.04em}
157
- .brand svg{display:block;width:36px;height:36px}
158
- .secure{display:flex;align-items:center;gap:8px;color:#52607a;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
159
- .secure-dot{display:block;width:8px;height:8px;border-radius:50%;background:#34c78a;box-shadow:0 0 0 5px #34c78a1f}
160
- .stage{display:grid;min-height:0;place-items:center;padding:12px 0}
161
- .panel{width:min(100%,590px);padding:clamp(26px,4vw,46px);text-align:center;background:#fffefa;border:1px solid #e3e6ed;border-radius:32px;box-shadow:0 36px 90px -52px #0f172a73;animation:arrive .55s cubic-bezier(.22,1,.36,1) both}
162
- .mascot-wrap{position:relative;display:grid;width:112px;height:96px;margin:0 auto 12px;place-items:center}
163
- .mascot-wrap:before{position:absolute;width:88px;height:88px;border-radius:50%;background:#fff4a8;content:"";box-shadow:0 0 0 14px #fff9d8}
164
- .mascot-wrap svg{position:relative;width:78px;height:78px;filter:drop-shadow(0 10px 12px #c58e0026);animation:mascot-arrive .65s .08s cubic-bezier(.22,1,.36,1) both}
165
- .eyebrow{display:inline-flex;align-items:center;gap:7px;margin:0 0 12px;padding:6px 11px;border-radius:999px;background:#fff8b8;color:#7a4d00;font-size:10px;font-weight:900;letter-spacing:.14em;text-transform:uppercase}
166
- .eyebrow:before{width:7px;height:7px;border-radius:50%;background:#34c78a;content:""}
167
- .failure .eyebrow{background:#fee6df;color:#9f2f24}
168
- .failure .eyebrow:before{background:#ef6a5b}
169
- h1{margin:0;color:#0f172a;font-family:"Outfit","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif;font-size:clamp(34px,5vw,48px);line-height:1;letter-spacing:-.045em}
170
- .message{max-width:450px;margin:14px auto 0;color:#52607a;font-size:15px;line-height:1.65}
171
- .terminal{max-width:450px;margin:24px auto 0;padding:14px 16px 16px;text-align:left;background:#111b30;color:#eff3fa;border:1px solid #293550;border-radius:18px;box-shadow:0 18px 42px -26px #0f172acc}
172
- .terminal-bar{display:flex;align-items:center;gap:6px;padding-bottom:12px;color:#8f9bb1;font-size:9px;font-weight:800;letter-spacing:.13em}
173
- .terminal-bar i{display:block;width:7px;height:7px;border-radius:50%;background:#ef6a5b}
174
- .terminal-bar i:nth-child(2){background:#facc15}
175
- .terminal-bar i:nth-child(3){margin-right:5px;background:#34c78a}
176
- .command{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
177
- .command b{color:#facc15}
178
- .terminal-result{display:flex;align-items:center;gap:8px;margin-top:9px;color:#aeb9ca;font-size:12px}
179
- .terminal-result span{color:#52d49e;font-weight:900}
180
- .return-note{display:flex;max-width:420px;margin:24px auto 0;padding:14px 16px;align-items:center;gap:12px;text-align:left;background:#fff2ed;border:1px solid #f6c9bd;border-radius:18px;color:#7e2f29}
181
- .return-note>span{font-size:24px}
182
- .return-note strong,.return-note small{display:block}
183
- .return-note small{margin-top:3px;color:#875b56}
184
- .footer{justify-content:center;color:#6f788a;font-size:11px}
185
- @keyframes arrive{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
186
- @keyframes mascot-arrive{from{opacity:0;transform:translateY(10px) rotate(-4deg) scale(.88)}to{opacity:1;transform:none}}
187
- @media(max-width:600px){.secure{display:none}.panel{border-radius:24px}.shape-c{display:none}}
188
- @media(max-height:700px){.shell{padding:16px}.stage{padding:6px 0}.panel{padding:20px 26px}.mascot-wrap{width:88px;height:72px;margin-bottom:6px}.mascot-wrap:before{width:68px;height:68px;box-shadow:0 0 0 10px #fff9d8}.mascot-wrap svg{width:60px;height:60px}.eyebrow{margin-bottom:8px}h1{font-size:32px}.message{margin-top:10px}.terminal,.return-note{margin-top:14px}}
189
- @media(prefers-reduced-motion:reduce){.panel,.mascot-wrap svg{animation:none}}
190
- </style>
191
- </head>
192
- <body class="${tone}">
193
- <main class="shell">
194
- <span class="shape shape-a"></span><span class="shape shape-b"></span><span class="shape shape-c"></span>
195
- <header class="topbar">
196
- <div class="brand">${ROLINO_MASCOT_SVG}<span>Rolino</span></div>
197
- <div class="secure"><span class="secure-dot"></span>Secure CLI authorization</div>
198
- </header>
199
- <section class="stage">
200
- <div class="panel">
201
- <div class="mascot-wrap">${ROLINO_MASCOT_SVG}</div>
202
- <p class="eyebrow">${eyebrow}</p>
203
- <h1>${escapeHtml(options.title)}</h1>
204
- <p class="message">${escapeHtml(options.message)}</p>
205
- ${detail}
206
- </div>
207
- </section>
208
- <footer class="footer">Secure browser handoff \xB7 no password shared</footer>
209
- </main>
210
- </body>
211
- </html>`;
137
+ function renderOAuthAuthorizationResultPage(options) {
138
+ const color = options.success ? "#087f5b" : "#b42318";
139
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>${escapeHtml(options.title)} \xB7 Rolino</title><style>body{min-height:100vh;margin:0;display:grid;place-items:center;background:#faf9f1;color:#0f172a;font:16px/1.5 system-ui,sans-serif}.card{width:min(34rem,calc(100% - 2rem));box-sizing:border-box;padding:2.5rem;border:1px solid #dfe3ea;border-radius:1.5rem;background:#fff;text-align:center;box-shadow:0 24px 70px #0f172a20}h1{margin:0 0 .75rem;color:${color};font-size:2rem}p{margin:0;color:#52607a}@media(prefers-color-scheme:dark){body{background:#111827;color:#f8fafc}.card{background:#182235;border-color:#344258}p{color:#cbd5e1}}</style></head><body><main class="card"><h1>${escapeHtml(options.title)}</h1><p>${escapeHtml(options.message)}</p></main></body></html>`;
212
140
  }
213
141
  function writePage(response, status, html) {
214
- response.writeHead(status, PAGE_HEADERS);
142
+ response.writeHead(status, {
143
+ "cache-control": "no-store",
144
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
145
+ "content-type": "text/html; charset=utf-8",
146
+ "referrer-policy": "no-referrer",
147
+ "x-content-type-options": "nosniff"
148
+ });
215
149
  response.end(html);
216
150
  }
217
151
  function secretsMatch(left, right) {
@@ -219,41 +153,63 @@ function secretsMatch(left, right) {
219
153
  const rightBytes = Buffer.from(right);
220
154
  return leftBytes.length === rightBytes.length && (0, import_node_crypto.timingSafeEqual)(leftBytes, rightBytes);
221
155
  }
222
- function createBrowserAuthorizationRequest(options) {
156
+ function createBrowserAuthorizationRequest(baseUrl) {
157
+ const configuration = (0, import_local_auth.oauthConfiguration)(baseUrl);
223
158
  const state = (0, import_node_crypto.randomBytes)(32).toString("base64url");
224
159
  const codeVerifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
225
160
  const codeChallenge = (0, import_node_crypto.createHash)("sha256").update(codeVerifier).digest("base64url");
226
- const url = new URL("/cli/authorize", options.baseUrl);
227
- const requestExpiresAt = new Date(
228
- Date.now() + import_contracts.CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS
229
- ).toISOString();
230
- url.searchParams.set("redirect_uri", options.redirectUri);
161
+ const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
162
+ const url = new URL(`${configuration.issuer}/oauth2/authorize`);
163
+ url.searchParams.set("response_type", "code");
164
+ url.searchParams.set("client_id", configuration.clientId);
165
+ url.searchParams.set("redirect_uri", redirectUri);
166
+ url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
167
+ url.searchParams.set("resource", configuration.resource);
231
168
  url.searchParams.set("state", state);
232
169
  url.searchParams.set("code_challenge", codeChallenge);
233
170
  url.searchParams.set("code_challenge_method", "S256");
234
- url.searchParams.set(
235
- "client_name",
236
- (options.clientName ?? `Rolino CLI on ${(0, import_node_os.hostname)()}`).slice(0, 32)
237
- );
238
- url.searchParams.set("request_expires_at", requestExpiresAt);
239
- return {
240
- url: url.toString(),
241
- state,
242
- codeVerifier,
243
- codeChallenge,
244
- requestExpiresAt
245
- };
171
+ return { configuration, url: url.toString(), redirectUri, state, codeVerifier, codeChallenge };
246
172
  }
247
- var CliAuthorizationDeniedError = class extends Error {
173
+ var OAuthAuthorizationDeniedError = class extends Error {
248
174
  constructor() {
249
175
  super("Authorization was denied.");
250
- this.name = "CliAuthorizationDeniedError";
176
+ this.name = "OAuthAuthorizationDeniedError";
251
177
  }
252
178
  };
179
+ async function exchangeAuthorizationCode(options) {
180
+ const { authorization } = options;
181
+ const response = await options.fetch(`${authorization.configuration.issuer}/oauth2/token`, {
182
+ method: "POST",
183
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
184
+ body: new URLSearchParams({
185
+ grant_type: "authorization_code",
186
+ code: options.code,
187
+ redirect_uri: authorization.redirectUri,
188
+ client_id: authorization.configuration.clientId,
189
+ code_verifier: authorization.codeVerifier,
190
+ resource: authorization.configuration.resource
191
+ })
192
+ });
193
+ const payload = await response.json().catch(() => null);
194
+ if (!response.ok || !payload || typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string") throw new Error("Rolino could not exchange the OAuth authorization code.");
195
+ const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
196
+ return {
197
+ host: authorization.configuration.host,
198
+ issuer: authorization.configuration.issuer,
199
+ clientId: authorization.configuration.clientId,
200
+ resource: authorization.configuration.resource,
201
+ accessToken: payload.access_token,
202
+ accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
203
+ refreshToken: payload.refresh_token,
204
+ refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
205
+ scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...DEFAULT_SCOPES]
206
+ };
207
+ }
253
208
  async function loginWithBrowser(options) {
209
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
254
210
  return new Promise((resolve3, reject) => {
255
211
  let settled = false;
256
- let authorization = null;
212
+ const authorization = createBrowserAuthorizationRequest(options.baseUrl);
257
213
  const finish = (callback) => {
258
214
  if (settled) return;
259
215
  settled = true;
@@ -263,82 +219,46 @@ async function loginWithBrowser(options) {
263
219
  };
264
220
  const server = (0, import_node_http.createServer)((request, response) => {
265
221
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
266
- if (request.method !== "GET" || url.pathname !== "/callback") {
222
+ if (request.method !== "GET" || url.pathname !== OAUTH_CALLBACK_PATH) {
267
223
  response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
268
224
  response.end("Not found");
269
225
  return;
270
226
  }
271
227
  void (async () => {
272
- if (!authorization) {
273
- writePage(response, 503, renderCliAuthorizationResultPage({
274
- title: "Authorization is starting",
275
- message: "Return to your terminal and try the browser link again in a moment.",
276
- success: false
277
- }));
278
- return;
279
- }
280
228
  if (!secretsMatch(url.searchParams.get("state") ?? "", authorization.state)) {
281
- writePage(response, 400, renderCliAuthorizationResultPage({
282
- title: "Authorization failed",
283
- message: "The callback state did not match. Return to your terminal and try again.",
284
- success: false
285
- }));
286
- finish(() => reject(new Error("The browser callback state did not match.")));
229
+ writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The callback state did not match.", success: false }));
230
+ finish(() => reject(new Error("The OAuth callback state did not match.")));
287
231
  return;
288
232
  }
289
233
  if (url.searchParams.get("error") === "access_denied") {
290
- writePage(response, 200, renderCliAuthorizationResultPage({
291
- title: "Access denied",
292
- message: "No credential was created. You can close this window and return to your terminal.",
293
- success: false
294
- }));
295
- finish(() => reject(new CliAuthorizationDeniedError()));
234
+ writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Access denied", message: "No OAuth token was saved.", success: false }));
235
+ finish(() => reject(new OAuthAuthorizationDeniedError()));
236
+ return;
237
+ }
238
+ if (url.searchParams.get("iss") !== authorization.configuration.issuer) {
239
+ writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The authorization server did not match.", success: false }));
240
+ finish(() => reject(new Error("The OAuth callback issuer did not match.")));
296
241
  return;
297
242
  }
298
243
  const code = url.searchParams.get("code");
299
- if (!code || !/^[A-Za-z0-9_-]{43,128}$/.test(code)) {
300
- writePage(response, 400, renderCliAuthorizationResultPage({
301
- title: "Authorization failed",
302
- message: "Rolino did not return a valid one-time code. Return to your terminal and try again.",
303
- success: false
304
- }));
305
- finish(() => reject(new Error("The browser callback did not include a valid code.")));
244
+ if (!code || code.length > 2048) {
245
+ writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "Rolino did not return a valid code.", success: false }));
246
+ finish(() => reject(new Error("The OAuth callback did not include a valid code.")));
306
247
  return;
307
248
  }
308
249
  try {
309
- const credential = await options.exchange({
310
- code,
311
- codeVerifier: authorization.codeVerifier
312
- });
313
- writePage(response, 200, renderCliAuthorizationResultPage({
314
- title: "Terminal connected",
315
- message: "Rolino is ready in your terminal. You can close this window.",
316
- success: true
317
- }));
318
- finish(() => resolve3(credential));
250
+ const tokenSet = await exchangeAuthorizationCode({ authorization, code, fetch: fetchImplementation });
251
+ writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Terminal connected", message: "You can close this window and return to your terminal.", success: true }));
252
+ finish(() => resolve3(tokenSet));
319
253
  } catch (error) {
320
- writePage(response, 502, renderCliAuthorizationResultPage({
321
- title: "Authorization failed",
322
- message: "The CLI could not exchange its one-time code. Return to your terminal and try again.",
323
- success: false
324
- }));
254
+ writePage(response, 502, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The CLI could not exchange the code.", success: false }));
325
255
  finish(() => reject(error));
326
256
  }
327
257
  })();
328
258
  });
329
259
  server.once("error", (error) => finish(() => reject(error)));
330
- server.listen(0, "127.0.0.1", () => {
331
- const address = server.address();
332
- if (!address || typeof address === "string") {
333
- finish(() => reject(new Error("Rolino could not start the local authorization callback.")));
334
- return;
335
- }
336
- authorization = createBrowserAuthorizationRequest({
337
- baseUrl: options.baseUrl,
338
- redirectUri: `http://127.0.0.1:${address.port}/callback`
339
- });
340
- options.stderr.write(`Opening Rolino in your browser\u2026
341
- `);
260
+ server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
261
+ options.stderr.write("Opening Rolino OAuth sign-in in your browser\u2026\n");
342
262
  options.stderr.write(`If it does not open, visit:
343
263
  ${authorization.url}
344
264
  `);
@@ -346,13 +266,13 @@ ${authorization.url}
346
266
  void (0, import_open.default)(authorization.url).catch(() => void 0);
347
267
  });
348
268
  const timeout = setTimeout(() => {
349
- finish(() => reject(new Error("Browser authorization timed out after 5 minutes.")));
350
- }, import_contracts.CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS);
269
+ finish(() => reject(new Error("OAuth sign-in timed out after 5 minutes.")));
270
+ }, OAUTH_TIMEOUT_MS);
351
271
  });
352
272
  }
353
273
 
354
274
  // src/cli.ts
355
- var import_local_auth = require("@rolino/local-auth");
275
+ var import_local_auth2 = require("@rolino/local-auth");
356
276
 
357
277
  // src/exit-codes.ts
358
278
  var EXIT_CODES = {
@@ -634,12 +554,12 @@ var import_node_child_process = require("child_process");
634
554
  var import_node_fs = require("fs");
635
555
  var import_promises = require("fs/promises");
636
556
  var import_node_module = require("module");
637
- var import_node_os2 = require("os");
557
+ var import_node_os = require("os");
638
558
  var import_node_path = require("path");
639
559
  var CODEX_BEGIN = "# BEGIN ROLINO MCP (managed by rolino setup mcp)";
640
560
  var CODEX_END = "# END ROLINO MCP";
641
561
  function homeDirectory(env) {
642
- return env.USERPROFILE || env.HOME || (0, import_node_os2.homedir)();
562
+ return env.USERPROFILE || env.HOME || (0, import_node_os.homedir)();
643
563
  }
644
564
  function tomlString(value) {
645
565
  return JSON.stringify(value);
@@ -905,40 +825,40 @@ function outputFormat(value) {
905
825
  throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
906
826
  }
907
827
  function postStatus(value) {
908
- const parsed = import_contracts2.PostStatusSchema.safeParse(value.toUpperCase());
828
+ const parsed = import_contracts.PostStatusSchema.safeParse(value.toUpperCase());
909
829
  if (parsed.success) return parsed.data;
910
830
  throw new import_commander.InvalidArgumentError(
911
- `Status must be one of ${import_contracts2.PostStatusSchema.options.join(", ")}.`
831
+ `Status must be one of ${import_contracts.PostStatusSchema.options.join(", ")}.`
912
832
  );
913
833
  }
914
834
  function seoOpportunityKind(value) {
915
- const parsed = import_contracts2.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
835
+ const parsed = import_contracts.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
916
836
  if (parsed.success) return parsed.data;
917
- throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts2.SeoOpportunityKindSchema.options.join(", ")}.`);
837
+ throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts.SeoOpportunityKindSchema.options.join(", ")}.`);
918
838
  }
919
839
  function seoExpectedImpact(value) {
920
- const parsed = import_contracts2.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
840
+ const parsed = import_contracts.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
921
841
  if (parsed.success) return parsed.data;
922
842
  throw new import_commander.InvalidArgumentError("SEO impact must be HIGH, MEDIUM, or LOW.");
923
843
  }
924
844
  function seoReportCompleteness(value) {
925
- const parsed = import_contracts2.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
845
+ const parsed = import_contracts.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
926
846
  if (parsed.success) return parsed.data;
927
847
  throw new import_commander.InvalidArgumentError("Report status must be COMPLETE or PARTIAL.");
928
848
  }
929
849
  function projectType(value) {
930
- const parsed = import_contracts2.ProjectTypeSchema.safeParse(value.toUpperCase());
850
+ const parsed = import_contracts.ProjectTypeSchema.safeParse(value.toUpperCase());
931
851
  if (parsed.success) return parsed.data;
932
852
  throw new import_commander.InvalidArgumentError(
933
- `Type must be one of ${import_contracts2.ProjectTypeSchema.options.join(", ")}.`
853
+ `Type must be one of ${import_contracts.ProjectTypeSchema.options.join(", ")}.`
934
854
  );
935
855
  }
936
856
  function publishingProvider(value) {
937
857
  const normalized = value.toUpperCase();
938
- const parsed = import_contracts2.PublishingProviderSchema.safeParse(normalized);
858
+ const parsed = import_contracts.PublishingProviderSchema.safeParse(normalized);
939
859
  if (parsed.success) return parsed.data;
940
860
  throw new import_commander.InvalidArgumentError(
941
- `Platform must be one of ${import_contracts2.PublishingProviderSchema.options.join(", ")}.`
861
+ `Platform must be one of ${import_contracts.PublishingProviderSchema.options.join(", ")}.`
942
862
  );
943
863
  }
944
864
  function collectPublishingProvider(value, previous) {
@@ -949,10 +869,10 @@ function collectString(value, previous) {
949
869
  }
950
870
  function deliveryOptionsProvider(value) {
951
871
  const normalized = value.toUpperCase();
952
- const parsed = import_contracts2.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
872
+ const parsed = import_contracts.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
953
873
  if (parsed.success) return parsed.data;
954
874
  throw new import_commander.InvalidArgumentError(
955
- `Provider must be one of ${import_contracts2.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
875
+ `Provider must be one of ${import_contracts.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
956
876
  );
957
877
  }
958
878
  function tiktokPostMode(value) {
@@ -990,7 +910,7 @@ function yesOrNo(value) {
990
910
  function youtubeSettings(options) {
991
911
  const hasYouTubeOptions = options.youtubeTitle !== void 0 || options.youtubeCategoryId !== void 0 || options.youtubePrivacy !== void 0 || options.youtubeMadeForKids !== void 0 || options.youtubeSyntheticMedia === true || options.youtubeNotifySubscribers === false || (options.youtubeTag?.length ?? 0) > 0;
992
912
  if (!hasYouTubeOptions) return null;
993
- return import_contracts2.YouTubePostSettingsSchema.parse({
913
+ return import_contracts.YouTubePostSettingsSchema.parse({
994
914
  title: options.youtubeTitle,
995
915
  categoryId: options.youtubeCategoryId,
996
916
  privacyStatus: options.youtubePrivacy,
@@ -1053,6 +973,23 @@ async function requireWriteConsent(options) {
1053
973
  );
1054
974
  }
1055
975
  }
976
+ var BLOG_WEEKDAY_VALUES = {
977
+ sun: 0,
978
+ mon: 1,
979
+ tue: 2,
980
+ wed: 3,
981
+ thu: 4,
982
+ fri: 5,
983
+ sat: 6
984
+ };
985
+ function parseBlogWeekdays(value) {
986
+ const names = value.split(",").map((day) => day.trim().toLowerCase());
987
+ const weekdays = names.map((day) => BLOG_WEEKDAY_VALUES[day]).filter((day) => day !== void 0);
988
+ if (!weekdays.length || weekdays.length !== names.length || new Set(weekdays).size !== weekdays.length) {
989
+ throw new TypeError("--weekdays must contain unique comma-separated sun,mon,tue,wed,thu,fri,sat values.");
990
+ }
991
+ return weekdays;
992
+ }
1056
993
  function requireBlogExecutionConsent(options) {
1057
994
  if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
1058
995
  throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
@@ -1083,16 +1020,16 @@ function baseUrlFor(options, runtime) {
1083
1020
  function clientFor(options, runtime) {
1084
1021
  const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
1085
1022
  const baseUrl = baseUrlFor(options, runtime);
1086
- const credential = (0, import_local_auth.resolveCredential)(baseUrl, runtime.env);
1023
+ const token = runtime.env.ROLINO_TOKEN ?? (0, import_local_auth2.createOAuthAccessTokenProvider)({ baseUrl, env: runtime.env, fetch: runtime.fetch });
1087
1024
  return new import_sdk.RolinoClient({
1088
1025
  baseUrl,
1089
- token: credential.token ?? void 0,
1026
+ token,
1090
1027
  timeoutMs,
1091
1028
  fetch: runtime.fetch
1092
1029
  });
1093
1030
  }
1094
1031
  function exitCodeFor(error) {
1095
- if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1032
+ if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1096
1033
  return EXIT_CODES.cancelled;
1097
1034
  }
1098
1035
  if (error instanceof import_sdk.RolinoNetworkError) {
@@ -1125,7 +1062,7 @@ function exitCodeFor(error) {
1125
1062
  return EXIT_CODES.unexpected;
1126
1063
  }
1127
1064
  function errorForOutput(error) {
1128
- if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1065
+ if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1129
1066
  return { code: "CANCELLED", message: error.message };
1130
1067
  }
1131
1068
  if (error instanceof import_sdk.RolinoApiError) {
@@ -1150,7 +1087,7 @@ function errorForOutput(error) {
1150
1087
  function tiktokSettings(options) {
1151
1088
  const hasTikTokOptions = options.tiktokMode !== void 0 || options.tiktokVisibility !== void 0 || options.tiktokComments !== void 0 || options.tiktokDuet !== void 0 || options.tiktokStitch !== void 0 || options.tiktokCommercialContent !== void 0 || options.tiktokPromotesOwnBrand !== void 0 || options.tiktokPromotesThirdParty !== void 0 || options.tiktokAiGenerated !== void 0 || options.tiktokCoverTimestampMs !== void 0 || options.tiktokSettingsReviewed === true;
1152
1089
  if (!hasTikTokOptions) return void 0;
1153
- return import_contracts2.TikTokDraftSettingsSchema.parse({
1090
+ return import_contracts.TikTokDraftSettingsSchema.parse({
1154
1091
  postMode: options.tiktokMode,
1155
1092
  privacyLevel: options.tiktokVisibility,
1156
1093
  allowComment: options.tiktokComments,
@@ -1294,7 +1231,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1294
1231
  global,
1295
1232
  runtime,
1296
1233
  async action(context, client) {
1297
- const parsed = import_contracts2.ProjectCreateInputSchema.safeParse({
1234
+ const parsed = import_contracts.ProjectCreateInputSchema.safeParse({
1298
1235
  name: local.name,
1299
1236
  type: local.type,
1300
1237
  websiteUrl: local.website,
@@ -1386,22 +1323,14 @@ async function runCli(argv = process.argv, overrides = {}) {
1386
1323
  "ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
1387
1324
  );
1388
1325
  }
1389
- const meta = await client.meta({ requestId: context.requestId });
1390
- if (!meta.authentication.cliBrowserAuthorization) {
1391
- throw new TypeError(
1392
- "This Rolino server does not support browser CLI authorization."
1393
- );
1394
- }
1395
- const credential = await loginWithBrowser({
1326
+ const tokenSet = await loginWithBrowser({
1396
1327
  baseUrl: client.baseUrl,
1397
1328
  stderr: runtime.stderr,
1398
- exchange: (input) => client.auth.exchangeCliAuthorization(input, {
1399
- requestId: context.requestId
1400
- })
1329
+ fetch: runtime.fetch
1401
1330
  });
1402
1331
  const authenticatedClient = new import_sdk.RolinoClient({
1403
1332
  baseUrl: client.baseUrl,
1404
- token: credential.token,
1333
+ token: tokenSet.accessToken,
1405
1334
  timeoutMs: client.timeoutMs,
1406
1335
  fetch: runtime.fetch
1407
1336
  });
@@ -1410,12 +1339,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1410
1339
  const actor = await authenticatedClient.whoami({
1411
1340
  requestId: context.requestId
1412
1341
  });
1413
- const path = (0, import_local_auth.saveStoredCredential)(client.baseUrl, {
1414
- token: credential.token,
1415
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1416
- expiresAt: credential.expiresAt,
1417
- organization: credential.organization
1418
- }, runtime.env);
1342
+ const path = (0, import_local_auth2.saveOAuthTokenSet)(tokenSet, runtime.env);
1419
1343
  credentialSaved = true;
1420
1344
  writeSuccess(
1421
1345
  context,
@@ -1424,23 +1348,20 @@ async function runCli(argv = process.argv, overrides = {}) {
1424
1348
  user: actor.user,
1425
1349
  organization: actor.organization,
1426
1350
  capabilities: actor.capabilities,
1427
- expiresAt: credential.expiresAt
1351
+ expiresAt: tokenSet.accessTokenExpiresAt
1428
1352
  },
1429
1353
  [
1430
1354
  `Authenticated as ${actor.user.email}.`,
1431
1355
  `Workspace: ${actor.organization.name}`,
1432
- `Expires: ${credential.expiresAt}`,
1356
+ `Access token expires: ${tokenSet.accessTokenExpiresAt}`,
1433
1357
  `Credential saved in a permission-restricted file at ${path}`
1434
1358
  ].join("\n"),
1435
1359
  ["rolino whoami --agent", "rolino projects list --agent"]
1436
1360
  );
1437
1361
  } catch (error) {
1438
1362
  if (credentialSaved) {
1439
- (0, import_local_auth.clearStoredCredential)(client.baseUrl, runtime.env);
1363
+ (0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
1440
1364
  }
1441
- await authenticatedClient.auth.revokeCurrentCredential({
1442
- requestId: context.requestId
1443
- }).catch(() => void 0);
1444
1365
  throw error;
1445
1366
  }
1446
1367
  }
@@ -1453,7 +1374,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1453
1374
  global,
1454
1375
  runtime,
1455
1376
  async action(context, client) {
1456
- const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
1377
+ const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
1457
1378
  if (credential.source === "none") {
1458
1379
  writeSuccess(
1459
1380
  context,
@@ -1472,7 +1393,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1472
1393
  baseUrl: client.baseUrl,
1473
1394
  user: actor.user,
1474
1395
  organization: actor.organization,
1475
- ...credential.source === "stored" ? { expiresAt: credential.credential.expiresAt } : {}
1396
+ ...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
1476
1397
  },
1477
1398
  [
1478
1399
  `Authenticated as ${actor.user.email}.`,
@@ -1490,7 +1411,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1490
1411
  global,
1491
1412
  runtime,
1492
1413
  async action(context, client) {
1493
- const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
1414
+ const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
1494
1415
  if (credential.source === "environment") {
1495
1416
  throw new TypeError(
1496
1417
  "ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
@@ -1504,15 +1425,36 @@ async function runCli(argv = process.argv, overrides = {}) {
1504
1425
  );
1505
1426
  return;
1506
1427
  }
1507
- try {
1508
- await client.auth.revokeCurrentCredential({
1509
- requestId: context.requestId
1428
+ const tokenSet = credential.tokenSet;
1429
+ const fetchImplementation = runtime.fetch ?? globalThis.fetch;
1430
+ const accessToken = await (0, import_local_auth2.createOAuthAccessTokenProvider)({
1431
+ baseUrl: client.baseUrl,
1432
+ env: runtime.env,
1433
+ fetch: fetchImplementation
1434
+ })();
1435
+ if (accessToken) {
1436
+ const disconnect = await fetchImplementation(`${client.baseUrl}/api/v1/oauth/logout`, {
1437
+ method: "POST",
1438
+ headers: {
1439
+ authorization: `Bearer ${accessToken}`,
1440
+ accept: "application/json",
1441
+ "x-request-id": context.requestId
1442
+ }
1510
1443
  });
1511
- } catch (error) {
1512
- const alreadyUnavailable = error instanceof import_sdk.RolinoApiError && (error.code === "AUTH_REQUIRED" || error.code === "NOT_FOUND");
1513
- if (!alreadyUnavailable) throw error;
1444
+ if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
1445
+ throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
1446
+ }
1514
1447
  }
1515
- (0, import_local_auth.clearStoredCredential)(client.baseUrl, runtime.env);
1448
+ await fetchImplementation(`${tokenSet.issuer}/oauth2/revoke`, {
1449
+ method: "POST",
1450
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1451
+ body: new URLSearchParams({
1452
+ token: tokenSet.refreshToken,
1453
+ token_type_hint: "refresh_token",
1454
+ client_id: tokenSet.clientId
1455
+ })
1456
+ }).catch(() => void 0);
1457
+ (0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
1516
1458
  writeSuccess(
1517
1459
  context,
1518
1460
  { revoked: true, baseUrl: client.baseUrl },
@@ -1591,7 +1533,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1591
1533
  });
1592
1534
  });
1593
1535
  const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
1594
- posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or 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) => {
1536
+ posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, LINKEDIN, or GOOGLE_BUSINESS_PROFILE; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "existing project media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--google-business-profile-caption <text>", "Google Business Profile update override; limited to 1,500 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "YouTube tag; repeat as needed", collectString, []).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm creation without an interactive prompt").action(async (local) => {
1595
1537
  const global = program.opts();
1596
1538
  commandExitCode = await execute({
1597
1539
  command: "posts create",
@@ -1618,7 +1560,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1618
1560
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1619
1561
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1620
1562
  ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1621
- ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1563
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
1564
+ ...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
1622
1565
  },
1623
1566
  tiktokSettings: tiktokSettings(local) ?? null,
1624
1567
  youtubeSettings: youtubeSettings(local)
@@ -1636,7 +1579,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1636
1579
  }
1637
1580
  });
1638
1581
  });
1639
- posts.command("update").description("Update selected draft fields using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).option("--caption <text>", "replacement draft caption; omitted fields are preserved").option("--platform <provider>", "replacement destinations; repeat as needed", collectPublishingProvider).option("--clear-platforms", "remove every draft destination").option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString).option("--clear-media", "remove every media asset from the draft").option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "replacement YouTube tag; repeat as needed", collectString).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
1582
+ posts.command("update").description("Update selected draft fields using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).option("--caption <text>", "replacement draft caption; omitted fields are preserved").option("--platform <provider>", "replacement destinations; repeat as needed", collectPublishingProvider).option("--clear-platforms", "remove every draft destination").option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString).option("--clear-media", "remove every media asset from the draft").option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--google-business-profile-caption <text>", "Google Business Profile update override; limited to 1,500 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "replacement YouTube tag; repeat as needed", collectString).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
1640
1583
  const global = program.opts();
1641
1584
  commandExitCode = await execute({
1642
1585
  command: "posts update",
@@ -1665,7 +1608,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1665
1608
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1666
1609
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1667
1610
  ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1668
- ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1611
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
1612
+ ...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
1669
1613
  };
1670
1614
  const resolvedTikTokSettings = tiktokSettings(local);
1671
1615
  const resolvedYouTubeSettings = youtubeSettings(local);
@@ -1813,7 +1757,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1813
1757
  });
1814
1758
  });
1815
1759
  const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
1816
- publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or LINKEDIN; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1760
+ publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, LINKEDIN, or GOOGLE_BUSINESS_PROFILE; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1817
1761
  const global = program.opts();
1818
1762
  commandExitCode = await execute({
1819
1763
  command: "posts publish preview",
@@ -1970,6 +1914,52 @@ async function runCli(argv = process.argv, overrides = {}) {
1970
1914
  writeSuccess(context, data, JSON.stringify(data, null, 2));
1971
1915
  } });
1972
1916
  });
1917
+ const blogWebhook = blogPublishing.command("webhook").description("Configure a signed custom Blog publishing webhook");
1918
+ const addWebhookChangeOptions = (command, executeChange) => {
1919
+ 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");
1920
+ 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");
1921
+ return command;
1922
+ };
1923
+ addWebhookChangeOptions(blogWebhook.command("preview"), false).action(async (local) => {
1924
+ const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
1925
+ const global = program.opts();
1926
+ commandExitCode = await execute({ command: "blog destinations webhook preview", global, runtime, async action(context, client) {
1927
+ const data = await client.blog.publishing.webhook.preview(local.project, input, { requestId: context.requestId });
1928
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1929
+ } });
1930
+ });
1931
+ addWebhookChangeOptions(blogWebhook.command("execute"), true).action(async (local) => {
1932
+ const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
1933
+ const global = program.opts();
1934
+ commandExitCode = await execute({ command: "blog destinations webhook execute", global, runtime, async action(context, client) {
1935
+ 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.` });
1936
+ const data = await client.blog.publishing.webhook.execute(local.project, { ...input, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
1937
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1938
+ } });
1939
+ });
1940
+ 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) => {
1941
+ const global = program.opts();
1942
+ commandExitCode = await execute({ command: "blog destinations webhook test", global, runtime, async action(context, client) {
1943
+ const data = await client.blog.publishing.webhook.test(local.project, { siteId: local.site, destinationId: local.destination, makePrimary: local.makePrimary }, { requestId: context.requestId });
1944
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1945
+ } });
1946
+ });
1947
+ const blogWebhookRotate = blogWebhook.command("rotate").description("Rotate the server-only signing secret");
1948
+ 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) => {
1949
+ const global = program.opts();
1950
+ commandExitCode = await execute({ command: "blog destinations webhook rotate preview", global, runtime, async action(context, client) {
1951
+ const data = await client.blog.publishing.webhook.previewRotation(local.project, { siteId: local.site, destinationId: local.destination }, { requestId: context.requestId });
1952
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1953
+ } });
1954
+ });
1955
+ 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) => {
1956
+ const global = program.opts();
1957
+ commandExitCode = await execute({ command: "blog destinations webhook rotate execute", global, runtime, async action(context, client) {
1958
+ 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.` });
1959
+ 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 });
1960
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1961
+ } });
1962
+ });
1973
1963
  const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
1974
1964
  blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
1975
1965
  const global = program.opts();
@@ -2002,9 +1992,7 @@ async function runCli(argv = process.argv, overrides = {}) {
2002
1992
  });
2003
1993
  const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
2004
1994
  blogPlan.command("create").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--start <date>", "local start date as YYYY-MM-DD").requiredOption("--weekdays <days>", "comma-separated weekday names such as mon,wed,fri").option("--time-zone <zone>", "IANA time zone", "UTC").requiredOption("--idempotency-key <key>", "stable retry key").action(async (local) => {
2005
- const weekdayMap = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
2006
- const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
2007
- if (!weekdays.length || weekdays.length !== local.weekdays.split(",").length) throw new TypeError("--weekdays must contain comma-separated sun,mon,tue,wed,thu,fri,sat values.");
1995
+ const weekdays = parseBlogWeekdays(local.weekdays);
2008
1996
  const global = program.opts();
2009
1997
  commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
2010
1998
  const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
@@ -2018,6 +2006,25 @@ async function runCli(argv = process.argv, overrides = {}) {
2018
2006
  writeSuccess(context, data, JSON.stringify(data, null, 2));
2019
2007
  } });
2020
2008
  });
2009
+ const blogPlanCadence = blogPlan.command("cadence").description("Preview or apply editorial cadence changes without publishing");
2010
+ 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) => {
2011
+ const global = program.opts();
2012
+ commandExitCode = await execute({ command: "blog plan cadence preview", global, runtime, async action(context, client) {
2013
+ const data = await client.blog.plans.previewCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion }, { requestId: context.requestId });
2014
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2015
+ } });
2016
+ });
2017
+ 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) => {
2018
+ const global = program.opts();
2019
+ commandExitCode = await execute({ command: "blog plan cadence apply", global, runtime, async action(context, client) {
2020
+ await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed editorial cadence change?
2021
+ Project: ${local.project}
2022
+ Plan: ${local.plan}
2023
+ This moves eligible editorial dates only. It does not schedule, publish, or unpublish an article.` });
2024
+ 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 });
2025
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2026
+ } });
2027
+ });
2021
2028
  blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
2022
2029
  const global = program.opts();
2023
2030
  commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
@@ -2098,7 +2105,7 @@ async function runCli(argv = process.argv, overrides = {}) {
2098
2105
  blogArticles.command("update").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--input <json-file>", "complete Blog draft JSON file").option("--yes", "confirm the draft save").action(async (articleId, local) => {
2099
2106
  const global = program.opts();
2100
2107
  commandExitCode = await execute({ command: "blog articles update", global, runtime, async action(context, client) {
2101
- const draft = import_contracts2.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2108
+ const draft = import_contracts.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
2102
2109
  await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Save a new immutable Blog draft revision?
2103
2110
  Project: ${local.project}
2104
2111
  Article: ${articleId}
@@ -2337,7 +2344,7 @@ Revision: ${local.revision}` });
2337
2344
  message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
2338
2345
  }
2339
2346
  ];
2340
- const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
2347
+ const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
2341
2348
  if (credential.source !== "none") {
2342
2349
  const actor = await client.whoami({ requestId: context.requestId });
2343
2350
  checks.push({