@canvora/cli 1.0.0 → 1.2.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/README.md CHANGED
@@ -45,6 +45,9 @@ canvora generate \
45
45
  | Command | What it does |
46
46
  |---|---|
47
47
  | `canvora generate` | Create visuals from an `--idea` (short concept, Canvora develops it), `--input` (your full content text), `--url`, or `--file-url` (PDF). `--wait` polls to completion and prints output URLs. |
48
+ | `canvora localize <id> --language es` | Same design, new language: translates an existing generation's visuals, layout untouched |
49
+ | `canvora variations <id> [--count n]` | Fresh takes on an existing generation's visuals |
50
+ | `canvora edit <outputId> --prompt "..."` | Natural-language edit of one visual (previews the change; `--yes` auto-confirms) |
48
51
  | `canvora status <id>` | Check a generation and list its output URLs |
49
52
  | `canvora download <id> --dir out/` | Save all output files locally |
50
53
  | `canvora formats` | All 100+ format IDs with dimensions |
@@ -61,10 +64,13 @@ Every command takes `--json` for machine-readable stdout; progress and errors go
61
64
  --url <url> extract content from a webpage
62
65
  --file-url <pdf-url> extract content from a PDF
63
66
  --format <id>[,<id>...] output formats (see: canvora formats)
64
- --slides <format>=<n> slide count for carousel/deck formats (1-10)
67
+ --slides <format>=<n> slide count for carousel/deck formats (1-10, default 5); match it to the item count your idea promises
65
68
  --brand <uuid> apply a brand kit
66
69
  --style <name> modern | minimal | bold | elegant | playful | corporate | creative | dark
67
70
  --resolution 2K|4K 4K on paid plans
71
+ --language <code-or-name> visual text language ("es", "Spanish"); default: matches your input's language
72
+ --exact use --input text verbatim, no rewording (quotes, taglines)
73
+ --workspace <uuid> bill a team workspace pool instead of your personal credits
68
74
  --wait poll until done (--timeout 600, --poll 5 seconds)
69
75
  --json machine-readable output
70
76
  ```
@@ -88,7 +94,7 @@ Full API guide: [canvora.ai/help/integrations/api-keys](https://canvora.ai/help/
88
94
  ## Built to be called by software
89
95
 
90
96
  - **Predictable costs** — flat credit pricing (10/visual, 15/slide); `canvora credits` before big jobs; failed generations auto-refund.
91
- - **Human in the loop** — everything an agent creates lands in the Canvora dashboard for review, editing, and export.
97
+ - **Human in the loop** — everything your agent creates also lands in your Canvora dashboard, ready for review, editing, and export before it goes anywhere.
92
98
  - **Scoped, revocable keys** — hashed at rest, per-key usage tracking, one-click revoke.
93
99
  - **Outputs on a CDN** — stable URLs your agent can post, embed, or hand to a scheduler.
94
100
  - **Works on every plan** — API and MCP access included on Free (free credits, no card).
package/bin/canvora.js CHANGED
@@ -22,7 +22,7 @@ process.stderr.on('error', (e) => { if (e.code === 'EPIPE') process.exit(0); thr
22
22
 
23
23
  const fs = require('fs');
24
24
  const path = require('path');
25
- const VERSION = '1.0.0';
25
+ const VERSION = '1.2.0';
26
26
  const API_URL = (process.env.CANVORA_API_URL || 'https://api.canvora.ai').replace(/\/+$/, '');
27
27
  const API_KEY = process.env.CANVORA_API_KEY || '';
28
28
  const REQUEST_TIMEOUT_MS = 60000;
@@ -196,28 +196,32 @@ async function cmdGenerate(flags) {
196
196
  if (flags.style) body.visualStyle = String(flags.style);
197
197
  if (flags.resolution) body.resolution = String(flags.resolution);
198
198
  if (flags.title) body.title = String(flags.title);
199
+ if (flags.language) body.language = String(flags.language);
200
+ if (flags.exact) body.useExactContent = true;
201
+ if (flags.workspace) body.workspaceId = String(flags.workspace);
199
202
 
200
203
  const created = await api('/api/generations', { method: 'POST', body });
201
204
  const gen = created.generation;
202
205
  say(`generation ${gen.id} started (${gen.creditsCharged} credits)`);
203
206
 
207
+ await finishOrWait(created, gen.id, flags);
208
+ }
209
+
210
+ // Shared by generate/localize/variations: print id, or poll to terminal state.
211
+ async function finishOrWait(created, genId, flags) {
204
212
  if (!flags.wait) {
205
213
  emit(created, flags.json, () => {
206
- process.stdout.write(`${gen.id}\n`);
207
- process.stdout.write(`check progress: canvora status ${gen.id}\n`);
214
+ process.stdout.write(`${genId}\n`);
215
+ process.stdout.write(`check progress: canvora status ${genId}\n`);
208
216
  });
209
217
  return;
210
218
  }
211
-
212
- // --wait: poll until terminal state
213
219
  const timeoutMs = (parseInt(flags.timeout, 10) || 600) * 1000;
214
220
  const pollMs = (parseInt(flags.poll, 10) || 5) * 1000;
215
221
  const started = Date.now();
216
- let last = null;
217
-
218
222
  while (Date.now() - started < timeoutMs) {
219
223
  await new Promise((r) => setTimeout(r, pollMs));
220
- last = await api(`/api/generations/${gen.id}`);
224
+ const last = await api(`/api/generations/${genId}`);
221
225
  const g = last.generation;
222
226
  say(` ${g.status}${g.progress != null ? ` ${g.progress}%` : ''}`);
223
227
  if (['completed', 'failed', 'partial', 'cancelled'].includes(g.status)) {
@@ -232,7 +236,89 @@ async function cmdGenerate(flags) {
232
236
  return;
233
237
  }
234
238
  }
235
- fail(`timed out after ${timeoutMs / 1000}s waiting for generation ${gen.id} (it may still complete: canvora status ${gen.id})`);
239
+ fail(`timed out after ${timeoutMs / 1000}s waiting for generation ${genId} (it may still complete: canvora status ${genId})`);
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // v1.2: refine existing generations (localize / variations / edit)
244
+ // All three preserve the original design instead of regenerating from scratch.
245
+ // Server gates these behind the Visual Editor feature (Starter+ or an active
246
+ // Campaign Pack) - a 403 here means the plan lacks it, not an auth problem.
247
+ // ---------------------------------------------------------------------------
248
+
249
+ async function cmdLocalize(flags, id) {
250
+ requireKey();
251
+ if (!id) fail('usage: canvora localize <generationId> --language <code-or-name> [--wait]', 2);
252
+ if (!flags.language) fail('--language is required (e.g. --language es, --language Spanish)', 2);
253
+ const created = await api('/api/images/localize-mcp', {
254
+ method: 'POST',
255
+ body: { referenceGenerationId: id, language: String(flags.language) }
256
+ });
257
+ say(`localizing ${created.expectedCount} visual(s) to ${flags.language} (${created.creditCost} credits) -> generation ${created.generationId}`);
258
+ await finishOrWait(created, created.generationId, flags);
259
+ }
260
+
261
+ async function cmdVariations(flags, id) {
262
+ requireKey();
263
+ if (!id) fail('usage: canvora variations <generationId> [--count <n>] [--wait]', 2);
264
+ const count = parseInt(flags.count, 10) || 1;
265
+ const created = await api('/api/images/variation-mcp', {
266
+ method: 'POST',
267
+ body: { referenceGenerationId: id, count }
268
+ });
269
+ say(`creating ${created.expectedCount} variation(s) (${created.creditCost} credits) -> generation ${created.generationId}`);
270
+ await finishOrWait(created, created.generationId, flags);
271
+ }
272
+
273
+ async function cmdEdit(flags, outputId) {
274
+ requireKey();
275
+ if (!outputId) fail('usage: canvora edit <outputId> --prompt "change..." [--yes | --confirm "exact instruction"]', 2);
276
+ const prompt = flags.prompt && String(flags.prompt).trim();
277
+ if (!prompt) fail('--prompt is required (what to change, in plain language)', 2);
278
+
279
+ // One-shot path: caller supplies the exact confirmed instruction
280
+ if (flags.confirm && typeof flags.confirm === 'string') {
281
+ const done = await api('/api/images/edit', {
282
+ method: 'POST',
283
+ body: { outputId, prompt, confirmedInstruction: String(flags.confirm) }
284
+ });
285
+ return emitEditResult(done, flags);
286
+ }
287
+
288
+ // Step 1: preview
289
+ const first = await api('/api/images/edit', { method: 'POST', body: { outputId, prompt } });
290
+ if (first.needsClarification) {
291
+ say(`needs clarification: ${first.clarificationMessage}`);
292
+ for (const s of first.suggestions || []) say(` - ${s}`);
293
+ fail('re-run with a more specific --prompt', 1);
294
+ }
295
+ if (first.needsConfirmation) {
296
+ if (!flags.yes) {
297
+ emit(first, flags.json, () => {
298
+ process.stdout.write(`preview: ${first.preview}\n`);
299
+ process.stdout.write(`confirm: canvora edit ${outputId} --prompt ${JSON.stringify(prompt)} --yes\n`);
300
+ });
301
+ return;
302
+ }
303
+ // Step 2: auto-confirm with the previewed interpretation
304
+ const done = await api('/api/images/edit', {
305
+ method: 'POST',
306
+ body: { outputId, prompt, confirmedInstruction: first.preview }
307
+ });
308
+ return emitEditResult(done, flags);
309
+ }
310
+ return emitEditResult(first, flags);
311
+ }
312
+
313
+ function emitEditResult(done, flags) {
314
+ if (done.needsClarification) {
315
+ say(`needs clarification: ${done.clarificationMessage}`);
316
+ fail('re-run with a more specific --prompt', 1);
317
+ }
318
+ emit(done, flags.json, () => {
319
+ if (done.imageUrl) process.stdout.write(`${done.imageUrl}\n`);
320
+ });
321
+ if (done.imageUrl) say(`edited (${done.creditsCharged} credits, v${done.version})`);
236
322
  }
237
323
 
238
324
  async function cmdStatus(flags, id) {
@@ -290,7 +376,16 @@ commands:
290
376
  [--slides <format>=<n>] [--brand <uuid>]
291
377
  [--style modern|minimal|bold|elegant|playful|corporate|creative|dark]
292
378
  [--resolution 2K|4K] [--title <t>]
379
+ [--language <code-or-name>] (default: matches input language)
380
+ [--exact] (use --input text verbatim, no rewording - quotes, taglines)
381
+ [--workspace <uuid>] (bill a team workspace pool)
293
382
  [--wait [--timeout 600] [--poll 5]]
383
+ localize same design, new language
384
+ canvora localize <generationId> --language es [--wait]
385
+ variations riff on a generation canvora variations <generationId> [--count 2] [--wait]
386
+ (--count is PER OUTPUT: 2 on a 5-slide carousel = 10 new images)
387
+ edit change one visual canvora edit <outputId> --prompt "warmer colors" [--yes]
388
+ (no --yes: prints the interpreted edit for confirmation)
294
389
  status check a generation canvora status <generationId>
295
390
  download save output files canvora download <generationId> [--dir out/]
296
391
  formats list all format IDs
@@ -316,6 +411,9 @@ async function main() {
316
411
  try {
317
412
  switch (cmd) {
318
413
  case 'generate': return await cmdGenerate(flags);
414
+ case 'localize': return await cmdLocalize(flags, pos[1]);
415
+ case 'variations': return await cmdVariations(flags, pos[1]);
416
+ case 'edit': return await cmdEdit(flags, pos[1]);
319
417
  case 'status': return await cmdStatus(flags, pos[1]);
320
418
  case 'download': return await cmdDownload(flags, pos[1]);
321
419
  case 'formats': return await cmdFormats(flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canvora/cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for Canvora (canvora.ai): turn any idea, URL, doc, or PDF into on-brand visuals in 100+ formats, native in 150+ languages. Built for AI agents and automation.",
5
5
  "bin": { "canvora": "bin/canvora.js" },
6
6
  "files": ["bin", "README.md", "LICENSE"],