@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 +8 -2
- package/bin/canvora.js +107 -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,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
|
|
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.
|
|
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(`${
|
|
207
|
-
process.stdout.write(`check progress: canvora status ${
|
|
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/${
|
|
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 ${
|
|
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.
|
|
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"],
|