@canvora/cli 1.1.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,11 +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
68
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
69
74
  --wait poll until done (--timeout 600, --poll 5 seconds)
70
75
  --json machine-readable output
71
76
  ```
@@ -89,7 +94,7 @@ Full API guide: [canvora.ai/help/integrations/api-keys](https://canvora.ai/help/
89
94
  ## Built to be called by software
90
95
 
91
96
  - **Predictable costs** — flat credit pricing (10/visual, 15/slide); `canvora credits` before big jobs; failed generations auto-refund.
92
- - **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.
93
98
  - **Scoped, revocable keys** — hashed at rest, per-key usage tracking, one-click revoke.
94
99
  - **Outputs on a CDN** — stable URLs your agent can post, embed, or hand to a scheduler.
95
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.1.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;
@@ -197,28 +197,31 @@ async function cmdGenerate(flags) {
197
197
  if (flags.resolution) body.resolution = String(flags.resolution);
198
198
  if (flags.title) body.title = String(flags.title);
199
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);
200
202
 
201
203
  const created = await api('/api/generations', { method: 'POST', body });
202
204
  const gen = created.generation;
203
205
  say(`generation ${gen.id} started (${gen.creditsCharged} credits)`);
204
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) {
205
212
  if (!flags.wait) {
206
213
  emit(created, flags.json, () => {
207
- process.stdout.write(`${gen.id}\n`);
208
- 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`);
209
216
  });
210
217
  return;
211
218
  }
212
-
213
- // --wait: poll until terminal state
214
219
  const timeoutMs = (parseInt(flags.timeout, 10) || 600) * 1000;
215
220
  const pollMs = (parseInt(flags.poll, 10) || 5) * 1000;
216
221
  const started = Date.now();
217
- let last = null;
218
-
219
222
  while (Date.now() - started < timeoutMs) {
220
223
  await new Promise((r) => setTimeout(r, pollMs));
221
- last = await api(`/api/generations/${gen.id}`);
224
+ const last = await api(`/api/generations/${genId}`);
222
225
  const g = last.generation;
223
226
  say(` ${g.status}${g.progress != null ? ` ${g.progress}%` : ''}`);
224
227
  if (['completed', 'failed', 'partial', 'cancelled'].includes(g.status)) {
@@ -233,7 +236,89 @@ async function cmdGenerate(flags) {
233
236
  return;
234
237
  }
235
238
  }
236
- 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})`);
237
322
  }
238
323
 
239
324
  async function cmdStatus(flags, id) {
@@ -292,7 +377,15 @@ commands:
292
377
  [--style modern|minimal|bold|elegant|playful|corporate|creative|dark]
293
378
  [--resolution 2K|4K] [--title <t>]
294
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)
295
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)
296
389
  status check a generation canvora status <generationId>
297
390
  download save output files canvora download <generationId> [--dir out/]
298
391
  formats list all format IDs
@@ -318,6 +411,9 @@ async function main() {
318
411
  try {
319
412
  switch (cmd) {
320
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]);
321
417
  case 'status': return await cmdStatus(flags, pos[1]);
322
418
  case 'download': return await cmdDownload(flags, pos[1]);
323
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.1.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"],