@ronana/mcp 0.1.1 → 0.3.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/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
 
@@ -38,6 +37,8 @@ ronana generate cost google/nano-banana-2-lite/edit --prompt "make it dusk" --
38
37
  ronana generate create google/nano-banana-2-lite/edit --prompt "make it dusk" --image ./photo.png --out ./renders
39
38
  ronana generate get <id> | wait <id> [--out dir]
40
39
  ronana generate list
40
+ ronana upscale <id | file | url> --resolution 4k --out ./renders
41
+ ronana folder list | add <folder> <id…>
41
42
  ```
42
43
 
43
44
  Every field from `model get` is a flag (`--aspect_ratio 16:9`, `--seed 7`).
@@ -81,9 +82,27 @@ add it to the app's MCP settings:
81
82
  }
82
83
  ```
83
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
+
84
102
  The local server adds one tool, `upload_local_reference`, which uploads a file
85
103
  from inside `RONANA_MCP_UPLOAD_ROOT`. Set it to the folder that holds your
86
- 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
87
106
  straight to storage, and Ronana checks each file before it adds it to your
88
107
  library.
89
108
 
@@ -95,18 +114,22 @@ library.
95
114
  | `get_model` | Get one model's contract: its input schema. |
96
115
  | `estimate_cost` | Estimate the credit cost of a generation. Takes the same `model` and `input` as `generate`. |
97
116
  | `generate` | Start an image or video generation. |
98
- | `get_generation` | Get one generation and its download link. |
117
+ | `get_generation` | Get one generation and its download link. Pass `wait_seconds` (up to 45) to wait for the result in one call. |
118
+ | `upscale` | Upscale a video to a higher resolution. |
99
119
  | `list_generations` | List recent generations. |
100
120
  | `list_references` | List library references. |
101
121
  | `upload_reference` | Add a file to the library from a URL (up to 25 MB) or from base64 (under 3 MB). |
102
122
  | `create_upload` | Get a private upload link for a larger local file (up to 200 MB). |
103
123
  | `finish_upload` | Add a file uploaded with `create_upload` to the library. |
124
+ | `list_folders` | List the folders in the library. |
125
+ | `add_to_folder` | File library items into a folder. |
104
126
  | `get_account` | Get the plan and credit balance. |
105
127
 
106
128
  A generate call takes a `model` and an `input`, exactly as `get_model` shows it
107
129
  in `input_schema`. A media field takes a public https URL or a Ronana id
108
- (`ref_…`, `img_…`, `gen_…`). Generation tools return an id immediately. Poll
109
- `get_generation` until the status is `completed` or `failed`.
130
+ (`ref_…`, `img_…`, `gen_…`). Generation tools return an id immediately. Call
131
+ `get_generation` with `wait_seconds` until the status is `completed` or
132
+ `failed`. Apps that support MCP Apps show the image or video right in the chat.
110
133
 
111
134
  ```json
112
135
  { "model": "google/nano-banana-2/edit", "input": { "prompt": "a lantern shop at dusk", "images": ["https://example.com/ref.png"] } }
package/dist/cli.js CHANGED
@@ -12,15 +12,17 @@
12
12
  // ronana generate cost <model> [same flags as create]
13
13
  // ronana generate get | wait <id> [--out dir]
14
14
  // ronana generate list [--limit n]
15
+ // ronana upscale <id | file | https-url> [--resolution r] [--model m] [--wait] [--out dir]
16
+ // ronana folder list | add <folder> <id…>
15
17
  import { randomUUID } from "node:crypto";
16
- import { createWriteStream } from "node:fs";
18
+ import { createWriteStream, existsSync, statSync } from "node:fs";
17
19
  import { mkdir } from "node:fs/promises";
18
20
  import { basename, extname, join, resolve } from "node:path";
19
21
  import { Readable } from "node:stream";
20
22
  import { pipeline } from "node:stream/promises";
21
23
  import { login, logout, readCredentials } from "./oauth.js";
22
24
  import { callTool, remoteClient, ToolError, uploadFile } from "./remote.js";
23
- import { buildInput, flag, has, parseArgs, replaceLocalMedia, typeOf } from "./input.js";
25
+ import { buildInput, flag, has, isRemoteValue, parseArgs, replaceLocalMedia, typeOf } from "./input.js";
24
26
  const HELP = `Usage: ronana <command>
25
27
 
26
28
  auth login Sign in with your browser (--base-url, --no-browser)
@@ -35,6 +37,9 @@ const HELP = `Usage: ronana <command>
35
37
  generate get <id> Status and download links
36
38
  generate wait <id> Wait for a generation to finish
37
39
  generate list Recent generations (--limit n)
40
+ upscale <id|file|url> Upscale a video (costs credits; --resolution, --model)
41
+ folder list Your library folders
42
+ folder add <folder> <id…> File items into a folder (made if missing)
38
43
 
39
44
  Generate flags:
40
45
  --<field> <value> Any field from \`model get\`. Repeat for lists.
@@ -65,6 +70,8 @@ function describeField(name, schema, required) {
65
70
  parts.push(`default ${JSON.stringify(schema.default)}`);
66
71
  return ` --${name.padEnd(22)} ${parts.join(" ")}${required ? " (required)" : ""}`;
67
72
  }
73
+ /** Model ids are lowercase paths; forgive the casing and a trailing slash. */
74
+ const modelId = (raw) => raw.trim().replace(/\/+$/, "").toLowerCase();
68
75
  const TERMINAL = new Set(["completed", "failed", "canceled"]);
69
76
  function seconds(args, name, fallback) {
70
77
  const raw = flag(args, name);
@@ -80,8 +87,12 @@ async function waitFor(remote, id, args) {
80
87
  let last = "";
81
88
  for (;;) {
82
89
  let gen;
90
+ const asked = Date.now();
83
91
  try {
84
- gen = await callTool(remote, "get_generation", { id });
92
+ // The server holds the call until the job settles (up to 25s), so this
93
+ // loop makes one request per 25s rather than one every few seconds.
94
+ const wait_seconds = Math.max(0, Math.min(25, Math.floor((deadline - asked) / 1000)));
95
+ gen = await callTool(remote, "get_generation", { id, wait_seconds });
85
96
  }
86
97
  catch (error) {
87
98
  // A server answer (not found, no access) is final; anything else is the
@@ -99,7 +110,11 @@ ${id} is still running. Resume with \`ronana generate wait ${id}\`.`);
99
110
  return gen;
100
111
  if (Date.now() > deadline)
101
112
  throw new Error(`Still ${gen.status} after ${timeoutMs / 1000}s. Check later with \`ronana generate wait ${id}\`.`);
102
- await new Promise((r) => setTimeout(r, intervalMs));
113
+ // A server that answered at once (an older one, or a short wait) still gets
114
+ // the local pause, so the loop never spins.
115
+ const left = intervalMs - (Date.now() - asked);
116
+ if (left > 0)
117
+ await new Promise((r) => setTimeout(r, left));
103
118
  }
104
119
  }
105
120
  const EXT_BY_TYPE = {
@@ -107,6 +122,8 @@ const EXT_BY_TYPE = {
107
122
  "video/mp4": ".mp4", "video/webm": ".webm", "video/quicktime": ".mov",
108
123
  };
109
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.`);
110
127
  await mkdir(dir, { recursive: true });
111
128
  const saved = [];
112
129
  for (const output of gen.outputs) {
@@ -160,6 +177,8 @@ async function generateCommand(sub, args) {
160
177
  const [target] = args.positional;
161
178
  if (sub === "list") {
162
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.");
163
182
  const rows = await withRemote((r) => callTool(r, "list_generations", { limit }));
164
183
  return emit(rows, () => rows.forEach((g) => print(`${g.id.padEnd(28)} ${g.status.padEnd(11)} ${g.created_at}`)));
165
184
  }
@@ -174,7 +193,7 @@ async function generateCommand(sub, args) {
174
193
  if (sub !== "create" && sub !== "cost")
175
194
  throw new Error("Use generate create | cost | get | wait | list.");
176
195
  await withRemote(async (remote) => {
177
- const contract = await callTool(remote, "get_model", { id: target });
196
+ const contract = await callTool(remote, "get_model", { id: modelId(target) });
178
197
  const input = await buildInput(contract, args);
179
198
  await replaceLocalMedia(contract, input, async (path) => {
180
199
  note(`Uploading ${basename(path)}…`);
@@ -199,17 +218,23 @@ async function generateCommand(sub, args) {
199
218
  async function modelCommand(sub, args) {
200
219
  if (sub === "list" || sub === undefined) {
201
220
  const kind = flag(args, "kind");
221
+ if (kind !== undefined && kind !== "image" && kind !== "video")
222
+ throw new Error("--kind must be image or video.");
202
223
  const filter = args.positional[0]?.toLowerCase();
203
224
  const models = (await withRemote((r) => callTool(r, "list_models", kind ? { kind } : {})))
204
225
  .filter((m) => !filter || m.id.toLowerCase().includes(filter) || m.name.toLowerCase().includes(filter));
205
- return emit(models, () => models.forEach((m) => print(`${m.id.padEnd(52)} ${m.kind.padEnd(6)} ${m.name}`)));
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
+ });
206
231
  }
207
232
  if (sub !== "get")
208
233
  throw new Error("Use model list | get.");
209
234
  const id = args.positional[0];
210
235
  if (!id)
211
236
  throw new Error("Usage: ronana model get <model>");
212
- const contract = await withRemote((r) => callTool(r, "get_model", { id }));
237
+ const contract = await withRemote((r) => callTool(r, "get_model", { id: modelId(id) }));
213
238
  emit(contract, () => {
214
239
  print(`${contract.name} (${contract.kind})`);
215
240
  print(contract.id);
@@ -225,10 +250,54 @@ async function modelCommand(sub, args) {
225
250
  print(`Priced by: ${contract.priced_fields.join(", ")}`);
226
251
  });
227
252
  }
253
+ async function upscaleCommand(args) {
254
+ const [source] = args.positional;
255
+ if (!source)
256
+ throw new Error("Usage: ronana upscale <gen_…|ref_… id | file | https-url> [--resolution 4k] [--out dir]");
257
+ await withRemote(async (remote) => {
258
+ // A local file uploads first; an id or a URL goes as-is.
259
+ const resolved = isRemoteValue(source) ? source : (note(`Uploading ${basename(source)}…`), (await uploadFile(remote, resolve(source))).id);
260
+ const queued = await callTool(remote, "upscale", {
261
+ source: resolved,
262
+ ...(flag(args, "model") ? { model: flag(args, "model") } : {}),
263
+ ...(flag(args, "resolution") ? { resolution: flag(args, "resolution") } : {}),
264
+ idempotency_key: flag(args, "idempotency_key") ?? randomUUID(),
265
+ });
266
+ if (!has(args, "wait") && !has(args, "out")) {
267
+ return emit(queued, () => { print(queued.id); note(`Check it with \`ronana generate wait ${queued.id}\`.`); });
268
+ }
269
+ note(`Queued ${queued.id}.`);
270
+ await finish(await waitFor(remote, queued.id, args), args);
271
+ });
272
+ }
273
+ async function folderCommand(sub, args) {
274
+ if (sub === "list" || sub === undefined) {
275
+ const folders = await withRemote((r) => callTool(r, "list_folders"));
276
+ return emit(folders, () => folders.forEach((f) => print(`${f.id.padEnd(28)} ${String(f.count).padStart(5)} ${f.name}`)));
277
+ }
278
+ if (sub !== "add")
279
+ throw new Error("Use folder list | add.");
280
+ const [folder, ...ids] = args.positional;
281
+ if (!folder?.trim() || ids.length === 0)
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.");
285
+ const filed = await withRemote((r) => callTool(r, "add_to_folder", { ids, folder, create: true }));
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;
293
+ }
228
294
  async function main() {
229
- const [command, sub, ...rest] = process.argv.slice(2);
230
- const takesSub = command === "auth" || command === "model" || command === "generate";
231
- const args = parseArgs(takesSub ? rest : [sub, ...rest].filter((v) => v !== undefined));
295
+ const [command, ...rest] = process.argv.slice(2);
296
+ const takesSub = command === "auth" || command === "model" || command === "generate" || command === "folder";
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;
232
301
  JSON_OUT = has(args, "json");
233
302
  if (!command || ["help", "--help", "-h"].includes(command) || has(args, "help"))
234
303
  return void process.stdout.write(HELP);
@@ -262,18 +331,29 @@ async function main() {
262
331
  const source = args.positional[0];
263
332
  if (!source)
264
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.");
265
336
  const ref = await withRemote((r) => /^https:\/\//i.test(source)
266
337
  ? callTool(r, "upload_reference", { url: source, filename: basename(new URL(source).pathname) || "upload" })
267
338
  : uploadFile(r, resolve(source)));
268
339
  return emit(ref, () => print(ref.id));
269
340
  }
270
341
  case "generate": return generateCommand(sub, args);
342
+ case "upscale": return upscaleCommand(args);
343
+ case "folder":
344
+ case "folders": return folderCommand(command === "folders" ? "list" : sub, args);
271
345
  default: throw new Error(`Unknown command "${command}". Run \`ronana --help\`.`);
272
346
  }
273
347
  }
274
348
  main().catch((error) => {
275
349
  if (error instanceof ToolError) {
276
- 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}`;
277
357
  if (JSON_OUT)
278
358
  process.stdout.write(`${JSON.stringify({ ok: false, error: error.error }, null, 2)}\n`);
279
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
@@ -64,6 +64,14 @@ async function tokenRequest(baseUrl, fields) {
64
64
  }
65
65
  export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ronana.ai", openInBrowser = true) {
66
66
  baseUrl = baseUrl.replace(/\/$/, "");
67
+ const parsed = (() => { try {
68
+ return new URL(baseUrl);
69
+ }
70
+ catch {
71
+ return null;
72
+ } })();
73
+ if (!parsed || !/^https?:$/.test(parsed.protocol))
74
+ throw new Error("--base-url must be a web address, like https://ronana.ai.");
67
75
  const verifier = randomBytes(32).toString("base64url");
68
76
  const challenge = createHash("sha256").update(verifier).digest("base64url");
69
77
  const state = randomBytes(24).toString("base64url");
@@ -113,8 +121,10 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
113
121
  response_types: ["code"],
114
122
  scope: SCOPES,
115
123
  }),
124
+ }).catch(() => { throw new Error(`Could not reach ${baseUrl}. Check --base-url and your connection.`); });
125
+ const registered = await registration.json().catch(() => {
126
+ throw new Error(`No Ronana sign-in at ${baseUrl}. Check --base-url.`);
116
127
  });
117
- const registered = await registration.json();
118
128
  if (!registration.ok || typeof registered.client_id !== "string")
119
129
  throw new Error("OAuth client registration failed.");
120
130
  const authorize = new URL(`${baseUrl}/api/auth/oauth2/authorize`);
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.1.1",
3
+ "version": "0.3.0",
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 {};