@rolino/cli 0.1.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 ADDED
@@ -0,0 +1,1870 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ EXIT_CODES: () => EXIT_CODES,
34
+ ROLINO_CLI_VERSION: () => ROLINO_CLI_VERSION,
35
+ runCli: () => runCli,
36
+ setupMcp: () => setupMcp,
37
+ updateClaudeProjectConfig: () => updateClaudeProjectConfig,
38
+ updateCodexConfig: () => updateCodexConfig
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/cli.ts
43
+ var import_node_crypto2 = require("crypto");
44
+ var import_promises2 = require("fs/promises");
45
+ var import_node_path2 = require("path");
46
+ var import_promises3 = require("readline/promises");
47
+ var import_commander = require("commander");
48
+
49
+ // package.json
50
+ var package_default = {
51
+ name: "@rolino/cli",
52
+ version: "0.1.0",
53
+ description: "Agent-friendly command-line interface for Rolino",
54
+ type: "module",
55
+ license: "MIT",
56
+ keywords: [
57
+ "rolino",
58
+ "cli",
59
+ "social-media",
60
+ "automation",
61
+ "ai-agent"
62
+ ],
63
+ repository: {
64
+ type: "git",
65
+ url: "git+https://github.com/deifos/rolino.git",
66
+ directory: "packages/cli"
67
+ },
68
+ homepage: "https://github.com/deifos/rolino/tree/main/packages/cli#readme",
69
+ bugs: {
70
+ url: "https://github.com/deifos/rolino/issues"
71
+ },
72
+ main: "./dist/index.cjs",
73
+ module: "./dist/index.js",
74
+ types: "./dist/index.d.ts",
75
+ bin: {
76
+ rolino: "./dist/bin.js"
77
+ },
78
+ exports: {
79
+ ".": {
80
+ import: {
81
+ types: "./dist/index.d.ts",
82
+ default: "./dist/index.js"
83
+ },
84
+ require: {
85
+ types: "./dist/index.d.cts",
86
+ default: "./dist/index.cjs"
87
+ }
88
+ }
89
+ },
90
+ files: [
91
+ "dist/**/*.js",
92
+ "dist/**/*.cjs",
93
+ "dist/**/*.map",
94
+ "dist/**/*.d.ts",
95
+ "dist/**/*.d.cts",
96
+ "README.md",
97
+ "CHANGELOG.md",
98
+ "LICENSE"
99
+ ],
100
+ publishConfig: {
101
+ access: "public"
102
+ },
103
+ scripts: {
104
+ build: "tsup src/index.ts src/bin.ts --format esm,cjs --dts --sourcemap --clean",
105
+ typecheck: "tsc --noEmit",
106
+ dev: "tsx src/bin.ts"
107
+ },
108
+ dependencies: {
109
+ "@rolino/contracts": "0.1.0",
110
+ "@rolino/local-auth": "0.1.0",
111
+ "@rolino/sdk": "0.1.0",
112
+ commander: "^15.0.0",
113
+ open: "^11.0.0"
114
+ },
115
+ devDependencies: {
116
+ tsx: "^4.21.0"
117
+ },
118
+ engines: {
119
+ node: ">=20.19.0"
120
+ }
121
+ };
122
+
123
+ // src/cli.ts
124
+ var import_contracts2 = require("@rolino/contracts");
125
+ var import_sdk = require("@rolino/sdk");
126
+
127
+ // src/browser-login.ts
128
+ var import_node_crypto = require("crypto");
129
+ var import_node_http = require("http");
130
+ var import_node_os = require("os");
131
+ var import_contracts = require("@rolino/contracts");
132
+ var import_open = __toESM(require("open"), 1);
133
+ var PAGE_HEADERS = {
134
+ "cache-control": "no-store",
135
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
136
+ "content-type": "text/html; charset=utf-8",
137
+ "referrer-policy": "no-referrer",
138
+ "x-content-type-options": "nosniff"
139
+ };
140
+ 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>';
141
+ function escapeHtml(value) {
142
+ return value.replace(/[&<>"']/g, (character) => ({
143
+ "&": "&amp;",
144
+ "<": "&lt;",
145
+ ">": "&gt;",
146
+ '"': "&quot;",
147
+ "'": "&#039;"
148
+ })[character]);
149
+ }
150
+ function renderCliAuthorizationResultPage(options) {
151
+ const tone = options.success ? "success" : "failure";
152
+ const eyebrow = options.success ? "CLI handoff complete" : "CLI handoff paused";
153
+ 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>';
154
+ return `<!doctype html>
155
+ <html lang="en">
156
+ <head>
157
+ <meta charset="utf-8">
158
+ <meta name="viewport" content="width=device-width,initial-scale=1">
159
+ <meta name="color-scheme" content="light">
160
+ <title>${escapeHtml(options.title)} \xB7 Rolino</title>
161
+ <style>
162
+ *{box-sizing:border-box}
163
+ html,body{height:100%;margin:0}
164
+ body{min-height:100svh;overflow:hidden;background:#faf9f1;color:#0f172a;font-family:"Poppins","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif}
165
+ .shell{position:relative;isolation:isolate;display:grid;min-height:100svh;grid-template-rows:auto 1fr auto;padding:clamp(18px,3vw,38px);overflow:hidden}
166
+ .shape{position:absolute;z-index:-1;border-radius:999px;pointer-events:none}
167
+ .shape-a{top:-160px;left:-130px;width:390px;height:390px;background:#facc1530;filter:blur(2px)}
168
+ .shape-b{right:-100px;bottom:-160px;width:360px;height:360px;background:#34c78a20}
169
+ .shape-c{right:9%;top:14%;width:22px;height:22px;background:#ef6a5b;transform:rotate(18deg);border-radius:7px}
170
+ .topbar,.footer{display:flex;width:min(100%,1100px);margin:auto;align-items:center;justify-content:space-between}
171
+ .brand{display:flex;align-items:center;gap:10px;color:#0f172a;font-size:22px;font-weight:800;letter-spacing:-.04em}
172
+ .brand svg{display:block;width:36px;height:36px}
173
+ .secure{display:flex;align-items:center;gap:8px;color:#52607a;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
174
+ .secure-dot{display:block;width:8px;height:8px;border-radius:50%;background:#34c78a;box-shadow:0 0 0 5px #34c78a1f}
175
+ .stage{display:grid;min-height:0;place-items:center;padding:12px 0}
176
+ .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}
177
+ .mascot-wrap{position:relative;display:grid;width:112px;height:96px;margin:0 auto 12px;place-items:center}
178
+ .mascot-wrap:before{position:absolute;width:88px;height:88px;border-radius:50%;background:#fff4a8;content:"";box-shadow:0 0 0 14px #fff9d8}
179
+ .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}
180
+ .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}
181
+ .eyebrow:before{width:7px;height:7px;border-radius:50%;background:#34c78a;content:""}
182
+ .failure .eyebrow{background:#fee6df;color:#9f2f24}
183
+ .failure .eyebrow:before{background:#ef6a5b}
184
+ 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}
185
+ .message{max-width:450px;margin:14px auto 0;color:#52607a;font-size:15px;line-height:1.65}
186
+ .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}
187
+ .terminal-bar{display:flex;align-items:center;gap:6px;padding-bottom:12px;color:#8f9bb1;font-size:9px;font-weight:800;letter-spacing:.13em}
188
+ .terminal-bar i{display:block;width:7px;height:7px;border-radius:50%;background:#ef6a5b}
189
+ .terminal-bar i:nth-child(2){background:#facc15}
190
+ .terminal-bar i:nth-child(3){margin-right:5px;background:#34c78a}
191
+ .command{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
192
+ .command b{color:#facc15}
193
+ .terminal-result{display:flex;align-items:center;gap:8px;margin-top:9px;color:#aeb9ca;font-size:12px}
194
+ .terminal-result span{color:#52d49e;font-weight:900}
195
+ .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}
196
+ .return-note>span{font-size:24px}
197
+ .return-note strong,.return-note small{display:block}
198
+ .return-note small{margin-top:3px;color:#875b56}
199
+ .footer{justify-content:center;color:#6f788a;font-size:11px}
200
+ @keyframes arrive{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
201
+ @keyframes mascot-arrive{from{opacity:0;transform:translateY(10px) rotate(-4deg) scale(.88)}to{opacity:1;transform:none}}
202
+ @media(max-width:600px){.secure{display:none}.panel{border-radius:24px}.shape-c{display:none}}
203
+ @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}}
204
+ @media(prefers-reduced-motion:reduce){.panel,.mascot-wrap svg{animation:none}}
205
+ </style>
206
+ </head>
207
+ <body class="${tone}">
208
+ <main class="shell">
209
+ <span class="shape shape-a"></span><span class="shape shape-b"></span><span class="shape shape-c"></span>
210
+ <header class="topbar">
211
+ <div class="brand">${ROLINO_MASCOT_SVG}<span>Rolino</span></div>
212
+ <div class="secure"><span class="secure-dot"></span>Secure CLI authorization</div>
213
+ </header>
214
+ <section class="stage">
215
+ <div class="panel">
216
+ <div class="mascot-wrap">${ROLINO_MASCOT_SVG}</div>
217
+ <p class="eyebrow">${eyebrow}</p>
218
+ <h1>${escapeHtml(options.title)}</h1>
219
+ <p class="message">${escapeHtml(options.message)}</p>
220
+ ${detail}
221
+ </div>
222
+ </section>
223
+ <footer class="footer">Secure browser handoff \xB7 no password shared</footer>
224
+ </main>
225
+ </body>
226
+ </html>`;
227
+ }
228
+ function writePage(response, status, html) {
229
+ response.writeHead(status, PAGE_HEADERS);
230
+ response.end(html);
231
+ }
232
+ function secretsMatch(left, right) {
233
+ const leftBytes = Buffer.from(left);
234
+ const rightBytes = Buffer.from(right);
235
+ return leftBytes.length === rightBytes.length && (0, import_node_crypto.timingSafeEqual)(leftBytes, rightBytes);
236
+ }
237
+ function createBrowserAuthorizationRequest(options) {
238
+ const state = (0, import_node_crypto.randomBytes)(32).toString("base64url");
239
+ const codeVerifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
240
+ const codeChallenge = (0, import_node_crypto.createHash)("sha256").update(codeVerifier).digest("base64url");
241
+ const url = new URL("/cli/authorize", options.baseUrl);
242
+ const requestExpiresAt = new Date(
243
+ Date.now() + import_contracts.CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS
244
+ ).toISOString();
245
+ url.searchParams.set("redirect_uri", options.redirectUri);
246
+ url.searchParams.set("state", state);
247
+ url.searchParams.set("code_challenge", codeChallenge);
248
+ url.searchParams.set("code_challenge_method", "S256");
249
+ url.searchParams.set(
250
+ "client_name",
251
+ (options.clientName ?? `Rolino CLI on ${(0, import_node_os.hostname)()}`).slice(0, 32)
252
+ );
253
+ url.searchParams.set("request_expires_at", requestExpiresAt);
254
+ return {
255
+ url: url.toString(),
256
+ state,
257
+ codeVerifier,
258
+ codeChallenge,
259
+ requestExpiresAt
260
+ };
261
+ }
262
+ var CliAuthorizationDeniedError = class extends Error {
263
+ constructor() {
264
+ super("Authorization was denied.");
265
+ this.name = "CliAuthorizationDeniedError";
266
+ }
267
+ };
268
+ async function loginWithBrowser(options) {
269
+ return new Promise((resolve3, reject) => {
270
+ let settled = false;
271
+ let authorization = null;
272
+ const finish = (callback) => {
273
+ if (settled) return;
274
+ settled = true;
275
+ clearTimeout(timeout);
276
+ server.close();
277
+ callback();
278
+ };
279
+ const server = (0, import_node_http.createServer)((request, response) => {
280
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
281
+ if (request.method !== "GET" || url.pathname !== "/callback") {
282
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
283
+ response.end("Not found");
284
+ return;
285
+ }
286
+ void (async () => {
287
+ if (!authorization) {
288
+ writePage(response, 503, renderCliAuthorizationResultPage({
289
+ title: "Authorization is starting",
290
+ message: "Return to your terminal and try the browser link again in a moment.",
291
+ success: false
292
+ }));
293
+ return;
294
+ }
295
+ if (!secretsMatch(url.searchParams.get("state") ?? "", authorization.state)) {
296
+ writePage(response, 400, renderCliAuthorizationResultPage({
297
+ title: "Authorization failed",
298
+ message: "The callback state did not match. Return to your terminal and try again.",
299
+ success: false
300
+ }));
301
+ finish(() => reject(new Error("The browser callback state did not match.")));
302
+ return;
303
+ }
304
+ if (url.searchParams.get("error") === "access_denied") {
305
+ writePage(response, 200, renderCliAuthorizationResultPage({
306
+ title: "Access denied",
307
+ message: "No credential was created. You can close this window and return to your terminal.",
308
+ success: false
309
+ }));
310
+ finish(() => reject(new CliAuthorizationDeniedError()));
311
+ return;
312
+ }
313
+ const code = url.searchParams.get("code");
314
+ if (!code || !/^[A-Za-z0-9_-]{43,128}$/.test(code)) {
315
+ writePage(response, 400, renderCliAuthorizationResultPage({
316
+ title: "Authorization failed",
317
+ message: "Rolino did not return a valid one-time code. Return to your terminal and try again.",
318
+ success: false
319
+ }));
320
+ finish(() => reject(new Error("The browser callback did not include a valid code.")));
321
+ return;
322
+ }
323
+ try {
324
+ const credential = await options.exchange({
325
+ code,
326
+ codeVerifier: authorization.codeVerifier
327
+ });
328
+ writePage(response, 200, renderCliAuthorizationResultPage({
329
+ title: "Terminal connected",
330
+ message: "Rolino is ready in your terminal. You can close this window.",
331
+ success: true
332
+ }));
333
+ finish(() => resolve3(credential));
334
+ } catch (error) {
335
+ writePage(response, 502, renderCliAuthorizationResultPage({
336
+ title: "Authorization failed",
337
+ message: "The CLI could not exchange its one-time code. Return to your terminal and try again.",
338
+ success: false
339
+ }));
340
+ finish(() => reject(error));
341
+ }
342
+ })();
343
+ });
344
+ server.once("error", (error) => finish(() => reject(error)));
345
+ server.listen(0, "127.0.0.1", () => {
346
+ const address = server.address();
347
+ if (!address || typeof address === "string") {
348
+ finish(() => reject(new Error("Rolino could not start the local authorization callback.")));
349
+ return;
350
+ }
351
+ authorization = createBrowserAuthorizationRequest({
352
+ baseUrl: options.baseUrl,
353
+ redirectUri: `http://127.0.0.1:${address.port}/callback`
354
+ });
355
+ options.stderr.write(`Opening Rolino in your browser\u2026
356
+ `);
357
+ options.stderr.write(`If it does not open, visit:
358
+ ${authorization.url}
359
+ `);
360
+ options.stderr.write("Waiting for authorization\u2026\n");
361
+ void (0, import_open.default)(authorization.url).catch(() => void 0);
362
+ });
363
+ const timeout = setTimeout(() => {
364
+ finish(() => reject(new Error("Browser authorization timed out after 5 minutes.")));
365
+ }, import_contracts.CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS);
366
+ });
367
+ }
368
+
369
+ // src/cli.ts
370
+ var import_local_auth = require("@rolino/local-auth");
371
+
372
+ // src/exit-codes.ts
373
+ var EXIT_CODES = {
374
+ success: 0,
375
+ unexpected: 1,
376
+ usage: 2,
377
+ authentication: 3,
378
+ permission: 4,
379
+ notFound: 5,
380
+ conflict: 6,
381
+ validation: 7,
382
+ rateLimited: 8,
383
+ server: 9,
384
+ network: 10,
385
+ timeout: 11,
386
+ cancelled: 12
387
+ };
388
+
389
+ // src/output.ts
390
+ function jsonLine(value) {
391
+ return `${JSON.stringify(value)}
392
+ `;
393
+ }
394
+ function writeSuccess(context, data, human, suggestions = []) {
395
+ if (context.format === "human") {
396
+ context.stdout.write(human.endsWith("\n") ? human : `${human}
397
+ `);
398
+ return;
399
+ }
400
+ context.stdout.write(jsonLine({
401
+ ok: true,
402
+ data,
403
+ meta: {
404
+ command: context.command,
405
+ requestId: context.requestId,
406
+ ...context.agent && suggestions.length > 0 ? { suggestions } : {}
407
+ }
408
+ }));
409
+ }
410
+ function writeFailure(context, error) {
411
+ const requestId2 = error.requestId ?? context.requestId;
412
+ if (context.format === "human") {
413
+ context.stderr.write(`Error [${error.code}]: ${error.message}
414
+ `);
415
+ for (const suggestion of error.suggestions ?? []) {
416
+ context.stderr.write(`Next: ${suggestion}
417
+ `);
418
+ }
419
+ context.stderr.write(`Request ID: ${requestId2}
420
+ `);
421
+ return;
422
+ }
423
+ context.stdout.write(jsonLine({
424
+ ok: false,
425
+ error: {
426
+ code: error.code,
427
+ message: error.message,
428
+ ...error.retryAfterSeconds === null || error.retryAfterSeconds === void 0 ? {} : { retryAfterSeconds: error.retryAfterSeconds }
429
+ },
430
+ meta: {
431
+ command: context.command,
432
+ requestId: requestId2,
433
+ ...context.agent && error.suggestions?.length ? { suggestions: error.suggestions } : {}
434
+ }
435
+ }));
436
+ }
437
+ function formatActor(actor) {
438
+ return [
439
+ `${actor.user.name} <${actor.user.email}>`,
440
+ `Organization: ${actor.organization.name} (${actor.organization.role})`,
441
+ `Authentication: ${actor.authentication.kind}`,
442
+ `Capabilities: ${actor.capabilities.join(", ") || "none"}`
443
+ ].join("\n");
444
+ }
445
+ function formatProjects(data) {
446
+ if (data.items.length === 0) return "No projects found.";
447
+ const rows = data.items.map((project) => `${project.id} ${project.name} ${project.status}`);
448
+ return ["ID NAME STATUS", ...rows].join("\n");
449
+ }
450
+ function formatProject(project) {
451
+ return [
452
+ `${project.name} (${project.id})`,
453
+ `Status: ${project.status}`,
454
+ `Type: ${project.type}`,
455
+ `Slug: ${project.slug}`,
456
+ ...project.websiteUrl ? [`Website: ${project.websiteUrl}`] : [],
457
+ ...project.description ? [`Description: ${project.description}`] : []
458
+ ].join("\n");
459
+ }
460
+ function formatMediaAssets(data) {
461
+ if (data.items.length === 0) return "No media assets found.";
462
+ return [
463
+ "ID TYPE SIZE FILE",
464
+ ...data.items.map((asset) => `${asset.id} ${asset.type} ${asset.sizeBytes ?? "-"} ${asset.originalFileName ?? "-"}`)
465
+ ].join("\n");
466
+ }
467
+ function formatMediaAsset(asset) {
468
+ return [
469
+ `${asset.originalFileName ?? "Media asset"} (${asset.id})`,
470
+ `Type: ${asset.type}`,
471
+ `Content type: ${asset.mimeType}`,
472
+ `Size: ${asset.sizeBytes ?? "unknown"} bytes`,
473
+ `URL: ${asset.url}`
474
+ ].join("\n");
475
+ }
476
+ function formatPosts(data) {
477
+ if (data.items.length === 0) return "No posts found.";
478
+ const rows = data.items.map((post) => `${post.id} ${post.status} ${post.scheduledAt ?? "-"} ${post.caption.replace(/\s+/g, " ").slice(0, 60) || "-"}`);
479
+ return ["ID STATUS SCHEDULED CAPTION", ...rows].join("\n");
480
+ }
481
+ function formatPost(post) {
482
+ const queued = post.status === "PUBLISHING";
483
+ return [
484
+ `Post ${post.id}`,
485
+ `Project: ${post.projectId}`,
486
+ `Version: ${post.version}`,
487
+ `Status: ${post.status}`,
488
+ `Caption: ${post.caption || "-"}`,
489
+ ...post.campaign ? [`Campaign: ${post.campaign.title} (${post.campaign.id})`] : [],
490
+ ...post.scheduledAt ? [
491
+ `${queued ? "Queued for publishing" : "Scheduled"}: ${post.scheduledAt}${post.timezone ? ` (${post.timezone})` : ""}`
492
+ ] : [],
493
+ ...post.publishedAt ? [`Published: ${post.publishedAt}`] : [],
494
+ `Media: ${post.media.length}`,
495
+ `Destinations: ${post.destinations.map((destination) => `${destination.provider}:${queued && destination.status === "SCHEDULED" ? "QUEUED" : destination.status}`).join(", ") || "none"}`
496
+ ].join("\n");
497
+ }
498
+ function formatPostSchedulePending(pending) {
499
+ return [
500
+ `YouTube schedule change pending for post ${pending.postId}.`,
501
+ pending.message,
502
+ `Mutation: ${pending.mutationId}`
503
+ ].join("\n");
504
+ }
505
+ function formatIntegrationHealth(data) {
506
+ const rows = data.items.map((item) => `${item.provider} ${item.connected ? "connected" : "disconnected"} ${item.health.status} ${item.health.isStale ? "stale" : "current"} ${item.health.message}`);
507
+ return ["PROVIDER CONNECTION HEALTH FRESHNESS DETAIL", ...rows].join("\n");
508
+ }
509
+ function formatPostReadiness(readiness) {
510
+ const attention = readiness.checks.filter((check) => check.status !== "pass");
511
+ const summary = readiness.hasBlockingChecks ? "Post is not ready to publish." : readiness.requiresLiveRefresh.length ? "Local checks passed; live provider health must be refreshed." : "Post is ready to publish.";
512
+ if (!attention.length) return summary;
513
+ return [
514
+ summary,
515
+ ...attention.map((check) => `${check.status.toUpperCase()} ${check.provider ?? "POST"} ${check.code} ${check.message}`)
516
+ ].join("\n");
517
+ }
518
+ function formatPostSchedulePreview(preview) {
519
+ const attention = preview.readiness.checks.filter((check) => check.status !== "pass");
520
+ return [
521
+ `Schedule preview for post ${preview.post.id}`,
522
+ `Current version: ${preview.post.version}`,
523
+ `Publish at: ${preview.schedule.scheduledAt} (${preview.schedule.timezone})`,
524
+ `Destinations: ${preview.destinations.join(", ")}`,
525
+ ...preview.providerReconciliation ? [`Provider reconciliation: ${preview.providerReconciliation.message}`] : [],
526
+ `Confirmation expires: ${preview.confirmation.expiresAt}`,
527
+ `Confirmation token: ${preview.confirmation.token}`,
528
+ ...attention.length ? ["Readiness notes:", ...attention.map((check) => `${check.status.toUpperCase()} ${check.provider ?? "POST"} ${check.code} ${check.message}`)] : ["All required readiness checks passed."]
529
+ ].join("\n");
530
+ }
531
+ function formatPostPublishPreview(preview) {
532
+ const attention = preview.readiness.checks.filter((check) => check.status !== "pass");
533
+ return [
534
+ `Immediate publish preview for post ${preview.post.id}`,
535
+ `Current version: ${preview.post.version}`,
536
+ `Destinations: ${preview.destinations.join(", ")}`,
537
+ "Effect: confirmed execution queues these destinations for immediate publishing.",
538
+ ...preview.destinations.includes("YOUTUBE") ? ["YouTube effect: execution starts a resumable upload; PREPARING remains visible until upload, processing, and configured visibility are provider-confirmed."] : [],
539
+ `Confirmation expires: ${preview.confirmation.expiresAt}`,
540
+ `Confirmation token: ${preview.confirmation.token}`,
541
+ ...attention.length ? ["Readiness notes:", ...attention.map((check) => `${check.status.toUpperCase()} ${check.provider ?? "POST"} ${check.code} ${check.message}`)] : ["All required readiness checks passed."]
542
+ ].join("\n");
543
+ }
544
+ function formatCalendarEvents(data) {
545
+ if (!data.items.length) {
546
+ return `No scheduled posts found from ${data.window.from} to ${data.window.to}.`;
547
+ }
548
+ const rows = data.items.map((event) => `${event.postId} ${event.scheduledAt} ${event.status} ${event.destinations.map((item) => item.provider).join(",") || "-"} ${event.title}`);
549
+ return ["POST ID SCHEDULED STATUS DESTINATIONS TITLE", ...rows].join("\n");
550
+ }
551
+
552
+ // src/mcp-setup.ts
553
+ var import_node_child_process = require("child_process");
554
+ var import_node_fs = require("fs");
555
+ var import_promises = require("fs/promises");
556
+ var import_node_module = require("module");
557
+ var import_node_os2 = require("os");
558
+ var import_node_path = require("path");
559
+ var CODEX_BEGIN = "# BEGIN ROLINO MCP (managed by rolino setup mcp)";
560
+ var CODEX_END = "# END ROLINO MCP";
561
+ function homeDirectory(env) {
562
+ return env.USERPROFILE || env.HOME || (0, import_node_os2.homedir)();
563
+ }
564
+ function tomlString(value) {
565
+ return JSON.stringify(value);
566
+ }
567
+ async function pathExists(path) {
568
+ try {
569
+ await (0, import_promises.access)(path, import_node_fs.constants.F_OK);
570
+ return true;
571
+ } catch {
572
+ return false;
573
+ }
574
+ }
575
+ async function resolveServerPath(options) {
576
+ if (options.serverPath) {
577
+ const path = (0, import_node_path.isAbsolute)(options.serverPath) ? options.serverPath : (0, import_node_path.resolve)(options.cwd, options.serverPath);
578
+ if (await pathExists(path)) return path;
579
+ throw new TypeError(
580
+ `Rolino MCP server was not found at ${path}. Check --server-path and try again.`
581
+ );
582
+ }
583
+ try {
584
+ const requireFromCli = (0, import_node_module.createRequire)((0, import_node_path.resolve)(options.cliEntryPath));
585
+ const installedPath = (0, import_node_path.join)(
586
+ (0, import_node_path.dirname)(requireFromCli.resolve("@rolino/mcp")),
587
+ "bin.js"
588
+ );
589
+ if (await pathExists(installedPath)) return installedPath;
590
+ throw new TypeError(
591
+ "The installed @rolino/mcp package is incomplete. Reinstall it with npm install --global @rolino/mcp, or pass --server-path."
592
+ );
593
+ } catch (error) {
594
+ if (error instanceof TypeError) throw error;
595
+ }
596
+ const workspacePath = (0, import_node_path.resolve)(
597
+ (0, import_node_path.dirname)((0, import_node_path.resolve)(options.cliEntryPath)),
598
+ "../../mcp/dist/bin.js"
599
+ );
600
+ if (await pathExists(workspacePath)) return workspacePath;
601
+ throw new TypeError(
602
+ "Rolino MCP is not installed. Run npm install --global @rolino/mcp, then rerun this command. From a Rolino source checkout, run npm run packages:build instead. You can also pass --server-path."
603
+ );
604
+ }
605
+ async function readOptional(path) {
606
+ try {
607
+ return { exists: true, value: await (0, import_promises.readFile)(path, "utf8") };
608
+ } catch (error) {
609
+ if (error.code === "ENOENT") {
610
+ return { exists: false, value: "" };
611
+ }
612
+ throw error;
613
+ }
614
+ }
615
+ async function replaceFile(path, value) {
616
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true, mode: 448 });
617
+ const temporaryPath = `${path}.rolino-${process.pid}.tmp`;
618
+ await (0, import_promises.writeFile)(temporaryPath, value, { encoding: "utf8", mode: 384 });
619
+ try {
620
+ await (0, import_promises.rename)(temporaryPath, path);
621
+ } catch {
622
+ await (0, import_promises.writeFile)(path, value, { encoding: "utf8", mode: 384 });
623
+ await (0, import_promises.unlink)(temporaryPath).catch(() => void 0);
624
+ }
625
+ await (0, import_promises.chmod)(path, 384).catch(() => void 0);
626
+ }
627
+ async function writeConfiguredFile(path, previous, value) {
628
+ const backupPath = previous.exists ? `${path}.rolino-backup` : null;
629
+ if (backupPath) await replaceFile(backupPath, previous.value);
630
+ await replaceFile(path, value);
631
+ return backupPath;
632
+ }
633
+ function removeExistingCodexEntry(source, newline) {
634
+ if (/^\s*\[\[mcp_servers\.rolino(?:\.|\]\])/m.test(source)) {
635
+ throw new TypeError(
636
+ "Codex config uses an unsupported array-table entry for mcp_servers.rolino."
637
+ );
638
+ }
639
+ const lines = source.split(/\r?\n/);
640
+ const kept = [];
641
+ let insideRolino = false;
642
+ for (const line of lines) {
643
+ const header = line.match(/^\s*\[([^\]]+)]\s*(?:#.*)?$/)?.[1]?.trim();
644
+ if (header) {
645
+ insideRolino = header === "mcp_servers.rolino" || header.startsWith("mcp_servers.rolino.");
646
+ }
647
+ if (!insideRolino) kept.push(line);
648
+ }
649
+ return kept.join(newline).trimEnd();
650
+ }
651
+ function codexBlock(server, newline) {
652
+ return [
653
+ CODEX_BEGIN,
654
+ "[mcp_servers.rolino]",
655
+ `command = ${tomlString(server.command)}`,
656
+ `args = [${server.args.map(tomlString).join(", ")}]`,
657
+ `env = { ROLINO_URL = ${tomlString(server.env.ROLINO_URL)} }`,
658
+ CODEX_END
659
+ ].join(newline);
660
+ }
661
+ function updateCodexConfig(source, server) {
662
+ const newline = source.includes("\r\n") ? "\r\n" : "\n";
663
+ const block = codexBlock(server, newline);
664
+ const begin = source.indexOf(CODEX_BEGIN);
665
+ const end = source.indexOf(CODEX_END);
666
+ if (begin === -1 !== (end === -1) || begin !== -1 && end < begin) {
667
+ throw new TypeError("Codex config contains an incomplete Rolino-managed block.");
668
+ }
669
+ let unmanagedSource = source;
670
+ if (begin !== -1) {
671
+ const after = end + CODEX_END.length;
672
+ unmanagedSource = `${source.slice(0, begin)}${source.slice(after)}`;
673
+ }
674
+ const withoutRolino = removeExistingCodexEntry(unmanagedSource, newline);
675
+ return `${withoutRolino ? `${withoutRolino}${newline}${newline}` : ""}${block}${newline}`;
676
+ }
677
+ function updateClaudeProjectConfig(source, server) {
678
+ let parsed = {};
679
+ if (source.trim()) {
680
+ try {
681
+ parsed = JSON.parse(source);
682
+ } catch {
683
+ throw new TypeError("Claude project .mcp.json is not valid JSON.");
684
+ }
685
+ }
686
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
687
+ throw new TypeError("Claude project .mcp.json must contain a JSON object.");
688
+ }
689
+ const currentServers = parsed.mcpServers;
690
+ if (currentServers !== void 0 && (!currentServers || Array.isArray(currentServers) || typeof currentServers !== "object")) {
691
+ throw new TypeError("Claude project .mcp.json mcpServers must be an object.");
692
+ }
693
+ return `${JSON.stringify({
694
+ ...parsed,
695
+ mcpServers: {
696
+ ...currentServers,
697
+ rolino: { type: "stdio", ...server }
698
+ }
699
+ }, null, 2)}
700
+ `;
701
+ }
702
+ function claudeCommand(args, options) {
703
+ const command = options.env.ROLINO_CLAUDE_COMMAND || "claude";
704
+ const executable = process.platform === "win32" ? options.env.ComSpec || options.env.COMSPEC || "cmd.exe" : command;
705
+ const commandArgs = process.platform === "win32" ? ["/d", "/s", "/c", command, ...args] : args;
706
+ return (0, import_node_child_process.spawnSync)(executable, commandArgs, {
707
+ cwd: options.cwd,
708
+ env: options.env,
709
+ encoding: "utf8",
710
+ windowsHide: true
711
+ });
712
+ }
713
+ async function setupCodex(options, server) {
714
+ const target = options.scope === "project" ? (0, import_node_path.join)(options.cwd, ".codex", "config.toml") : (0, import_node_path.join)(options.env.CODEX_HOME || (0, import_node_path.join)(homeDirectory(options.env), ".codex"), "config.toml");
715
+ const current = await readOptional(target);
716
+ const updated = updateCodexConfig(current.value, server);
717
+ const changed = updated !== current.value;
718
+ const backupPath = changed && current.exists ? `${target}.rolino-backup` : null;
719
+ if (changed && !options.dryRun) {
720
+ await writeConfiguredFile(target, current, updated);
721
+ }
722
+ return {
723
+ client: "codex",
724
+ scope: options.scope,
725
+ target,
726
+ backupPath,
727
+ changed,
728
+ dryRun: options.dryRun ?? false,
729
+ server
730
+ };
731
+ }
732
+ async function setupClaudeCode(options, server) {
733
+ if (options.scope === "project") {
734
+ const target2 = (0, import_node_path.join)(options.cwd, ".mcp.json");
735
+ const current = await readOptional(target2);
736
+ const updated = updateClaudeProjectConfig(current.value, server);
737
+ const changed = updated !== current.value;
738
+ const backupPath = changed && current.exists ? `${target2}.rolino-backup` : null;
739
+ if (changed && !options.dryRun) {
740
+ await writeConfiguredFile(target2, current, updated);
741
+ }
742
+ return {
743
+ client: "claude-code",
744
+ scope: "project",
745
+ target: target2,
746
+ backupPath,
747
+ changed,
748
+ dryRun: options.dryRun ?? false,
749
+ server
750
+ };
751
+ }
752
+ const target = "Claude Code user MCP configuration";
753
+ if (options.dryRun) {
754
+ return {
755
+ client: "claude-code",
756
+ scope: "user",
757
+ target,
758
+ backupPath: null,
759
+ changed: true,
760
+ dryRun: true,
761
+ server
762
+ };
763
+ }
764
+ if (options.force) {
765
+ claudeCommand(
766
+ ["mcp", "remove", "rolino", "--scope", "user"],
767
+ options
768
+ );
769
+ }
770
+ const result = claudeCommand([
771
+ "mcp",
772
+ "add-json",
773
+ "rolino",
774
+ JSON.stringify({ type: "stdio", ...server }),
775
+ "--scope",
776
+ "user"
777
+ ], options);
778
+ if (result.error || result.status !== 0) {
779
+ throw new TypeError(
780
+ "Claude Code could not add the Rolino MCP server. Install the `claude` command, or use --scope project. If Rolino already exists, rerun with --force."
781
+ );
782
+ }
783
+ return {
784
+ client: "claude-code",
785
+ scope: "user",
786
+ target,
787
+ backupPath: null,
788
+ changed: true,
789
+ dryRun: false,
790
+ server
791
+ };
792
+ }
793
+ async function setupMcp(options) {
794
+ const serverPath = await resolveServerPath(options);
795
+ const server = {
796
+ command: options.nodePath,
797
+ args: [serverPath],
798
+ env: { ROLINO_URL: options.baseUrl }
799
+ };
800
+ return options.client === "codex" ? setupCodex(options, server) : setupClaudeCode(options, server);
801
+ }
802
+
803
+ // src/cli.ts
804
+ var ROLINO_CLI_VERSION = package_default.version;
805
+ var REQUEST_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
806
+ function duration(value) {
807
+ try {
808
+ return (0, import_sdk.parseRolinoTimeout)(value);
809
+ } catch (error) {
810
+ throw new import_commander.InvalidArgumentError(
811
+ error instanceof Error ? error.message : "Timeout is invalid."
812
+ );
813
+ }
814
+ }
815
+ function requestId(value) {
816
+ if (!REQUEST_ID_PATTERN.test(value)) {
817
+ throw new import_commander.InvalidArgumentError(
818
+ "Request ID must be 1-128 letters, numbers, dots, underscores, or dashes."
819
+ );
820
+ }
821
+ return value;
822
+ }
823
+ function outputFormat(value) {
824
+ if (value === "human" || value === "json" || value === "jsonl") return value;
825
+ throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
826
+ }
827
+ function postStatus(value) {
828
+ const parsed = import_contracts2.PostStatusSchema.safeParse(value.toUpperCase());
829
+ if (parsed.success) return parsed.data;
830
+ throw new import_commander.InvalidArgumentError(
831
+ `Status must be one of ${import_contracts2.PostStatusSchema.options.join(", ")}.`
832
+ );
833
+ }
834
+ function projectType(value) {
835
+ const parsed = import_contracts2.ProjectTypeSchema.safeParse(value.toUpperCase());
836
+ if (parsed.success) return parsed.data;
837
+ throw new import_commander.InvalidArgumentError(
838
+ `Type must be one of ${import_contracts2.ProjectTypeSchema.options.join(", ")}.`
839
+ );
840
+ }
841
+ function publishingProvider(value) {
842
+ const normalized = value.toUpperCase();
843
+ const parsed = import_contracts2.PublishingProviderSchema.safeParse(normalized);
844
+ if (parsed.success) return parsed.data;
845
+ throw new import_commander.InvalidArgumentError(
846
+ `Platform must be one of ${import_contracts2.PublishingProviderSchema.options.join(", ")}.`
847
+ );
848
+ }
849
+ function collectPublishingProvider(value, previous) {
850
+ return [...previous ?? [], publishingProvider(value)];
851
+ }
852
+ function collectString(value, previous) {
853
+ return [...previous, value];
854
+ }
855
+ function youtubePrivacy(value) {
856
+ const normalized = value.toUpperCase();
857
+ if (normalized === "PUBLIC" || normalized === "UNLISTED" || normalized === "PRIVATE") {
858
+ return normalized;
859
+ }
860
+ throw new import_commander.InvalidArgumentError(
861
+ "YouTube privacy must be PUBLIC, UNLISTED, or PRIVATE."
862
+ );
863
+ }
864
+ function yesOrNo(value) {
865
+ const normalized = value.trim().toLowerCase();
866
+ if (normalized === "yes") return true;
867
+ if (normalized === "no") return false;
868
+ throw new import_commander.InvalidArgumentError("Expected yes or no.");
869
+ }
870
+ function youtubeSettings(options) {
871
+ 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;
872
+ if (!hasYouTubeOptions) return null;
873
+ return import_contracts2.YouTubePostSettingsSchema.parse({
874
+ title: options.youtubeTitle,
875
+ categoryId: options.youtubeCategoryId,
876
+ privacyStatus: options.youtubePrivacy,
877
+ madeForKids: options.youtubeMadeForKids,
878
+ containsSyntheticMedia: options.youtubeSyntheticMedia ?? false,
879
+ notifySubscribers: options.youtubeNotifySubscribers ?? true,
880
+ tags: options.youtubeTag
881
+ });
882
+ }
883
+ function positiveInteger(value) {
884
+ const parsed = Number(value);
885
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
886
+ throw new import_commander.InvalidArgumentError("Expected version must be a positive integer.");
887
+ }
888
+ return parsed;
889
+ }
890
+ function mcpClient(value) {
891
+ if (value === "codex" || value === "claude-code") return value;
892
+ throw new import_commander.InvalidArgumentError("MCP client must be codex or claude-code.");
893
+ }
894
+ function mcpScope(value) {
895
+ if (value === "user" || value === "project") return value;
896
+ throw new import_commander.InvalidArgumentError("MCP setup scope must be user or project.");
897
+ }
898
+ function isoDateTime(value) {
899
+ const date = new Date(value);
900
+ if (Number.isNaN(date.getTime())) {
901
+ throw new import_commander.InvalidArgumentError("Date and time must be a valid ISO-8601 value.");
902
+ }
903
+ return date.toISOString();
904
+ }
905
+ var CliUserCancelledError = class extends Error {
906
+ constructor(message) {
907
+ super(message);
908
+ this.name = "CliUserCancelledError";
909
+ }
910
+ };
911
+ async function confirmInTerminal(message) {
912
+ const prompt = (0, import_promises3.createInterface)({ input: process.stdin, output: process.stderr });
913
+ try {
914
+ const answer = await prompt.question(`${message}
915
+ Continue? [y/N] `);
916
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
917
+ } finally {
918
+ prompt.close();
919
+ }
920
+ }
921
+ async function requireWriteConsent(options) {
922
+ if (options.yes) return;
923
+ const projectCreation = options.subject === "project";
924
+ const mediaUpload = options.subject === "media";
925
+ if (options.global.agent || !options.runtime.isTTY) {
926
+ throw new TypeError(
927
+ projectCreation ? "Project creation requires explicit consent. Review the command and pass --yes." : mediaUpload ? "Media upload requires explicit consent. Review the command and pass --yes." : "Draft writes require explicit consent. Review the command and pass --yes."
928
+ );
929
+ }
930
+ if (!await options.runtime.confirm(options.preview)) {
931
+ throw new CliUserCancelledError(
932
+ projectCreation ? "Project creation was cancelled." : mediaUpload ? "The media upload was cancelled." : "The draft write was cancelled."
933
+ );
934
+ }
935
+ }
936
+ function formatMcpSetupPreview(result) {
937
+ return [
938
+ `Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
939
+ `Scope: ${result.scope}`,
940
+ `Target: ${result.target}`,
941
+ `Command: ${result.server.command}`,
942
+ `Arguments: ${result.server.args.join(" ")}`,
943
+ `Rolino URL: ${result.server.env.ROLINO_URL}`,
944
+ "No token will be written to MCP configuration."
945
+ ].join("\n");
946
+ }
947
+ function resolveFormat(options, runtime) {
948
+ if (options.agent || options.json) return "json";
949
+ if (options.output) return options.output;
950
+ const fromEnvironment = runtime.env.ROLINO_OUTPUT;
951
+ if (fromEnvironment) return outputFormat(fromEnvironment);
952
+ return runtime.isTTY ? "human" : "json";
953
+ }
954
+ function baseUrlFor(options, runtime) {
955
+ return (0, import_sdk.normalizeRolinoBaseUrl)(
956
+ options.baseUrl ?? runtime.env.ROLINO_URL ?? "https://getrolino.com"
957
+ );
958
+ }
959
+ function clientFor(options, runtime) {
960
+ const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
961
+ const baseUrl = baseUrlFor(options, runtime);
962
+ const credential = (0, import_local_auth.resolveCredential)(baseUrl, runtime.env);
963
+ return new import_sdk.RolinoClient({
964
+ baseUrl,
965
+ token: credential.token ?? void 0,
966
+ timeoutMs,
967
+ fetch: runtime.fetch
968
+ });
969
+ }
970
+ function exitCodeFor(error) {
971
+ if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
972
+ return EXIT_CODES.cancelled;
973
+ }
974
+ if (error instanceof import_sdk.RolinoNetworkError) {
975
+ return error.kind === "TIMEOUT" ? EXIT_CODES.timeout : EXIT_CODES.network;
976
+ }
977
+ if (error instanceof import_sdk.RolinoApiError) {
978
+ switch (error.code) {
979
+ case "AUTH_REQUIRED":
980
+ return EXIT_CODES.authentication;
981
+ case "FORBIDDEN":
982
+ return EXIT_CODES.permission;
983
+ case "BILLING_REQUIRED":
984
+ return EXIT_CODES.permission;
985
+ case "NOT_FOUND":
986
+ case "FEATURE_DISABLED":
987
+ return EXIT_CODES.notFound;
988
+ case "LIMIT_REACHED":
989
+ return EXIT_CODES.conflict;
990
+ case "CONFLICT":
991
+ return EXIT_CODES.conflict;
992
+ case "VALIDATION_ERROR":
993
+ return EXIT_CODES.validation;
994
+ case "RATE_LIMITED":
995
+ return EXIT_CODES.rateLimited;
996
+ case "INTERNAL_ERROR":
997
+ return EXIT_CODES.server;
998
+ }
999
+ }
1000
+ if (error instanceof TypeError) return EXIT_CODES.usage;
1001
+ return EXIT_CODES.unexpected;
1002
+ }
1003
+ function errorForOutput(error) {
1004
+ if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1005
+ return { code: "CANCELLED", message: error.message };
1006
+ }
1007
+ if (error instanceof import_sdk.RolinoApiError) {
1008
+ return {
1009
+ code: error.code,
1010
+ message: error.message,
1011
+ requestId: error.requestId,
1012
+ retryAfterSeconds: error.retryAfterSeconds
1013
+ };
1014
+ }
1015
+ if (error instanceof import_sdk.RolinoNetworkError) {
1016
+ return { code: error.kind, message: error.message };
1017
+ }
1018
+ if (error instanceof TypeError) {
1019
+ return { code: "USAGE_ERROR", message: error.message };
1020
+ }
1021
+ if (error instanceof Error) {
1022
+ return { code: "CLI_ERROR", message: error.message };
1023
+ }
1024
+ return { code: "CLI_ERROR", message: "The command failed unexpectedly." };
1025
+ }
1026
+ function recoverySuggestions(code, command) {
1027
+ switch (code) {
1028
+ case "AUTH_REQUIRED":
1029
+ return [
1030
+ "rolino auth login",
1031
+ "After browser approval, run: rolino --agent auth status"
1032
+ ];
1033
+ case "FORBIDDEN":
1034
+ return [
1035
+ "Check granted access: rolino --agent auth status",
1036
+ "Ask the user to reauthorize Rolino with the required capability."
1037
+ ];
1038
+ case "BILLING_REQUIRED":
1039
+ case "LIMIT_REACHED":
1040
+ return ["Open the workspace billing page: https://getrolino.com/settings/billing"];
1041
+ case "FEATURE_DISABLED":
1042
+ return ["Check the project Connections page and hosted channel availability."];
1043
+ case "VALIDATION_ERROR":
1044
+ case "USAGE_ERROR":
1045
+ return [`rolino ${command} --help`];
1046
+ case "CONFLICT":
1047
+ return ["Fetch the latest resource state before retrying with a new decision."];
1048
+ case "RATE_LIMITED":
1049
+ return ["Wait for retryAfterSeconds before retrying."];
1050
+ case "TIMEOUT":
1051
+ case "NETWORK":
1052
+ return [
1053
+ "rolino --agent doctor",
1054
+ "Inspect current Rolino state before retrying a schedule or publish operation."
1055
+ ];
1056
+ default:
1057
+ return [];
1058
+ }
1059
+ }
1060
+ function commandContext(command, options, runtime) {
1061
+ return {
1062
+ stdout: runtime.stdout,
1063
+ stderr: runtime.stderr,
1064
+ format: resolveFormat(options, runtime),
1065
+ agent: options.agent ?? false,
1066
+ command,
1067
+ requestId: options.requestId ?? (0, import_node_crypto2.randomUUID)()
1068
+ };
1069
+ }
1070
+ async function execute(options) {
1071
+ let context = {
1072
+ stdout: options.runtime.stdout,
1073
+ stderr: options.runtime.stderr,
1074
+ format: options.runtime.isTTY ? "human" : "json",
1075
+ agent: options.global.agent ?? false,
1076
+ command: options.command,
1077
+ requestId: options.global.requestId ?? (0, import_node_crypto2.randomUUID)()
1078
+ };
1079
+ try {
1080
+ context = commandContext(options.command, options.global, options.runtime);
1081
+ await options.action(context, clientFor(options.global, options.runtime));
1082
+ return EXIT_CODES.success;
1083
+ } catch (error) {
1084
+ const outputError = errorForOutput(error);
1085
+ writeFailure(context, {
1086
+ ...outputError,
1087
+ suggestions: recoverySuggestions(outputError.code, options.command)
1088
+ });
1089
+ return exitCodeFor(error);
1090
+ }
1091
+ }
1092
+ async function runCli(argv = process.argv, overrides = {}) {
1093
+ const runtime = {
1094
+ stdout: overrides.stdout ?? process.stdout,
1095
+ stderr: overrides.stderr ?? process.stderr,
1096
+ isTTY: overrides.isTTY ?? Boolean(process.stdout.isTTY),
1097
+ env: overrides.env ?? process.env,
1098
+ fetch: overrides.fetch,
1099
+ cwd: overrides.cwd ?? process.cwd(),
1100
+ nodePath: overrides.nodePath ?? process.execPath,
1101
+ cliEntryPath: overrides.cliEntryPath ?? process.argv[1] ?? process.execPath,
1102
+ confirm: overrides.confirm ?? confirmInTerminal
1103
+ };
1104
+ let commandExitCode = EXIT_CODES.success;
1105
+ const parserStderr = [];
1106
+ const program = new import_commander.Command().name("rolino").description("Agent-friendly command-line interface for Rolino").version(ROLINO_CLI_VERSION).option("--base-url <url>", "hosted or self-hosted Rolino base URL").option("--output <format>", "human, json, or jsonl output", outputFormat).option("--json", "alias for --output json").option("--agent", "deterministic machine mode with next-step suggestions").option("--no-color", "disable color in human output").option("--timeout <duration>", "request timeout, for example 15s", duration).option("--request-id <id>", "caller correlation ID", requestId).showHelpAfterError().exitOverride().configureOutput({
1107
+ writeOut: (value) => runtime.stdout.write(value),
1108
+ writeErr: (value) => parserStderr.push(value)
1109
+ });
1110
+ program.command("whoami").description("Show the authenticated Rolino identity and organization").action(async () => {
1111
+ const global = program.opts();
1112
+ commandExitCode = await execute({
1113
+ command: "whoami",
1114
+ global,
1115
+ runtime,
1116
+ async action(context, client) {
1117
+ const actor = await client.whoami({ requestId: context.requestId });
1118
+ writeSuccess(
1119
+ context,
1120
+ actor,
1121
+ formatActor(actor),
1122
+ ["rolino projects list --agent"]
1123
+ );
1124
+ }
1125
+ });
1126
+ });
1127
+ const projects = program.command("projects").description("Read and create projects for the current Rolino actor");
1128
+ projects.command("list").description("List projects").option("--limit <number>", "maximum projects to return", (value) => {
1129
+ const parsed = Number(value);
1130
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
1131
+ throw new import_commander.InvalidArgumentError("Limit must be an integer from 1 to 100.");
1132
+ }
1133
+ return parsed;
1134
+ }).option("--cursor <id>", "continue after a project ID").action(async (local) => {
1135
+ const global = program.opts();
1136
+ commandExitCode = await execute({
1137
+ command: "projects list",
1138
+ global,
1139
+ runtime,
1140
+ async action(context, client) {
1141
+ const data = await client.projects.list(local, {
1142
+ requestId: context.requestId
1143
+ });
1144
+ const suggestions = data.items.slice(0, 3).map((project) => `rolino posts list --project ${project.id} --agent`);
1145
+ writeSuccess(context, data, formatProjects(data), suggestions);
1146
+ }
1147
+ });
1148
+ });
1149
+ projects.command("create").description("Create a saved project with its paired brand").requiredOption("--name <name>", "project and paired brand name").option("--type <type>", "project type; defaults to PRODUCT", projectType).option("--website <url>", "absolute http or https website URL").option("--description <text>", "optional project description").option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm creation without an interactive prompt").action(async (local) => {
1150
+ const global = program.opts();
1151
+ commandExitCode = await execute({
1152
+ command: "projects create",
1153
+ global,
1154
+ runtime,
1155
+ async action(context, client) {
1156
+ const parsed = import_contracts2.ProjectCreateInputSchema.safeParse({
1157
+ name: local.name,
1158
+ type: local.type,
1159
+ websiteUrl: local.website,
1160
+ description: local.description
1161
+ });
1162
+ if (!parsed.success) {
1163
+ throw new TypeError(
1164
+ parsed.error.issues[0]?.message ?? "Project creation input is invalid."
1165
+ );
1166
+ }
1167
+ await requireWriteConsent({
1168
+ yes: local.yes,
1169
+ global,
1170
+ runtime,
1171
+ subject: "project",
1172
+ preview: [
1173
+ `Create project "${parsed.data.name}"?`,
1174
+ `Type: ${parsed.data.type}`,
1175
+ `Website: ${parsed.data.websiteUrl ?? "none"}`,
1176
+ "A paired brand will be saved with the project.",
1177
+ "No research, connection, scheduling, or publishing will occur."
1178
+ ].join("\n")
1179
+ });
1180
+ const project = await client.projects.create(parsed.data, {
1181
+ requestId: context.requestId,
1182
+ idempotencyKey: local.idempotencyKey ?? context.requestId
1183
+ });
1184
+ writeSuccess(
1185
+ context,
1186
+ project,
1187
+ formatProject(project),
1188
+ [`rolino projects show ${project.id} --agent`]
1189
+ );
1190
+ }
1191
+ });
1192
+ });
1193
+ const media = program.command("media").description("List and upload reusable project media");
1194
+ media.command("list").description("List media assets in a project").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum assets to return", (value) => {
1195
+ const parsed = Number(value);
1196
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) throw new import_commander.InvalidArgumentError("Limit must be an integer from 1 to 100.");
1197
+ return parsed;
1198
+ }).option("--cursor <id>", "continue after a media asset ID").option("--type <type>", "IMAGE or VIDEO", (value) => {
1199
+ const normalized = value.toUpperCase();
1200
+ if (normalized !== "IMAGE" && normalized !== "VIDEO") throw new import_commander.InvalidArgumentError("Type must be IMAGE or VIDEO.");
1201
+ return normalized;
1202
+ }).option("--query <text>", "filter by original file name").action(async (local) => {
1203
+ const global = program.opts();
1204
+ commandExitCode = await execute({
1205
+ command: "media list",
1206
+ global,
1207
+ runtime,
1208
+ async action(context, client) {
1209
+ const { project, ...options } = local;
1210
+ const data = await client.media.list(project, options, { requestId: context.requestId });
1211
+ writeSuccess(context, data, formatMediaAssets(data), data.items.slice(0, 3).map((asset) => `rolino posts create --project ${project} --caption <text> --media ${asset.id} --agent --yes`));
1212
+ }
1213
+ });
1214
+ });
1215
+ media.command("upload").description("Upload one local image or video to a project's media library").argument("<file-path>").requiredOption("--project <project-id>", "exact Rolino project ID").option("--yes", "confirm upload without an interactive prompt").action(async (filePath, local) => {
1216
+ const global = program.opts();
1217
+ commandExitCode = await execute({
1218
+ command: "media upload",
1219
+ global,
1220
+ runtime,
1221
+ async action(context, client) {
1222
+ const absolutePath = (0, import_node_path2.resolve)(runtime.cwd, filePath);
1223
+ const details = await (0, import_promises2.stat)(absolutePath).catch(() => null);
1224
+ if (!details?.isFile()) throw new TypeError("The media path must point to a readable file.");
1225
+ const contentTypes = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".mp4": "video/mp4", ".mov": "video/quicktime" };
1226
+ const contentType = contentTypes[(0, import_node_path2.extname)(absolutePath).toLowerCase()];
1227
+ if (!contentType) throw new TypeError("Use a JPEG, PNG, WebP, MP4, or MOV file.");
1228
+ await requireWriteConsent({ yes: local.yes, global, runtime, subject: "media", preview: [`Upload ${(0, import_node_path2.basename)(absolutePath)} to Rolino?`, `Project: ${local.project}`, `Size: ${details.size} bytes`, "This adds a reusable media asset but does not create, schedule, or publish a post."].join("\n") });
1229
+ const bytes = await (0, import_promises2.readFile)(absolutePath);
1230
+ const asset = await client.media.upload(local.project, { fileName: (0, import_node_path2.basename)(absolutePath), contentType, fileSize: details.size, body: new Blob([bytes], { type: contentType }) }, { requestId: context.requestId });
1231
+ writeSuccess(context, asset, formatMediaAsset(asset), [`rolino posts create --project ${local.project} --caption <text> --media ${asset.id} --agent --yes`]);
1232
+ }
1233
+ });
1234
+ });
1235
+ const authCommands = program.command("auth").description("Authenticate the Rolino CLI");
1236
+ authCommands.command("login").description("Authorize this CLI through your browser").action(async () => {
1237
+ const global = program.opts();
1238
+ commandExitCode = await execute({
1239
+ command: "auth login",
1240
+ global,
1241
+ runtime,
1242
+ async action(context, client) {
1243
+ if (runtime.env.ROLINO_TOKEN) {
1244
+ throw new TypeError(
1245
+ "ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
1246
+ );
1247
+ }
1248
+ const meta = await client.meta({ requestId: context.requestId });
1249
+ if (!meta.authentication.cliBrowserAuthorization) {
1250
+ throw new TypeError(
1251
+ "This Rolino server does not support browser CLI authorization."
1252
+ );
1253
+ }
1254
+ const credential = await loginWithBrowser({
1255
+ baseUrl: client.baseUrl,
1256
+ stderr: runtime.stderr,
1257
+ exchange: (input) => client.auth.exchangeCliAuthorization(input, {
1258
+ requestId: context.requestId
1259
+ })
1260
+ });
1261
+ const authenticatedClient = new import_sdk.RolinoClient({
1262
+ baseUrl: client.baseUrl,
1263
+ token: credential.token,
1264
+ timeoutMs: client.timeoutMs,
1265
+ fetch: runtime.fetch
1266
+ });
1267
+ let credentialSaved = false;
1268
+ try {
1269
+ const actor = await authenticatedClient.whoami({
1270
+ requestId: context.requestId
1271
+ });
1272
+ const path = (0, import_local_auth.saveStoredCredential)(client.baseUrl, {
1273
+ token: credential.token,
1274
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1275
+ expiresAt: credential.expiresAt,
1276
+ organization: credential.organization
1277
+ }, runtime.env);
1278
+ credentialSaved = true;
1279
+ writeSuccess(
1280
+ context,
1281
+ {
1282
+ authenticated: true,
1283
+ user: actor.user,
1284
+ organization: actor.organization,
1285
+ capabilities: actor.capabilities,
1286
+ expiresAt: credential.expiresAt
1287
+ },
1288
+ [
1289
+ `Authenticated as ${actor.user.email}.`,
1290
+ `Workspace: ${actor.organization.name}`,
1291
+ `Expires: ${credential.expiresAt}`,
1292
+ `Credential saved in a permission-restricted file at ${path}`
1293
+ ].join("\n"),
1294
+ ["rolino whoami --agent", "rolino projects list --agent"]
1295
+ );
1296
+ } catch (error) {
1297
+ if (credentialSaved) {
1298
+ (0, import_local_auth.clearStoredCredential)(client.baseUrl, runtime.env);
1299
+ }
1300
+ await authenticatedClient.auth.revokeCurrentCredential({
1301
+ requestId: context.requestId
1302
+ }).catch(() => void 0);
1303
+ throw error;
1304
+ }
1305
+ }
1306
+ });
1307
+ });
1308
+ authCommands.command("status").description("Show the active CLI authentication source and identity").action(async () => {
1309
+ const global = program.opts();
1310
+ commandExitCode = await execute({
1311
+ command: "auth status",
1312
+ global,
1313
+ runtime,
1314
+ async action(context, client) {
1315
+ const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
1316
+ if (credential.source === "none") {
1317
+ writeSuccess(
1318
+ context,
1319
+ { authenticated: false, source: "none", baseUrl: client.baseUrl },
1320
+ `Not authenticated for ${client.baseUrl}`,
1321
+ ["rolino auth login"]
1322
+ );
1323
+ return;
1324
+ }
1325
+ const actor = await client.whoami({ requestId: context.requestId });
1326
+ writeSuccess(
1327
+ context,
1328
+ {
1329
+ authenticated: true,
1330
+ source: credential.source,
1331
+ baseUrl: client.baseUrl,
1332
+ user: actor.user,
1333
+ organization: actor.organization,
1334
+ ...credential.source === "stored" ? { expiresAt: credential.credential.expiresAt } : {}
1335
+ },
1336
+ [
1337
+ `Authenticated as ${actor.user.email}.`,
1338
+ `Workspace: ${actor.organization.name}`,
1339
+ `Source: ${credential.source}`
1340
+ ].join("\n")
1341
+ );
1342
+ }
1343
+ });
1344
+ });
1345
+ authCommands.command("logout").description("Revoke and remove the saved CLI credential").action(async () => {
1346
+ const global = program.opts();
1347
+ commandExitCode = await execute({
1348
+ command: "auth logout",
1349
+ global,
1350
+ runtime,
1351
+ async action(context, client) {
1352
+ const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
1353
+ if (credential.source === "environment") {
1354
+ throw new TypeError(
1355
+ "ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
1356
+ );
1357
+ }
1358
+ if (credential.source === "none") {
1359
+ writeSuccess(
1360
+ context,
1361
+ { revoked: false, reason: "not_authenticated", baseUrl: client.baseUrl },
1362
+ `No saved credential exists for ${client.baseUrl}`
1363
+ );
1364
+ return;
1365
+ }
1366
+ try {
1367
+ await client.auth.revokeCurrentCredential({
1368
+ requestId: context.requestId
1369
+ });
1370
+ } catch (error) {
1371
+ const alreadyUnavailable = error instanceof import_sdk.RolinoApiError && (error.code === "AUTH_REQUIRED" || error.code === "NOT_FOUND");
1372
+ if (!alreadyUnavailable) throw error;
1373
+ }
1374
+ (0, import_local_auth.clearStoredCredential)(client.baseUrl, runtime.env);
1375
+ writeSuccess(
1376
+ context,
1377
+ { revoked: true, baseUrl: client.baseUrl },
1378
+ "The CLI credential was revoked and removed from this machine."
1379
+ );
1380
+ }
1381
+ });
1382
+ });
1383
+ const setup = program.command("setup").description("Configure local agent tools for Rolino");
1384
+ setup.command("mcp").description("Configure the Rolino stdio MCP server for a supported client").requiredOption("--client <client>", "codex or claude-code", mcpClient).option("--scope <scope>", "user or project", mcpScope, "user").option("--server-path <path>", "absolute or working-directory-relative MCP server path").option("--dry-run", "show the intended configuration without writing it").option("--yes", "apply without an interactive confirmation").option("--force", "replace an existing Claude Code user-scoped Rolino server").action(async (local) => {
1385
+ const global = program.opts();
1386
+ commandExitCode = await execute({
1387
+ command: "setup mcp",
1388
+ global,
1389
+ runtime,
1390
+ async action(context, client) {
1391
+ const setupOptions = {
1392
+ ...local,
1393
+ baseUrl: client.baseUrl,
1394
+ cwd: runtime.cwd,
1395
+ env: runtime.env,
1396
+ nodePath: runtime.nodePath,
1397
+ cliEntryPath: runtime.cliEntryPath
1398
+ };
1399
+ const preview = await setupMcp({ ...setupOptions, dryRun: true });
1400
+ let result = preview;
1401
+ if (!local.dryRun && preview.changed) {
1402
+ if (!local.yes) {
1403
+ if (global.agent || !runtime.isTTY) {
1404
+ throw new TypeError(
1405
+ "MCP setup requires confirmation. Preview with --dry-run or apply with --yes."
1406
+ );
1407
+ }
1408
+ const approved = await runtime.confirm(formatMcpSetupPreview(preview));
1409
+ if (!approved) {
1410
+ throw new CliUserCancelledError("MCP setup was cancelled.");
1411
+ }
1412
+ }
1413
+ result = await setupMcp({ ...setupOptions, dryRun: false });
1414
+ }
1415
+ const state = result.dryRun ? "Would configure" : result.changed ? "Configured" : "Already configured";
1416
+ const clientLabel = result.client === "codex" ? "Codex" : "Claude Code";
1417
+ writeSuccess(
1418
+ context,
1419
+ result,
1420
+ [
1421
+ `${state} Rolino MCP for ${clientLabel}.`,
1422
+ `Scope: ${result.scope}`,
1423
+ `Target: ${result.target}`,
1424
+ ...result.backupPath && !result.dryRun ? [`Backup: ${result.backupPath}`] : [],
1425
+ `Rolino URL: ${client.baseUrl}`,
1426
+ "No token was written to MCP configuration."
1427
+ ].join("\n"),
1428
+ result.client === "codex" ? ["codex mcp list", "rolino auth login"] : ["claude mcp get rolino", "rolino auth login"]
1429
+ );
1430
+ }
1431
+ });
1432
+ });
1433
+ projects.command("show").description("Show one project").argument("<project-id>").action(async (projectId) => {
1434
+ const global = program.opts();
1435
+ commandExitCode = await execute({
1436
+ command: "projects show",
1437
+ global,
1438
+ runtime,
1439
+ async action(context, client) {
1440
+ const project = await client.projects.get(projectId, {
1441
+ requestId: context.requestId
1442
+ });
1443
+ writeSuccess(
1444
+ context,
1445
+ project,
1446
+ formatProject(project),
1447
+ [`rolino posts list --project ${project.id} --agent`]
1448
+ );
1449
+ }
1450
+ });
1451
+ });
1452
+ const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
1453
+ 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, or BLUESKY; 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("--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("--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) => {
1454
+ const global = program.opts();
1455
+ commandExitCode = await execute({
1456
+ command: "posts create",
1457
+ global,
1458
+ runtime,
1459
+ async action(context, client) {
1460
+ await requireWriteConsent({
1461
+ yes: local.yes,
1462
+ global,
1463
+ runtime,
1464
+ preview: [
1465
+ "Create a Rolino draft post?",
1466
+ `Project: ${local.project}`,
1467
+ `Destinations: ${local.platform.join(", ") || "none"}`,
1468
+ "This will not schedule or publish the post."
1469
+ ].join("\n")
1470
+ });
1471
+ const input = {
1472
+ caption: local.caption,
1473
+ platforms: local.platform,
1474
+ mediaAssetIds: local.media,
1475
+ captionOverrides: {
1476
+ ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1477
+ ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1478
+ ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1479
+ ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption }
1480
+ },
1481
+ tiktokSettings: null,
1482
+ youtubeSettings: youtubeSettings(local)
1483
+ };
1484
+ const post = await client.posts.create(local.project, input, {
1485
+ requestId: context.requestId,
1486
+ idempotencyKey: local.idempotencyKey ?? context.requestId
1487
+ });
1488
+ writeSuccess(
1489
+ context,
1490
+ post,
1491
+ formatPost(post),
1492
+ [`rolino posts readiness ${post.id} --project ${local.project} --agent`]
1493
+ );
1494
+ }
1495
+ });
1496
+ });
1497
+ posts.command("update").description("Replace an existing draft 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).requiredOption("--caption <text>", "replacement draft caption").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, or BLUESKY; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").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("--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 the update without an interactive prompt").action(async (postId, local) => {
1498
+ const global = program.opts();
1499
+ commandExitCode = await execute({
1500
+ command: "posts update",
1501
+ global,
1502
+ runtime,
1503
+ async action(context, client) {
1504
+ await requireWriteConsent({
1505
+ yes: local.yes,
1506
+ global,
1507
+ runtime,
1508
+ preview: [
1509
+ `Replace draft ${postId}?`,
1510
+ `Project: ${local.project}`,
1511
+ `Expected version: ${local.expectedVersion}`,
1512
+ "This will not schedule or publish the post."
1513
+ ].join("\n")
1514
+ });
1515
+ const input = {
1516
+ caption: local.caption,
1517
+ platforms: local.platform,
1518
+ mediaAssetIds: local.media,
1519
+ captionOverrides: {
1520
+ ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1521
+ ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1522
+ ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1523
+ ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption }
1524
+ },
1525
+ tiktokSettings: null,
1526
+ youtubeSettings: youtubeSettings(local)
1527
+ };
1528
+ const post = await client.posts.update(local.project, postId, input, {
1529
+ requestId: context.requestId,
1530
+ idempotencyKey: local.idempotencyKey ?? context.requestId,
1531
+ expectedVersion: local.expectedVersion
1532
+ });
1533
+ writeSuccess(
1534
+ context,
1535
+ post,
1536
+ formatPost(post),
1537
+ [`rolino posts readiness ${post.id} --project ${local.project} --agent`]
1538
+ );
1539
+ }
1540
+ });
1541
+ });
1542
+ posts.command("list").description("List posts in a project").requiredOption("--project <project-id>", "exact Rolino project ID").option("--limit <number>", "maximum posts to return", (value) => {
1543
+ const parsed = Number(value);
1544
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
1545
+ throw new import_commander.InvalidArgumentError("Limit must be an integer from 1 to 100.");
1546
+ }
1547
+ return parsed;
1548
+ }).option("--cursor <id>", "continue after a post ID").option("--status <status>", "filter by post status", postStatus).action(async (local) => {
1549
+ const global = program.opts();
1550
+ commandExitCode = await execute({
1551
+ command: "posts list",
1552
+ global,
1553
+ runtime,
1554
+ async action(context, client) {
1555
+ const { project, ...options } = local;
1556
+ const data = await client.posts.list(project, options, {
1557
+ requestId: context.requestId
1558
+ });
1559
+ const suggestions = data.items.slice(0, 3).map((post) => `rolino posts show ${post.id} --project ${project} --agent`);
1560
+ writeSuccess(context, data, formatPosts(data), suggestions);
1561
+ }
1562
+ });
1563
+ });
1564
+ posts.command("show").description("Show one post").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (postId, local) => {
1565
+ const global = program.opts();
1566
+ commandExitCode = await execute({
1567
+ command: "posts show",
1568
+ global,
1569
+ runtime,
1570
+ async action(context, client) {
1571
+ const post = await client.posts.get(local.project, postId, {
1572
+ requestId: context.requestId
1573
+ });
1574
+ writeSuccess(context, post, formatPost(post));
1575
+ }
1576
+ });
1577
+ });
1578
+ posts.command("readiness").description("Show deterministic publishing readiness for one post").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (postId, local) => {
1579
+ const global = program.opts();
1580
+ commandExitCode = await execute({
1581
+ command: "posts readiness",
1582
+ global,
1583
+ runtime,
1584
+ async action(context, client) {
1585
+ const readiness = await client.posts.readiness(
1586
+ local.project,
1587
+ postId,
1588
+ { requestId: context.requestId }
1589
+ );
1590
+ writeSuccess(
1591
+ context,
1592
+ readiness,
1593
+ formatPostReadiness(readiness),
1594
+ [`rolino integrations health --project ${local.project} --agent`]
1595
+ );
1596
+ }
1597
+ });
1598
+ });
1599
+ const schedule = posts.command("schedule").description("Preview and execute server-confirmed post scheduling");
1600
+ schedule.command("preview").description("Validate an exact schedule 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("--at <datetime>", "ISO-8601 publishing time", isoDateTime).requiredOption("--timezone <iana-timezone>", "IANA timezone such as America/Toronto").action(async (postId, local) => {
1601
+ const global = program.opts();
1602
+ commandExitCode = await execute({
1603
+ command: "posts schedule preview",
1604
+ global,
1605
+ runtime,
1606
+ async action(context, client) {
1607
+ const preview = await client.posts.previewSchedule(
1608
+ local.project,
1609
+ postId,
1610
+ { scheduledAt: local.at, timezone: local.timezone },
1611
+ {
1612
+ expectedVersion: local.expectedVersion,
1613
+ requestId: context.requestId
1614
+ }
1615
+ );
1616
+ writeSuccess(
1617
+ context,
1618
+ preview,
1619
+ formatPostSchedulePreview(preview),
1620
+ [
1621
+ `rolino posts schedule execute ${postId} --project ${local.project} --expected-version ${local.expectedVersion} --at ${preview.schedule.scheduledAt} --timezone ${preview.schedule.timezone} --confirmation-token <token> --agent`
1622
+ ]
1623
+ );
1624
+ }
1625
+ });
1626
+ });
1627
+ schedule.command("execute").description("Consume a matching confirmation and schedule the post").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "version used by schedule preview", positiveInteger).requiredOption("--at <datetime>", "exact ISO-8601 time returned by preview", isoDateTime).requiredOption("--timezone <iana-timezone>", "exact IANA timezone returned by preview").option("--confirmation-token <token>", "one-time token returned by schedule preview; ROLINO_CONFIRMATION_TOKEN is safer for scripts").option("--idempotency-key <key>", "stable retry key; defaults to the request ID").action(async (postId, local) => {
1628
+ const global = program.opts();
1629
+ commandExitCode = await execute({
1630
+ command: "posts schedule execute",
1631
+ global,
1632
+ runtime,
1633
+ async action(context, client) {
1634
+ const confirmationToken = local.confirmationToken ?? runtime.env.ROLINO_CONFIRMATION_TOKEN;
1635
+ if (!confirmationToken) {
1636
+ throw new TypeError(
1637
+ "A confirmation token is required. Preview first, then use --confirmation-token or ROLINO_CONFIRMATION_TOKEN."
1638
+ );
1639
+ }
1640
+ const result = await client.posts.executeSchedule(
1641
+ local.project,
1642
+ postId,
1643
+ {
1644
+ scheduledAt: local.at,
1645
+ timezone: local.timezone,
1646
+ confirmationToken
1647
+ },
1648
+ {
1649
+ expectedVersion: local.expectedVersion,
1650
+ idempotencyKey: local.idempotencyKey ?? context.requestId,
1651
+ requestId: context.requestId
1652
+ }
1653
+ );
1654
+ writeSuccess(
1655
+ context,
1656
+ result,
1657
+ result.status === "PENDING" ? formatPostSchedulePending(result) : formatPost(result),
1658
+ [`rolino calendar list --project ${local.project} --agent`]
1659
+ );
1660
+ }
1661
+ });
1662
+ });
1663
+ const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
1664
+ 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, or BLUESKY; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1665
+ const global = program.opts();
1666
+ commandExitCode = await execute({
1667
+ command: "posts publish preview",
1668
+ global,
1669
+ runtime,
1670
+ async action(context, client) {
1671
+ const preview = await client.posts.previewPublish(
1672
+ local.project,
1673
+ postId,
1674
+ { destinations: local.platform },
1675
+ {
1676
+ expectedVersion: local.expectedVersion,
1677
+ requestId: context.requestId
1678
+ }
1679
+ );
1680
+ const platforms = preview.destinations.map((provider) => `--platform ${provider}`).join(" ");
1681
+ writeSuccess(
1682
+ context,
1683
+ preview,
1684
+ formatPostPublishPreview(preview),
1685
+ [
1686
+ `rolino posts publish execute ${postId} --project ${local.project} --expected-version ${local.expectedVersion} ${platforms} --confirmation-token <token> --agent`
1687
+ ]
1688
+ );
1689
+ }
1690
+ });
1691
+ });
1692
+ publish.command("execute").description("Consume a matching confirmation and queue publishing now").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "version used by publish preview", positiveInteger).requiredOption("--platform <provider>", "exact destination from preview; repeat for both", collectPublishingProvider).option("--confirmation-token <token>", "one-time token returned by publish preview; ROLINO_CONFIRMATION_TOKEN is safer for scripts").option("--idempotency-key <key>", "stable retry key; defaults to the request ID").action(async (postId, local) => {
1693
+ const global = program.opts();
1694
+ commandExitCode = await execute({
1695
+ command: "posts publish execute",
1696
+ global,
1697
+ runtime,
1698
+ async action(context, client) {
1699
+ const confirmationToken = local.confirmationToken ?? runtime.env.ROLINO_CONFIRMATION_TOKEN;
1700
+ if (!confirmationToken) {
1701
+ throw new TypeError(
1702
+ "A confirmation token is required. Preview first, then use --confirmation-token or ROLINO_CONFIRMATION_TOKEN."
1703
+ );
1704
+ }
1705
+ const post = await client.posts.executePublish(
1706
+ local.project,
1707
+ postId,
1708
+ {
1709
+ destinations: local.platform,
1710
+ confirmationToken
1711
+ },
1712
+ {
1713
+ expectedVersion: local.expectedVersion,
1714
+ idempotencyKey: local.idempotencyKey ?? context.requestId,
1715
+ requestId: context.requestId
1716
+ }
1717
+ );
1718
+ writeSuccess(
1719
+ context,
1720
+ post,
1721
+ formatPost(post),
1722
+ [`rolino posts show ${postId} --project ${local.project} --agent`]
1723
+ );
1724
+ }
1725
+ });
1726
+ });
1727
+ const integrations = program.command("integrations").description("Read publishing integration state");
1728
+ integrations.command("health").description("Show cached health for every enabled publishing provider").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
1729
+ const global = program.opts();
1730
+ commandExitCode = await execute({
1731
+ command: "integrations health",
1732
+ global,
1733
+ runtime,
1734
+ async action(context, client) {
1735
+ const health = await client.integrations.health(local.project, {
1736
+ requestId: context.requestId
1737
+ });
1738
+ writeSuccess(
1739
+ context,
1740
+ health,
1741
+ formatIntegrationHealth(health),
1742
+ [`rolino posts list --project ${local.project} --agent`]
1743
+ );
1744
+ }
1745
+ });
1746
+ });
1747
+ const calendar = program.command("calendar").description("Read scheduled publishing events");
1748
+ calendar.command("list").description("List scheduled posts in a bounded calendar window").requiredOption("--project <project-id>", "exact Rolino project ID").option("--from <datetime>", "inclusive ISO-8601 window start", isoDateTime).option("--to <datetime>", "exclusive ISO-8601 window end", isoDateTime).option("--limit <number>", "maximum events to return", (value) => {
1749
+ const parsed = Number(value);
1750
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
1751
+ throw new import_commander.InvalidArgumentError("Limit must be an integer from 1 to 100.");
1752
+ }
1753
+ return parsed;
1754
+ }).option("--cursor <cursor>", "continue from a calendar cursor").action(async (local) => {
1755
+ const global = program.opts();
1756
+ commandExitCode = await execute({
1757
+ command: "calendar list",
1758
+ global,
1759
+ runtime,
1760
+ async action(context, client) {
1761
+ if (local.from === void 0 !== (local.to === void 0)) {
1762
+ throw new TypeError("--from and --to must be provided together.");
1763
+ }
1764
+ const { project, ...options } = local;
1765
+ const events = await client.calendar.list(project, options, {
1766
+ requestId: context.requestId
1767
+ });
1768
+ writeSuccess(
1769
+ context,
1770
+ events,
1771
+ formatCalendarEvents(events),
1772
+ events.items.slice(0, 3).map((event) => `rolino posts show ${event.postId} --project ${project} --agent`)
1773
+ );
1774
+ }
1775
+ });
1776
+ });
1777
+ program.command("doctor").description("Check configuration, compatibility, and authentication").action(async () => {
1778
+ const global = program.opts();
1779
+ commandExitCode = await execute({
1780
+ command: "doctor",
1781
+ global,
1782
+ runtime,
1783
+ async action(context, client) {
1784
+ const meta = await client.meta({ requestId: context.requestId });
1785
+ const checks = [
1786
+ {
1787
+ name: "server",
1788
+ status: "pass",
1789
+ message: `${meta.name} ${meta.serverVersion} is reachable.`
1790
+ },
1791
+ {
1792
+ name: "api",
1793
+ status: meta.apiVersion === "v1" ? "pass" : "fail",
1794
+ message: `API ${meta.apiVersion}, contracts ${meta.contractVersion}.`
1795
+ },
1796
+ {
1797
+ name: "remote-mcp",
1798
+ status: meta.mcp.streamableHttp ? "pass" : "info",
1799
+ message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
1800
+ }
1801
+ ];
1802
+ const credential = (0, import_local_auth.resolveCredential)(client.baseUrl, runtime.env);
1803
+ if (credential.source !== "none") {
1804
+ const actor = await client.whoami({ requestId: context.requestId });
1805
+ checks.push({
1806
+ name: "authentication",
1807
+ status: "pass",
1808
+ message: `Authenticated as ${actor.user.email} via ${credential.source} credential.`
1809
+ });
1810
+ } else {
1811
+ checks.push({
1812
+ name: "authentication",
1813
+ status: "info",
1814
+ message: "No environment or saved credential is available; run rolino auth login."
1815
+ });
1816
+ }
1817
+ const data = { healthy: true, checks, server: meta };
1818
+ const human = checks.map((check) => `${check.status.toUpperCase()} ${check.name} ${check.message}`).join("\n");
1819
+ writeSuccess(context, data, human);
1820
+ }
1821
+ });
1822
+ });
1823
+ try {
1824
+ await program.parseAsync(argv);
1825
+ return commandExitCode;
1826
+ } catch (error) {
1827
+ if (error instanceof import_commander.CommanderError) {
1828
+ if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
1829
+ return EXIT_CODES.success;
1830
+ }
1831
+ const global = program.opts();
1832
+ let context;
1833
+ try {
1834
+ context = commandContext("usage", global, runtime);
1835
+ } catch {
1836
+ context = {
1837
+ stdout: runtime.stdout,
1838
+ stderr: runtime.stderr,
1839
+ format: runtime.isTTY ? "human" : "json",
1840
+ agent: global.agent ?? false,
1841
+ command: "usage",
1842
+ requestId: global.requestId ?? (0, import_node_crypto2.randomUUID)()
1843
+ };
1844
+ }
1845
+ if (context.format === "human") {
1846
+ runtime.stderr.write(
1847
+ parserStderr.join("") || `Error: ${error.message}
1848
+ `
1849
+ );
1850
+ } else {
1851
+ writeFailure(context, {
1852
+ code: "USAGE_ERROR",
1853
+ message: error.message
1854
+ });
1855
+ }
1856
+ return EXIT_CODES.usage;
1857
+ }
1858
+ throw error;
1859
+ }
1860
+ }
1861
+ // Annotate the CommonJS export names for ESM import in node:
1862
+ 0 && (module.exports = {
1863
+ EXIT_CODES,
1864
+ ROLINO_CLI_VERSION,
1865
+ runCli,
1866
+ setupMcp,
1867
+ updateClaudeProjectConfig,
1868
+ updateCodexConfig
1869
+ });
1870
+ //# sourceMappingURL=index.cjs.map