@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 +7 -2
- package/bin/canvora.js +105 -9
- package/package.json +1 -1
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
|
|
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.
|
|
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(`${
|
|
208
|
-
process.stdout.write(`check progress: canvora status ${
|
|
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/${
|
|
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 ${
|
|
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.
|
|
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"],
|