@ronana/mcp 0.2.0 → 0.3.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ronana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Ronana CLI and MCP
2
2
 
3
- NOTE: MCP and the command line are in admin preview. At launch they will
4
- require a paid plan.
3
+ Requires a Ronana paid plan.
5
4
 
6
5
  WARNING: Generations spend your account credits.
7
6
 
@@ -39,7 +38,6 @@ ronana generate create google/nano-banana-2-lite/edit --prompt "make it dusk" --
39
38
  ronana generate get <id> | wait <id> [--out dir]
40
39
  ronana generate list
41
40
  ronana upscale <id | file | url> --resolution 4k --out ./renders
42
- ronana improve google/nano-banana-2-lite/edit --prompt "make it dusk"
43
41
  ronana folder list | add <folder> <id…>
44
42
  ```
45
43
 
@@ -84,9 +82,27 @@ add it to the app's MCP settings:
84
82
  }
85
83
  ```
86
84
 
85
+ On Windows, start it through `cmd`, or the app cannot find `npx`:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "ronana": {
91
+ "command": "cmd",
92
+ "args": ["/c", "npx", "-y", "-p", "@ronana/mcp", "ronana-mcp"],
93
+ "env": { "RONANA_MCP_UPLOAD_ROOT": "C:\\Users\\you\\Pictures\\ronana" }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ In Claude Code: `claude mcp add ronana -- cmd /c npx -y -p @ronana/mcp ronana-mcp`.
100
+ In PowerShell, quote the separator (`'--'`), or PowerShell drops it.
101
+
87
102
  The local server adds one tool, `upload_local_reference`, which uploads a file
88
103
  from inside `RONANA_MCP_UPLOAD_ROOT`. Set it to the folder that holds your
89
- media, never a home directory or a drive root. Files larger than 3 MB stream
104
+ media. The tool is off until it is set, and it refuses a home directory or a
105
+ drive root. Files larger than 3 MB stream
90
106
  straight to storage, and Ronana checks each file before it adds it to your
91
107
  library.
92
108
 
@@ -100,7 +116,6 @@ library.
100
116
  | `generate` | Start an image or video generation. |
101
117
  | `get_generation` | Get one generation and its download link. Pass `wait_seconds` (up to 45) to wait for the result in one call. |
102
118
  | `upscale` | Upscale a video to a higher resolution. |
103
- | `improve_prompt` | Rewrite a prompt so it works better on one model. |
104
119
  | `list_generations` | List recent generations. |
105
120
  | `list_references` | List library references. |
106
121
  | `upload_reference` | Add a file to the library from a URL (up to 25 MB) or from base64 (under 3 MB). |
package/dist/cli.js CHANGED
@@ -13,10 +13,9 @@
13
13
  // ronana generate get | wait <id> [--out dir]
14
14
  // ronana generate list [--limit n]
15
15
  // ronana upscale <id | file | https-url> [--resolution r] [--model m] [--wait] [--out dir]
16
- // ronana improve <model> --prompt "…"
17
16
  // ronana folder list | add <folder> <id…>
18
17
  import { randomUUID } from "node:crypto";
19
- import { createWriteStream } from "node:fs";
18
+ import { createWriteStream, existsSync, statSync } from "node:fs";
20
19
  import { mkdir } from "node:fs/promises";
21
20
  import { basename, extname, join, resolve } from "node:path";
22
21
  import { Readable } from "node:stream";
@@ -39,7 +38,6 @@ const HELP = `Usage: ronana <command>
39
38
  generate wait <id> Wait for a generation to finish
40
39
  generate list Recent generations (--limit n)
41
40
  upscale <id|file|url> Upscale a video (costs credits; --resolution, --model)
42
- improve <model> --prompt "…" Rewrite a prompt for a model (costs a little)
43
41
  folder list Your library folders
44
42
  folder add <folder> <id…> File items into a folder (made if missing)
45
43
 
@@ -72,6 +70,8 @@ function describeField(name, schema, required) {
72
70
  parts.push(`default ${JSON.stringify(schema.default)}`);
73
71
  return ` --${name.padEnd(22)} ${parts.join(" ")}${required ? " (required)" : ""}`;
74
72
  }
73
+ /** Model ids are lowercase paths; forgive the casing and a trailing slash. */
74
+ const modelId = (raw) => raw.trim().replace(/\/+$/, "").toLowerCase();
75
75
  const TERMINAL = new Set(["completed", "failed", "canceled"]);
76
76
  function seconds(args, name, fallback) {
77
77
  const raw = flag(args, name);
@@ -122,6 +122,8 @@ const EXT_BY_TYPE = {
122
122
  "video/mp4": ".mp4", "video/webm": ".webm", "video/quicktime": ".mov",
123
123
  };
124
124
  async function download(gen, dir) {
125
+ if (existsSync(dir) && !statSync(dir).isDirectory())
126
+ throw new Error(`--out must be a folder; ${dir} is a file.`);
125
127
  await mkdir(dir, { recursive: true });
126
128
  const saved = [];
127
129
  for (const output of gen.outputs) {
@@ -175,6 +177,8 @@ async function generateCommand(sub, args) {
175
177
  const [target] = args.positional;
176
178
  if (sub === "list") {
177
179
  const limit = Number(flag(args, "limit") ?? 25);
180
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
181
+ throw new Error("--limit must be a whole number from 1 to 100.");
178
182
  const rows = await withRemote((r) => callTool(r, "list_generations", { limit }));
179
183
  return emit(rows, () => rows.forEach((g) => print(`${g.id.padEnd(28)} ${g.status.padEnd(11)} ${g.created_at}`)));
180
184
  }
@@ -189,7 +193,7 @@ async function generateCommand(sub, args) {
189
193
  if (sub !== "create" && sub !== "cost")
190
194
  throw new Error("Use generate create | cost | get | wait | list.");
191
195
  await withRemote(async (remote) => {
192
- const contract = await callTool(remote, "get_model", { id: target });
196
+ const contract = await callTool(remote, "get_model", { id: modelId(target) });
193
197
  const input = await buildInput(contract, args);
194
198
  await replaceLocalMedia(contract, input, async (path) => {
195
199
  note(`Uploading ${basename(path)}…`);
@@ -214,17 +218,23 @@ async function generateCommand(sub, args) {
214
218
  async function modelCommand(sub, args) {
215
219
  if (sub === "list" || sub === undefined) {
216
220
  const kind = flag(args, "kind");
221
+ if (kind !== undefined && kind !== "image" && kind !== "video")
222
+ throw new Error("--kind must be image or video.");
217
223
  const filter = args.positional[0]?.toLowerCase();
218
224
  const models = (await withRemote((r) => callTool(r, "list_models", kind ? { kind } : {})))
219
225
  .filter((m) => !filter || m.id.toLowerCase().includes(filter) || m.name.toLowerCase().includes(filter));
220
- return emit(models, () => models.forEach((m) => print(`${m.id.padEnd(52)} ${m.kind.padEnd(6)} ${m.name}${m.best_for ? ` ${m.best_for}` : ""}`)));
226
+ return emit(models, () => {
227
+ if (!models.length)
228
+ note(filter ? `No models match "${args.positional[0]}".` : "No models are available on your plan.");
229
+ models.forEach((m) => print(`${m.id.padEnd(52)} ${m.kind.padEnd(6)} ${m.name}${m.best_for ? ` ${m.best_for}` : ""}`));
230
+ });
221
231
  }
222
232
  if (sub !== "get")
223
233
  throw new Error("Use model list | get.");
224
234
  const id = args.positional[0];
225
235
  if (!id)
226
236
  throw new Error("Usage: ronana model get <model>");
227
- const contract = await withRemote((r) => callTool(r, "get_model", { id }));
237
+ const contract = await withRemote((r) => callTool(r, "get_model", { id: modelId(id) }));
228
238
  emit(contract, () => {
229
239
  print(`${contract.name} (${contract.kind})`);
230
240
  print(contract.id);
@@ -268,15 +278,26 @@ async function folderCommand(sub, args) {
268
278
  if (sub !== "add")
269
279
  throw new Error("Use folder list | add.");
270
280
  const [folder, ...ids] = args.positional;
271
- if (!folder || ids.length === 0)
281
+ if (!folder?.trim() || ids.length === 0)
272
282
  throw new Error("Usage: ronana folder add <folder> <id…>");
283
+ if (folder.trim().length > 60)
284
+ throw new Error("Folder names are at most 60 characters.");
273
285
  const filed = await withRemote((r) => callTool(r, "add_to_folder", { ids, folder, create: true }));
274
- return emit(filed, () => print(`Filed ${filed.filed.length} in "${filed.folder.name}".`));
286
+ emit(filed, () => {
287
+ print(`Filed ${filed.filed.length} in "${filed.folder.name}".`);
288
+ if (filed.not_found?.length)
289
+ note(`Not found: ${filed.not_found.join(", ")}`);
290
+ });
291
+ if (filed.not_found?.length)
292
+ process.exitCode = 1;
275
293
  }
276
294
  async function main() {
277
- const [command, sub, ...rest] = process.argv.slice(2);
295
+ const [command, ...rest] = process.argv.slice(2);
278
296
  const takesSub = command === "auth" || command === "model" || command === "generate" || command === "folder";
279
- const args = parseArgs(takesSub ? rest : [sub, ...rest].filter((v) => v !== undefined));
297
+ const args = parseArgs(rest);
298
+ // The subcommand is the first word that is not a flag, wherever the flags
299
+ // sit (`ronana generate --json list`, `ronana generate --help`).
300
+ const sub = takesSub ? args.positional.shift() : undefined;
280
301
  JSON_OUT = has(args, "json");
281
302
  if (!command || ["help", "--help", "-h"].includes(command) || has(args, "help"))
282
303
  return void process.stdout.write(HELP);
@@ -310,6 +331,8 @@ async function main() {
310
331
  const source = args.positional[0];
311
332
  if (!source)
312
333
  throw new Error("Usage: ronana upload <file | https-url>");
334
+ if (/^http:\/\//i.test(source))
335
+ throw new Error("Use an https:// link; plain http is not accepted.");
313
336
  const ref = await withRemote((r) => /^https:\/\//i.test(source)
314
337
  ? callTool(r, "upload_reference", { url: source, filename: basename(new URL(source).pathname) || "upload" })
315
338
  : uploadFile(r, resolve(source)));
@@ -317,14 +340,6 @@ async function main() {
317
340
  }
318
341
  case "generate": return generateCommand(sub, args);
319
342
  case "upscale": return upscaleCommand(args);
320
- case "improve": {
321
- const model = args.positional[0];
322
- const prompt = flag(args, "prompt");
323
- if (!model || !prompt)
324
- throw new Error("Usage: ronana improve <model> --prompt \"…\"");
325
- const improved = await withRemote((r) => callTool(r, "improve_prompt", { model, prompt }));
326
- return emit(improved, () => print(improved.prompt));
327
- }
328
343
  case "folder":
329
344
  case "folders": return folderCommand(command === "folders" ? "list" : sub, args);
330
345
  default: throw new Error(`Unknown command "${command}". Run \`ronana --help\`.`);
@@ -332,7 +347,13 @@ async function main() {
332
347
  }
333
348
  main().catch((error) => {
334
349
  if (error instanceof ToolError) {
335
- const { code, message, url, billing } = error.error;
350
+ const { code, url, billing, field } = error.error;
351
+ let message = String(error.error.message ?? "The request failed.");
352
+ if (code === "MODEL_UNKNOWN")
353
+ message = "This model is not available. Run `ronana model list` for the current ids.";
354
+ // Name the field the server refused, unless the message already does.
355
+ else if (typeof field === "string" && !message.includes(field))
356
+ message = `${field}: ${message}`;
336
357
  if (JSON_OUT)
337
358
  process.stdout.write(`${JSON.stringify({ ok: false, error: error.error }, null, 2)}\n`);
338
359
  else
package/dist/input.js CHANGED
@@ -49,7 +49,7 @@ function coerce(field, raw, schema) {
49
49
  if (type === "integer" || type === "number") {
50
50
  const n = Number(raw);
51
51
  if (!Number.isFinite(n) || (type === "integer" && !Number.isInteger(n)))
52
- throw new Error(`--${field} must be a ${type}.`);
52
+ throw new Error(`--${field} must be ${type === "integer" ? "a whole number" : "a number"}.`);
53
53
  return n;
54
54
  }
55
55
  if (type === "boolean") {
@@ -70,7 +70,9 @@ function coerce(field, raw, schema) {
70
70
  async function readInputFlag(raw) {
71
71
  if (!raw)
72
72
  return {};
73
- const text = raw.startsWith("@") ? await readFile(raw.slice(1), "utf8") : raw;
73
+ const text = raw.startsWith("@")
74
+ ? await readFile(raw.slice(1), "utf8").catch(() => { throw new Error(`--input: cannot read ${raw.slice(1)}.`); })
75
+ : raw;
74
76
  let parsed;
75
77
  try {
76
78
  parsed = JSON.parse(text);
package/dist/oauth.js CHANGED
@@ -26,15 +26,19 @@ export function readCredentials() {
26
26
  function writeCredentials(value) {
27
27
  entry().setPassword(JSON.stringify(value));
28
28
  }
29
+ /** Ends a stored sign-in on the server, so it leaves the account's connections.
30
+ * Best effort: an unreachable server still lets the local step go ahead. */
31
+ async function revoke(credentials) {
32
+ await fetch(`${credentials.baseUrl}/api/auth/oauth2/revoke`, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
35
+ body: new URLSearchParams({ token: credentials.refreshToken, client_id: credentials.clientId }),
36
+ }).catch(() => null);
37
+ }
29
38
  export async function logout() {
30
39
  const current = readCredentials();
31
- if (current) {
32
- await fetch(`${current.baseUrl}/api/auth/oauth2/revoke`, {
33
- method: "POST",
34
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
35
- body: new URLSearchParams({ token: current.refreshToken, client_id: current.clientId }),
36
- }).catch(() => null);
37
- }
40
+ if (current)
41
+ await revoke(current);
38
42
  try {
39
43
  entry().deletePassword();
40
44
  }
@@ -64,6 +68,14 @@ async function tokenRequest(baseUrl, fields) {
64
68
  }
65
69
  export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ronana.ai", openInBrowser = true) {
66
70
  baseUrl = baseUrl.replace(/\/$/, "");
71
+ const parsed = (() => { try {
72
+ return new URL(baseUrl);
73
+ }
74
+ catch {
75
+ return null;
76
+ } })();
77
+ if (!parsed || !/^https?:$/.test(parsed.protocol))
78
+ throw new Error("--base-url must be a web address, like https://ronana.ai.");
67
79
  const verifier = randomBytes(32).toString("base64url");
68
80
  const challenge = createHash("sha256").update(verifier).digest("base64url");
69
81
  const state = randomBytes(24).toString("base64url");
@@ -75,11 +87,13 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
75
87
  const close = () => { clearTimeout(timer); server.close(); };
76
88
  const server = createServer((req, res) => {
77
89
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
78
- if (url.searchParams.get("state") !== state) {
79
- res.writeHead(400).end("Invalid OAuth state.");
80
- fail(new Error("OAuth state did not match."));
90
+ // Anything but our own callback (a favicon, a prefetch, a stale tab from an
91
+ // earlier sign-in) is ignored, so it cannot end the sign-in it isn't part of.
92
+ if (url.pathname !== "/callback" || url.searchParams.get("state") !== state) {
93
+ res.writeHead(404).end();
94
+ return;
81
95
  }
82
- else if (url.searchParams.get("error")) {
96
+ if (url.searchParams.get("error")) {
83
97
  res.writeHead(400).end("Authorization was not completed.");
84
98
  fail(new Error(url.searchParams.get("error_description") ?? url.searchParams.get("error")));
85
99
  }
@@ -113,8 +127,10 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
113
127
  response_types: ["code"],
114
128
  scope: SCOPES,
115
129
  }),
130
+ }).catch(() => { throw new Error(`Could not reach ${baseUrl}. Check --base-url and your connection.`); });
131
+ const registered = await registration.json().catch(() => {
132
+ throw new Error(`No Ronana sign-in at ${baseUrl}. Check --base-url.`);
116
133
  });
117
- const registered = await registration.json();
118
134
  if (!registration.ok || typeof registered.client_id !== "string")
119
135
  throw new Error("OAuth client registration failed.");
120
136
  const authorize = new URL(`${baseUrl}/api/auth/oauth2/authorize`);
@@ -144,6 +160,9 @@ ${authorize}
144
160
  });
145
161
  if (typeof tokens.access_token !== "string" || typeof tokens.refresh_token !== "string")
146
162
  throw new Error("OAuth response did not contain tokens.");
163
+ // Signing in again replaces the stored sign-in; end the old one too, or
164
+ // every login leaves another connection behind on the account.
165
+ const previous = readCredentials();
147
166
  writeCredentials({
148
167
  baseUrl,
149
168
  clientId: registered.client_id,
@@ -151,6 +170,8 @@ ${authorize}
151
170
  refreshToken: tokens.refresh_token,
152
171
  expiresAt: Date.now() + Number(tokens.expires_in ?? 900) * 1000,
153
172
  });
173
+ if (previous && previous.clientId !== registered.client_id)
174
+ await revoke(previous);
154
175
  }
155
176
  finally {
156
177
  callback.close();
@@ -210,6 +231,13 @@ export async function accessToken() {
210
231
  client_id: current.clientId,
211
232
  refresh_token: current.refreshToken,
212
233
  resource: `${current.baseUrl}/mcp`,
234
+ }).catch((error) => {
235
+ // A network failure (fetch's TypeError) may pass; a refusal means this
236
+ // sign-in is over (disconnected on the website, or expired), and the
237
+ // server's own words ("session not found") would read as something else.
238
+ if (error instanceof TypeError)
239
+ throw error;
240
+ throw new Error("Your Ronana sign-in has ended (disconnected or expired). Run `ronana auth login` again.");
213
241
  });
214
242
  if (typeof tokens.access_token !== "string")
215
243
  throw new Error("OAuth refresh failed.");
package/dist/remote.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // The one connection to Ronana's remote MCP endpoint, shared by the stdio
2
2
  // bridge (server.ts) and the CLI (cli.ts). Both sign in once, with the same
3
3
  // stored OAuth credential, and every call runs the server's own gates.
4
- import { createReadStream, statSync } from "node:fs";
4
+ import { createReadStream, existsSync, statSync } from "node:fs";
5
5
  import { readFile } from "node:fs/promises";
6
6
  import { basename, extname } from "node:path";
7
7
  import { Readable } from "node:stream";
@@ -27,7 +27,7 @@ export async function remoteClient() {
27
27
  const advertised = await fetch(`${credentials.baseUrl}/.well-known/oauth-protected-resource/mcp`)
28
28
  .then((r) => r.ok, () => false);
29
29
  throw new Error(advertised
30
- ? "This account does not have access to Ronana's CLI and MCP yet."
30
+ ? `Ronana's CLI and MCP need a paid plan: ${credentials.baseUrl}/pricing`
31
31
  : `No Ronana MCP endpoint at ${credentials.baseUrl}. Sign in again with the right --base-url.`);
32
32
  }
33
33
  throw error;
@@ -53,6 +53,11 @@ export async function callTool(remote, name, args = {}) {
53
53
  if (body?.error)
54
54
  throw new ToolError(body.error);
55
55
  const text = Array.isArray(response.content) ? response.content.map((c) => ("text" in c ? c.text : "")).join(" ") : "";
56
+ // The SDK's own argument check answers in prose ("Input validation error:
57
+ // Invalid arguments for tool x: limit: Invalid input"); keep only the field.
58
+ const invalid = /Invalid arguments for tool \S+: ([\w.[\]-]+):/.exec(text);
59
+ if (invalid)
60
+ throw new ToolError({ code: "INVALID_REQUEST", message: `The value for "${invalid[1]}" is not valid.`, field: invalid[1] });
56
61
  throw new ToolError({ code: "TOOL_FAILED", message: text || `${name} failed.` });
57
62
  }
58
63
  /* Must match upload_reference's `data` cap (3 MB of bytes ≈ 4.2 MB of base64,
@@ -67,7 +72,14 @@ const CONTENT_TYPES = {
67
72
  * as base64; larger ones stream straight to storage through a signed PUT. */
68
73
  export async function uploadFile(remote, path) {
69
74
  const filename = basename(path);
70
- const { size } = statSync(path);
75
+ if (!existsSync(path))
76
+ throw new Error(`No file at ${path}.`);
77
+ const stat = statSync(path);
78
+ if (!stat.isFile())
79
+ throw new Error(`${path} is a folder, not a file.`);
80
+ const { size } = stat;
81
+ if (size === 0)
82
+ throw new Error(`${filename} is empty.`);
71
83
  if (size <= INLINE_LIMIT) {
72
84
  const data = (await readFile(path)).toString("base64");
73
85
  return callTool(remote, "upload_reference", { data, filename });
package/dist/server.js CHANGED
@@ -1,15 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpath, stat } from "node:fs/promises";
3
- import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, parse, relative, resolve, sep } from "node:path";
4
5
  import { fromJsonSchema, McpServer } from "@modelcontextprotocol/server";
5
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
6
7
  import { z } from "zod4";
7
8
  import { login, logout, readCredentials } from "./oauth.js";
8
9
  import { remoteClient, ToolError, uploadFile } from "./remote.js";
9
- const UPLOAD_ROOT = resolve(process.env.RONANA_MCP_UPLOAD_ROOT ?? process.cwd());
10
+ /* Uploads are off until the user names a folder: defaulting to the working
11
+ directory handed an AI app whatever folder it happened to start in, often
12
+ the home folder. A home folder or a drive root is refused for the same
13
+ reason. */
14
+ const UPLOAD_ROOT = process.env.RONANA_MCP_UPLOAD_ROOT ? resolve(process.env.RONANA_MCP_UPLOAD_ROOT) : null;
10
15
  async function resolveInsideRoot(inputPath) {
11
- const root = await realpath(UPLOAD_ROOT);
12
- const candidate = await realpath(isAbsolute(inputPath) ? inputPath : resolve(root, inputPath));
16
+ if (!UPLOAD_ROOT)
17
+ throw new Error("Local uploads are off. Set RONANA_MCP_UPLOAD_ROOT to the folder that holds your media.");
18
+ const root = await realpath(UPLOAD_ROOT).catch(() => {
19
+ throw new Error(`RONANA_MCP_UPLOAD_ROOT (${UPLOAD_ROOT}) does not exist.`);
20
+ });
21
+ // A root that holds the home folder (the home folder itself, C:\Users, /home
22
+ // or a drive root) would expose every file in it.
23
+ const home = await realpath(homedir()).catch(() => homedir());
24
+ const homeFromRoot = relative(root, home);
25
+ if (root === parse(root).root || homeFromRoot === "" || !(homeFromRoot.startsWith("..") || isAbsolute(homeFromRoot))) {
26
+ throw new Error("RONANA_MCP_UPLOAD_ROOT must be a media folder, not a home folder, a folder that contains one, or a drive root.");
27
+ }
28
+ const candidate = await realpath(isAbsolute(inputPath) ? inputPath : resolve(root, inputPath)).catch(() => {
29
+ throw new Error(`No file at ${inputPath} in RONANA_MCP_UPLOAD_ROOT.`);
30
+ });
13
31
  const rel = relative(root, candidate);
14
32
  if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
15
33
  throw new Error("The path is outside RONANA_MCP_UPLOAD_ROOT.");
@@ -18,8 +36,18 @@ async function resolveInsideRoot(inputPath) {
18
36
  async function serve() {
19
37
  const remote = await remoteClient();
20
38
  const { tools } = await remote.listTools();
39
+ // Present as Ronana itself: the remote server's own title, logo and website,
40
+ // so an app lists this bridge with the Ronana mark rather than a bare name.
41
+ const remoteInfo = remote.getServerVersion();
42
+ const icons = remoteInfo?.icons;
21
43
  serveStdio(() => {
22
- const server = new McpServer({ name: "Ronana local bridge", version: "1.0.0" });
44
+ const server = new McpServer({
45
+ name: "ronana",
46
+ title: remoteInfo?.title ?? "Ronana",
47
+ version: "1.0.0",
48
+ ...(remoteInfo?.websiteUrl ? { websiteUrl: remoteInfo.websiteUrl } : {}),
49
+ ...(icons ? { icons } : {}),
50
+ });
23
51
  for (const tool of tools) {
24
52
  server.registerTool(tool.name, {
25
53
  title: tool.title,
@@ -27,15 +55,19 @@ async function serve() {
27
55
  inputSchema: fromJsonSchema(tool.inputSchema),
28
56
  outputSchema: tool.outputSchema ? fromJsonSchema(tool.outputSchema) : undefined,
29
57
  annotations: tool.annotations,
58
+ ...(tool.icons ? { icons: tool.icons } : {}),
30
59
  }, async (args) => remote.callTool({
31
60
  name: tool.name,
32
61
  arguments: args,
33
62
  }));
34
63
  }
35
64
  server.registerTool("upload_local_reference", {
36
- description: `Upload a local file below ${UPLOAD_ROOT} to the account library.`,
65
+ description: UPLOAD_ROOT
66
+ ? `Upload a local file below ${UPLOAD_ROOT} to the account library.`
67
+ : "Upload a local file to the account library. Off until RONANA_MCP_UPLOAD_ROOT names a media folder.",
37
68
  inputSchema: z.object({ path: z.string() }),
38
69
  annotations: { readOnlyHint: false, destructiveHint: false },
70
+ ...(icons ? { icons } : {}),
39
71
  }, async ({ path }) => {
40
72
  try {
41
73
  const safePath = await resolveInsideRoot(path);
package/package.json CHANGED
@@ -1,14 +1,36 @@
1
1
  {
2
2
  "name": "@ronana/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Ronana command line and local MCP bridge.",
5
- "license": "UNLICENSED",
5
+ "license": "MIT",
6
6
  "homepage": "https://ronana.ai",
7
- "keywords": ["ronana", "mcp", "cli", "image generation", "video generation"],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ronana-ai/cli.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ronana-ai/cli/issues"
13
+ },
14
+ "keywords": [
15
+ "ronana",
16
+ "mcp",
17
+ "cli",
18
+ "image generation",
19
+ "video generation"
20
+ ],
8
21
  "type": "module",
9
- "bin": { "ronana": "dist/cli.js", "ronana-mcp": "dist/server.js" },
10
- "files": ["dist", "README.md"],
11
- "publishConfig": { "access": "public" },
22
+ "bin": {
23
+ "ronana": "dist/cli.js",
24
+ "ronana-mcp": "dist/server.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "provenance": true
33
+ },
12
34
  "scripts": {
13
35
  "build": "tsc -p tsconfig.json",
14
36
  "prepublishOnly": "npm run build",
@@ -21,8 +43,11 @@
21
43
  "zod4": "npm:zod@4.4.3"
22
44
  },
23
45
  "devDependencies": {
46
+ "@types/node": "^22.10.5",
24
47
  "typescript": "^5",
25
48
  "tsx": "^4"
26
49
  },
27
- "engines": { "node": ">=20" }
50
+ "engines": {
51
+ "node": ">=20"
52
+ }
28
53
  }
package/dist/client.d.ts DELETED
@@ -1,61 +0,0 @@
1
- import type { ApiErrorBody, ApiPublicModel, GenerationState } from "./types.js";
2
- export declare class RonanaApiError extends Error {
3
- readonly code: string;
4
- readonly status: number;
5
- readonly body: ApiErrorBody["error"] | null;
6
- constructor(code: string, message: string, status: number, body: ApiErrorBody["error"] | null);
7
- /** A sentence a person or an agent can act on. The `action` field is the
8
- * server's own decision about which door to open, so this never guesses. */
9
- get guidance(): string;
10
- }
11
- export interface ClientOptions {
12
- apiKey: string;
13
- baseUrl: string;
14
- }
15
- /** Read the configuration from the environment. Throws with a clear message
16
- * when the key is missing, because that is the one mistake every new user
17
- * makes. */
18
- export declare function clientOptionsFromEnv(env?: NodeJS.ProcessEnv): ClientOptions;
19
- export declare class RonanaClient {
20
- private readonly options;
21
- constructor(options: ClientOptions);
22
- private request;
23
- listModels(): Promise<{
24
- models: ApiPublicModel[];
25
- tier: string;
26
- }>;
27
- account(): Promise<unknown>;
28
- quote(input: Record<string, unknown>): Promise<{
29
- available: boolean;
30
- credits: number;
31
- }>;
32
- submitVideo(body: Record<string, unknown>): Promise<{
33
- id: string;
34
- status: string;
35
- }>;
36
- submitImage(body: Record<string, unknown>): Promise<{
37
- id: string;
38
- status: string;
39
- }>;
40
- generation(id: string): Promise<GenerationState>;
41
- /** Upload a file inline. Use this up to 4 MB; larger files need the presigned
42
- * flow below, because a serverless function body cannot exceed 4.5 MB. */
43
- uploadInline(bytes: Uint8Array, filename: string): Promise<{
44
- ref: {
45
- id: string;
46
- };
47
- reused?: boolean;
48
- }>;
49
- /** Upload a file larger than 4 MB: sign, PUT straight to storage, finalize.
50
- *
51
- * CAUTION: The PUT must send exactly the content type and the content length
52
- * that were signed. Any other value fails the storage signature check. */
53
- uploadPresigned(bytes: Uint8Array, filename: string, contentType: string): Promise<{
54
- ref: {
55
- id: string;
56
- };
57
- reused?: boolean;
58
- }>;
59
- }
60
- /** The most bytes an inline upload may carry. Above this, use the signed flow. */
61
- export declare const INLINE_UPLOAD_MAX_BYTES: number;
package/dist/client.js DELETED
@@ -1,128 +0,0 @@
1
- // The HTTP client for the RonanaLive public API.
2
- //
3
- // THE LOAD-BEARING RULE OF THIS PACKAGE: it is a thin HTTP client of
4
- // `/api/v1/*`. It imports no database, no credit module, and no ORM. If it ever
5
- // imported the application's database layer, somebody would eventually run this
6
- // package against production.
7
- //
8
- // Everything it needs is two environment variables:
9
- // RONANA_API_KEY the key, `rk_…`, minted at /developers/keys
10
- // RONANA_BASE_URL the origin, default https://ronana.ai
11
- export class RonanaApiError extends Error {
12
- code;
13
- status;
14
- body;
15
- constructor(code, message, status, body) {
16
- super(message);
17
- this.code = code;
18
- this.status = status;
19
- this.body = body;
20
- this.name = "RonanaApiError";
21
- }
22
- /** A sentence a person or an agent can act on. The `action` field is the
23
- * server's own decision about which door to open, so this never guesses. */
24
- get guidance() {
25
- const billing = this.body?.billing;
26
- if (!billing)
27
- return this.message;
28
- switch (billing.action) {
29
- case "buy_credits": return `${this.message} Buy more credits at ${billing.url}.`;
30
- case "upgrade_plan": return `${this.message} Move to a larger plan at ${billing.url}.`;
31
- case "either": return `${this.message} Buy credits or move to a larger plan at ${billing.url}.`;
32
- case "verify_email": return `${this.message} Verify the account email at ${billing.url}.`;
33
- case "wait": return `${this.message} Wait, then try again.`;
34
- case "contact_support": return `${this.message} Contact support.`;
35
- default: return this.message;
36
- }
37
- }
38
- }
39
- /** Read the configuration from the environment. Throws with a clear message
40
- * when the key is missing, because that is the one mistake every new user
41
- * makes. */
42
- export function clientOptionsFromEnv(env = process.env) {
43
- const apiKey = env.RONANA_API_KEY?.trim();
44
- if (!apiKey) {
45
- throw new Error("RONANA_API_KEY is not set. Create a key at https://ronana.ai/developers/keys, " +
46
- "then put it in your MCP client configuration as RONANA_API_KEY.");
47
- }
48
- const baseUrl = (env.RONANA_BASE_URL?.trim() || "https://ronana.ai").replace(/\/+$/, "");
49
- return { apiKey, baseUrl };
50
- }
51
- export class RonanaClient {
52
- options;
53
- constructor(options) {
54
- this.options = options;
55
- }
56
- async request(method, path, body) {
57
- const res = await fetch(`${this.options.baseUrl}${path}`, {
58
- method,
59
- headers: {
60
- authorization: `Bearer ${this.options.apiKey}`,
61
- ...(body === undefined ? {} : { "content-type": "application/json" }),
62
- },
63
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
64
- });
65
- const text = await res.text();
66
- const parsed = text ? JSON.parse(text) : null;
67
- if (!res.ok) {
68
- const envelope = parsed?.error ?? null;
69
- throw new RonanaApiError(envelope?.code ?? "UNKNOWN", envelope?.message ?? `Request failed with status ${res.status}.`, res.status, envelope);
70
- }
71
- return parsed;
72
- }
73
- listModels() {
74
- return this.request("GET", "/api/v1/models");
75
- }
76
- account() {
77
- return this.request("GET", "/api/v1/me");
78
- }
79
- quote(input) {
80
- return this.request("POST", "/api/v1/pricing/quote", input);
81
- }
82
- submitVideo(body) {
83
- return this.request("POST", "/api/v1/generations", body);
84
- }
85
- submitImage(body) {
86
- return this.request("POST", "/api/v1/images", body);
87
- }
88
- generation(id) {
89
- return this.request("GET", `/api/v1/generations/${encodeURIComponent(id)}`);
90
- }
91
- /** Upload a file inline. Use this up to 4 MB; larger files need the presigned
92
- * flow below, because a serverless function body cannot exceed 4.5 MB. */
93
- async uploadInline(bytes, filename) {
94
- const res = await fetch(`${this.options.baseUrl}/api/v1/refs`, {
95
- method: "POST",
96
- headers: {
97
- authorization: `Bearer ${this.options.apiKey}`,
98
- "content-type": "application/octet-stream",
99
- "x-filename": encodeURIComponent(filename),
100
- },
101
- body: bytes,
102
- });
103
- const text = await res.text();
104
- const parsed = text ? JSON.parse(text) : null;
105
- if (!res.ok) {
106
- const envelope = parsed?.error ?? null;
107
- throw new RonanaApiError(envelope?.code ?? "UNKNOWN", envelope?.message ?? "Upload failed.", res.status, envelope);
108
- }
109
- return parsed;
110
- }
111
- /** Upload a file larger than 4 MB: sign, PUT straight to storage, finalize.
112
- *
113
- * CAUTION: The PUT must send exactly the content type and the content length
114
- * that were signed. Any other value fails the storage signature check. */
115
- async uploadPresigned(bytes, filename, contentType) {
116
- const signed = await this.request("POST", "/api/v1/refs/upload-url", { filename, contentType, size: bytes.byteLength });
117
- const put = await fetch(signed.uploadUrl, {
118
- method: "PUT",
119
- headers: { "content-type": signed.contentType, "content-length": String(bytes.byteLength) },
120
- body: bytes,
121
- });
122
- if (!put.ok)
123
- throw new Error(`The upload to storage failed with status ${put.status}.`);
124
- return this.request("POST", "/api/v1/refs", { tmpKey: signed.tmpKey, filename });
125
- }
126
- }
127
- /** The most bytes an inline upload may carry. Above this, use the signed flow. */
128
- export const INLINE_UPLOAD_MAX_BYTES = 4 * 1024 * 1024;
package/dist/schemas.d.ts DELETED
@@ -1,101 +0,0 @@
1
- import { z } from "zod";
2
- /** MCP request schemas mirror the public HTTP schemas.
3
- * Contract tests compare their fields and boundaries with those schemas. */
4
- export declare const estimateCostInputSchema: {
5
- readonly modelId: z.ZodString;
6
- readonly resolution: z.ZodOptional<z.ZodString>;
7
- readonly aspectRatio: z.ZodOptional<z.ZodString>;
8
- readonly durationSeconds: z.ZodOptional<z.ZodNumber>;
9
- readonly quality: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
10
- readonly hasVideoRefs: z.ZodOptional<z.ZodBoolean>;
11
- readonly generateAudio: z.ZodOptional<z.ZodBoolean>;
12
- readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
13
- };
14
- export declare const generateInputSchema: {
15
- readonly model: z.ZodString;
16
- readonly prompt: z.ZodOptional<z.ZodString>;
17
- readonly negativePrompt: z.ZodOptional<z.ZodString>;
18
- readonly aspectRatio: z.ZodOptional<z.ZodString>;
19
- readonly resolution: z.ZodOptional<z.ZodString>;
20
- readonly attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
21
- source: z.ZodEnum<["ref", "video_generation", "image_generation", "media_asset"]>;
22
- id: z.ZodString;
23
- }, "strip", z.ZodTypeAny, {
24
- id: string;
25
- source: "ref" | "video_generation" | "image_generation" | "media_asset";
26
- }, {
27
- id: string;
28
- source: "ref" | "video_generation" | "image_generation" | "media_asset";
29
- }>, "many">>;
30
- readonly seed: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
31
- readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
32
- readonly idempotencyKey: z.ZodOptional<z.ZodString>;
33
- readonly wait: z.ZodOptional<z.ZodBoolean>;
34
- };
35
- export declare const videoGenerationInputSchema: {
36
- readonly slotBindings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
37
- readonly keyframes: z.ZodOptional<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodNumber>, Record<string, number>, Record<string, number>>>;
38
- readonly duration: z.ZodOptional<z.ZodNumber>;
39
- readonly generateAudio: z.ZodOptional<z.ZodBoolean>;
40
- readonly movementAmplitude: z.ZodOptional<z.ZodEnum<["auto", "small", "medium", "large"]>>;
41
- readonly shotType: z.ZodOptional<z.ZodEnum<["intelligence", "customize"]>>;
42
- readonly multiPrompt: z.ZodOptional<z.ZodArray<z.ZodObject<{
43
- prompt: z.ZodString;
44
- duration: z.ZodNumber;
45
- shotSize: z.ZodOptional<z.ZodEnum<["auto", "wide", "medium", "closeup", "macro", "pov"]>>;
46
- cameraMove: z.ZodOptional<z.ZodEnum<["auto", "static", "pan", "tracking", "dollyIn", "dollyOut", "zoom"]>>;
47
- }, "strip", z.ZodTypeAny, {
48
- prompt: string;
49
- duration: number;
50
- shotSize?: "medium" | "auto" | "wide" | "closeup" | "macro" | "pov" | undefined;
51
- cameraMove?: "auto" | "static" | "pan" | "tracking" | "dollyIn" | "dollyOut" | "zoom" | undefined;
52
- }, {
53
- prompt: string;
54
- duration: number;
55
- shotSize?: "medium" | "auto" | "wide" | "closeup" | "macro" | "pov" | undefined;
56
- cameraMove?: "auto" | "static" | "pan" | "tracking" | "dollyIn" | "dollyOut" | "zoom" | undefined;
57
- }>, "many">>;
58
- readonly model: z.ZodString;
59
- readonly prompt: z.ZodOptional<z.ZodString>;
60
- readonly negativePrompt: z.ZodOptional<z.ZodString>;
61
- readonly aspectRatio: z.ZodOptional<z.ZodString>;
62
- readonly resolution: z.ZodOptional<z.ZodString>;
63
- readonly attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
64
- source: z.ZodEnum<["ref", "video_generation", "image_generation", "media_asset"]>;
65
- id: z.ZodString;
66
- }, "strip", z.ZodTypeAny, {
67
- id: string;
68
- source: "ref" | "video_generation" | "image_generation" | "media_asset";
69
- }, {
70
- id: string;
71
- source: "ref" | "video_generation" | "image_generation" | "media_asset";
72
- }>, "many">>;
73
- readonly seed: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
74
- readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
75
- readonly idempotencyKey: z.ZodOptional<z.ZodString>;
76
- readonly wait: z.ZodOptional<z.ZodBoolean>;
77
- };
78
- export declare const imageGenerationInputSchema: {
79
- readonly quality: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
80
- readonly webSearch: z.ZodOptional<z.ZodBoolean>;
81
- readonly maskDataUrl: z.ZodOptional<z.ZodString>;
82
- readonly model: z.ZodString;
83
- readonly prompt: z.ZodOptional<z.ZodString>;
84
- readonly negativePrompt: z.ZodOptional<z.ZodString>;
85
- readonly aspectRatio: z.ZodOptional<z.ZodString>;
86
- readonly resolution: z.ZodOptional<z.ZodString>;
87
- readonly attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
88
- source: z.ZodEnum<["ref", "video_generation", "image_generation", "media_asset"]>;
89
- id: z.ZodString;
90
- }, "strip", z.ZodTypeAny, {
91
- id: string;
92
- source: "ref" | "video_generation" | "image_generation" | "media_asset";
93
- }, {
94
- id: string;
95
- source: "ref" | "video_generation" | "image_generation" | "media_asset";
96
- }>, "many">>;
97
- readonly seed: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
98
- readonly params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodNumber, z.ZodString, z.ZodBoolean]>>>;
99
- readonly idempotencyKey: z.ZodOptional<z.ZodString>;
100
- readonly wait: z.ZodOptional<z.ZodBoolean>;
101
- };
package/dist/schemas.js DELETED
@@ -1,55 +0,0 @@
1
- import { z } from "zod";
2
- const MAX_ATTACHMENTS_PER_SUBMIT = 50;
3
- /** MCP request schemas mirror the public HTTP schemas.
4
- * Contract tests compare their fields and boundaries with those schemas. */
5
- export const estimateCostInputSchema = {
6
- modelId: z.string().min(1).max(300).describe("A model id from list_models."),
7
- resolution: z.string().max(40).optional(),
8
- aspectRatio: z.string().max(20).optional(),
9
- durationSeconds: z.number().min(-1).max(600).optional(),
10
- quality: z.enum(["low", "medium", "high"]).optional(),
11
- hasVideoRefs: z.boolean().optional(),
12
- generateAudio: z.boolean().optional(),
13
- params: z.record(z.string().max(64), z.union([
14
- z.number(), z.string().max(64), z.boolean(),
15
- ])).optional(),
16
- };
17
- const attachment = z.object({
18
- source: z.enum(["ref", "video_generation", "image_generation", "media_asset"]),
19
- id: z.string().min(1).max(128),
20
- });
21
- export const generateInputSchema = {
22
- model: z.string().min(1).max(120).describe("A model id from list_models."),
23
- prompt: z.string().min(1).max(25_000).optional(),
24
- negativePrompt: z.string().max(1000).optional(),
25
- aspectRatio: z.string().min(1).max(16).optional(),
26
- resolution: z.string().min(1).max(16).optional(),
27
- attachments: z.array(attachment).max(MAX_ATTACHMENTS_PER_SUBMIT).optional(),
28
- seed: z.number().int().min(0).max(4294967295).nullable().optional(),
29
- params: z.record(z.string().max(64), z.union([
30
- z.number(), z.string().max(64), z.boolean(),
31
- ])).optional(),
32
- idempotencyKey: z.string().max(64).optional(),
33
- wait: z.boolean().optional(),
34
- };
35
- export const videoGenerationInputSchema = {
36
- ...generateInputSchema,
37
- slotBindings: z.record(z.string().max(32), z.string().max(128)).optional(),
38
- keyframes: z.record(z.string().max(128), z.number().min(0).max(3600)).refine((value) => Object.keys(value).length <= 20, "Maximum 20 keyframes.").optional(),
39
- duration: z.number().int().min(-1).max(60).optional(),
40
- generateAudio: z.boolean().optional(),
41
- movementAmplitude: z.enum(["auto", "small", "medium", "large"]).optional(),
42
- shotType: z.enum(["intelligence", "customize"]).optional(),
43
- multiPrompt: z.array(z.object({
44
- prompt: z.string().min(1).max(2500),
45
- duration: z.number().int().min(1).max(15),
46
- shotSize: z.enum(["auto", "wide", "medium", "closeup", "macro", "pov"]).optional(),
47
- cameraMove: z.enum(["auto", "static", "pan", "tracking", "dollyIn", "dollyOut", "zoom"]).optional(),
48
- })).max(6).optional(),
49
- };
50
- export const imageGenerationInputSchema = {
51
- ...generateInputSchema,
52
- quality: z.enum(["low", "medium", "high"]).optional(),
53
- webSearch: z.boolean().optional(),
54
- maskDataUrl: z.string().max(7_000_000).optional(),
55
- };
package/dist/types.d.ts DELETED
@@ -1,173 +0,0 @@
1
- export type RefKind = "image" | "video" | "audio";
2
- export interface ApiSlotSpec {
3
- id: string;
4
- kind: RefKind;
5
- required: boolean;
6
- }
7
- export interface ApiRefSpec {
8
- kinds: readonly RefKind[];
9
- max: {
10
- images?: number;
11
- videos?: number;
12
- audio?: number;
13
- total?: number;
14
- videosTotalSec?: number;
15
- audioTotalSec?: number;
16
- videoMaxBytes?: number;
17
- audioMaxBytes?: number;
18
- videoMinSec?: number;
19
- videoMinWidthPx?: number;
20
- videoMaxWidthPx?: number;
21
- imageMinPx?: number;
22
- imageMaxPx?: number;
23
- imageMaxBytes?: number;
24
- imageMaxPixels?: number;
25
- imageMinAspect?: number;
26
- imageMaxAspect?: number;
27
- videoMaxSec?: number;
28
- };
29
- inline: boolean;
30
- slots: readonly ApiSlotSpec[];
31
- attachmentGroups: {
32
- tokenPrefix?: string;
33
- maxGroups: number;
34
- kinds: Array<"image" | "video" | "audio">;
35
- minPerGroup: number;
36
- maxPerGroup: number;
37
- primaryFirst?: boolean;
38
- } | null;
39
- }
40
- export type ApiParamSpec = {
41
- id: string;
42
- kind: "number";
43
- min: number;
44
- max: number;
45
- step: number;
46
- } | {
47
- id: string;
48
- kind: "select";
49
- options: readonly string[];
50
- } | {
51
- id: string;
52
- kind: "toggle";
53
- };
54
- export interface ApiInputConditionedRule {
55
- mode: "t2v" | "i2v" | "r2v";
56
- aspect?: string[];
57
- resolution?: string[];
58
- duration?: number[];
59
- when: readonly {
60
- imageRefs: "0" | "1" | "2+" | "any";
61
- audioRefs: "0" | "1+" | "any";
62
- }[];
63
- }
64
- export interface ApiModelSpec {
65
- id: string;
66
- kind: "video" | "image";
67
- name: string;
68
- prompt: {
69
- required: boolean;
70
- maxChars?: number;
71
- };
72
- aspect: readonly string[];
73
- aspectWithImage: readonly string[];
74
- resolution: readonly string[];
75
- duration: readonly number[];
76
- quality: readonly ("low" | "medium" | "high")[];
77
- dimensionsFromReference: boolean;
78
- refs: ApiRefSpec;
79
- params: readonly ApiParamSpec[];
80
- variants: readonly ApiInputConditionedRule[];
81
- multiShot: boolean;
82
- seed: boolean;
83
- negativePrompt: boolean;
84
- generateAudio: boolean;
85
- movementAmplitude: boolean;
86
- webSearch: boolean;
87
- mask: {
88
- format: "png-data-url";
89
- dimensions: "match-first-image";
90
- polarity: "alpha-punch" | "white-on-black";
91
- } | null;
92
- }
93
- /**
94
- * What the server actually SENDS for a model.
95
- *
96
- * `ApiModelSpec` above is the total shape. The wire is smaller, twice over: it
97
- * drops every field carrying no information (`false`, `null`, an empty list),
98
- * and it drops the fields only the studio uses (`category`, `preset`, and the
99
- * `group`/`label` split, which arrives as one `name`). A capability the model
100
- * does not have is ABSENT rather than sent as a "no", so read presence as
101
- * support:
102
- *
103
- * if (model.seed) { ... } // correct, whether absent or true
104
- * for (const s of model.refs?.slots ?? []) { ... }
105
- *
106
- * Derived from `ApiModelSpec`, never hand-written, so a new field on the server
107
- * reaches this type with no second edit here.
108
- */
109
- type Loosen<T> = {
110
- [K in keyof T]?: T[K];
111
- };
112
- export type InternalModelField = never;
113
- export type ApiPublicModel = Loosen<Omit<ApiModelSpec, "refs" | "prompt" | InternalModelField>> & {
114
- /** The model's display name, in one piece. */
115
- name?: string;
116
- refs?: Loosen<ApiRefSpec>;
117
- prompt?: Loosen<ApiModelSpec["prompt"]>;
118
- };
119
- /** The error envelope every v1 endpoint returns on a failure. */
120
- export interface ApiErrorBody {
121
- error: {
122
- code: "AUTH_REQUIRED" | "AUTH_INVALID" | "AUTH_EXPIRED" | "SCOPE_INSUFFICIENT" | "ACCOUNT_SUSPENDED" | "ACCOUNT_PENDING_DELETION" | "EMAIL_UNVERIFIED" | "PLAN_UPGRADE_REQUIRED" | "CREDITS_INSUFFICIENT" | "SPEND_LOCKED" | "DAILY_CREDIT_CAP" | "KEY_DAILY_CREDIT_CAP" | "DAILY_GENERATION_CAP" | "STORAGE_FULL" | "RATE_LIMITED" | "CONCURRENCY_LIMIT" | "INVALID_REQUEST" | "MODEL_UNKNOWN" | "MODEL_DISABLED" | "NOT_FOUND" | "PROMPT_TOO_LONG" | "UNSUPPORTED_OPTION" | "LAUNCH_CLOSED" | "SERVICE_UNAVAILABLE" | "INTERNAL";
123
- message: string;
124
- requestId: string;
125
- retryAfterMs?: number;
126
- resetAt?: string;
127
- field?: string;
128
- billing?: {
129
- balance?: number;
130
- needed?: number;
131
- currentTier?: string;
132
- requiredTier?: string;
133
- nextTier?: {
134
- id: string;
135
- creditsMonthly: number;
136
- } | null;
137
- purchasesEnabled?: boolean;
138
- atMaxTier?: boolean;
139
- action: "buy_credits" | "upgrade_plan" | "either" | "verify_email" | "wait" | "contact_support";
140
- url: string;
141
- };
142
- };
143
- }
144
- export interface GenerationState {
145
- id: string;
146
- kind: "video" | "image";
147
- status: "queued" | "processing" | "completed" | "failed" | "canceled";
148
- model: string;
149
- prompt: string;
150
- aspectRatio: string;
151
- resolution: string;
152
- durationS?: number;
153
- quality?: "low" | "medium" | "high" | null;
154
- source: "api";
155
- output: {
156
- url: string | null;
157
- expiresAt: string | null;
158
- bytes: number | null;
159
- width: number | null;
160
- height: number | null;
161
- durationS?: number | null;
162
- retentionExpiresAt: string | null;
163
- };
164
- failure: {
165
- reason: string;
166
- title: string;
167
- detail: string;
168
- detailKey?: string;
169
- } | null;
170
- createdAt: string;
171
- completedAt: string | null;
172
- }
173
- export {};
package/dist/types.js DELETED
@@ -1,11 +0,0 @@
1
- // The public shapes this package reads, declared with NO runtime import.
2
- //
3
- // They are a copy of the types the server publishes at `GET /api/v1/models`
4
- // (lib/api/modelSchema.ts). The copy is deliberate: this package must run from
5
- // npm with nothing but `RONANA_API_KEY` and `RONANA_BASE_URL`, so it can hold no
6
- // path into the application repository.
7
- //
8
- // tests/mcpPublicTypes.test.ts asserts the two definitions stay compatible, in
9
- // both directions. If somebody adds a field to `ApiModelSpec` and forgets this
10
- // file, that test fails.
11
- export {};