batchedits 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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/cli.js +114 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,7 +19,8 @@ Needs Node.js 18.17+ and a [BatchEdits](https://batchedits.com) account (7-day f
19
19
  | `login` / `logout` / `whoami` | Sign in through your browser; check the connection |
20
20
  | `styles` | List your styles |
21
21
  | `actions` | List editing actions and their settings |
22
- | `style create <name> [--captions] [--emojis] [--silence] [--repeats] [--crop 9:16] [--zoom] [--clips [n]]` | Create a style from flags |
22
+ | `style create <name> [--captions] [--emojis] [--silence] [--repeats] [--crop 9:16] [--zoom] [--clips [n]] [--set action.setting=value…]` | Create a style from flags |
23
+ | `style edit <name> [same flags] [--set …] [--remove <action>…] [--name <new>]` | Change a saved style's edits and settings |
23
24
  | `upload <files or folders…>` | Upload up to 50 videos, print their ids |
24
25
  | `process <ids…> --style <name> [--yes]` | Edit videos (1 credit each, 1 per AI clip) |
25
26
  | `status [ids…] [--watch]` | Show status, or wait until done |
@@ -28,6 +29,16 @@ Needs Node.js 18.17+ and a [BatchEdits](https://batchedits.com) account (7-day f
28
29
 
29
30
  Every command has `--help` with an example, and `--json` for machine-readable output.
30
31
 
32
+ ### Settings
33
+
34
+ `--set action.setting=value` sets anything `batchedits actions` lists. Short action names: `captions`, `silence`, `crop`, `zoom`, `clips`, `repeats`.
35
+
36
+ ```bash
37
+ batchedits style create "Shorts" --crop 9:16 --silence --captions \
38
+ --set captions.position=center --set captions.highlightColor=#FF6B00 --set zoom.zoomPercent=125
39
+ batchedits style edit "Shorts" --set captions.fontColor=#FFFF00 --remove zoom
40
+ ```
41
+
31
42
  ## Sign in
32
43
 
33
44
  - `batchedits login` opens BatchEdits in your browser to approve the CLI, then stores the login in `~/.config/batchedits`. The token is never printed, so agents can run commands without seeing it.
package/dist/cli.js CHANGED
@@ -355,6 +355,67 @@ async function confirm(flags, question) {
355
355
  rl.close();
356
356
  if (answer !== "y" && answer !== "yes") throw new CliError("Cancelled. Nothing was processed.", EXIT.ERROR);
357
357
  }
358
+ var ACTION_ORDER = ["crop_video", "remove_silence", "remove_repeated_sentences", "add_captions", "punch_zoom", "ai_clips"];
359
+ var ACTION_ALIASES = {
360
+ crop: "crop_video",
361
+ silence: "remove_silence",
362
+ repeats: "remove_repeated_sentences",
363
+ captions: "add_captions",
364
+ zoom: "punch_zoom",
365
+ clips: "ai_clips"
366
+ };
367
+ var SET_HELP = [
368
+ " --set <action.setting=value> Any setting from `batchedits actions` (repeatable). Adds the edit if missing.",
369
+ " captions.position=top|center|bottom captions.fontColor=#FFFFFF captions.highlightColor=#FF6B00",
370
+ " captions.fontSize=64 silence.silentThreshold=0.06 zoom.zoomPercent=125 clips.maxSeconds=60"
371
+ ].join("\n");
372
+ var actionName = (raw) => ACTION_ALIASES[raw.trim().toLowerCase()] ?? raw.trim().toLowerCase();
373
+ var list = (v) => Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
374
+ var sortActions = (a) => [...a].sort((x, y) => (ACTION_ORDER.indexOf(x.name) + 1 || 99) - (ACTION_ORDER.indexOf(y.name) + 1 || 99));
375
+ function upsert(actions, name, params) {
376
+ const existing = actions.find((a) => a.name === name);
377
+ if (!existing) return sortActions([...actions, { name, ...params ? { customParams: params } : {} }]);
378
+ return actions.map((a) => a.name === name && params ? { ...a, customParams: { ...a.customParams ?? {}, ...params } } : a);
379
+ }
380
+ function applyFlagActions(actions, flags) {
381
+ let out = actions;
382
+ if (typeof flags.crop === "string") out = upsert(out, "crop_video", { aspectRatio: flags.crop });
383
+ if (flags.silence) out = upsert(out, "remove_silence");
384
+ if (flags.repeats) out = upsert(out, "remove_repeated_sentences");
385
+ if (flags.captions || flags.emojis) out = upsert(out, "add_captions", flags.emojis ? { emojis: true } : void 0);
386
+ if (flags.zoom) out = upsert(out, "punch_zoom");
387
+ if (flags.clips) out = upsert(out, "ai_clips", typeof flags.clips === "string" ? { maxClips: Number(flags.clips) } : void 0);
388
+ return out;
389
+ }
390
+ function parseValue(raw) {
391
+ if (raw === "true" || raw === "false") return raw === "true";
392
+ if (raw.trim() !== "" && !Number.isNaN(Number(raw))) return Number(raw);
393
+ return raw;
394
+ }
395
+ function applySets(actions, sets) {
396
+ let out = actions;
397
+ for (const entry of list(sets)) {
398
+ const m = entry.match(/^([\w-]+)\.([\w-]+)=(.*)$/s);
399
+ if (!m) throw new CliError(`Can't read --set ${entry}.`, EXIT.USAGE, "Use action.setting=value, e.g. --set captions.position=center");
400
+ out = upsert(out, actionName(m[1]), { [m[2]]: parseValue(m[3]) });
401
+ }
402
+ return out;
403
+ }
404
+ async function actionIds(api) {
405
+ const { actions } = await api.call("list_actions");
406
+ return new Map(actions.map((a) => [a.name, a.id]));
407
+ }
408
+ function toToolActions(actions, ids) {
409
+ const unknown = actions.filter((a) => !ids.has(a.name)).map((a) => a.name);
410
+ if (unknown.length) throw new CliError(`Unknown edit: ${unknown.join(", ")}.`, EXIT.USAGE, `Use one of: ${Object.keys(ACTION_ALIASES).join(", ")} (or a name from: batchedits actions).`);
411
+ return actions.map((a) => ({ id: ids.get(a.name), ...a.customParams ? { customParams: a.customParams } : {} }));
412
+ }
413
+ function describeActions(actions) {
414
+ return actions.map((a) => {
415
+ const p = Object.entries(a.customParams ?? {}).filter(([k]) => !["focusX", "focusY", "fitToVideo"].includes(k));
416
+ return p.length ? `${a.name} (${p.map(([k, v]) => `${k} ${v}`).join(", ")})` : a.name;
417
+ }).join(", ");
418
+ }
358
419
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
359
420
  function withClips(all, ids) {
360
421
  const wanted = new Set(ids);
@@ -463,7 +524,7 @@ var commands = {
463
524
  },
464
525
  "style create": {
465
526
  summary: "Create a style from simple flags",
466
- usage: "batchedits style create <name> [--captions] [--emojis] [--silence] [--crop 9:16] [--zoom] [--clips [n]] [--repeats]",
527
+ usage: "batchedits style create <name> [--captions] [--emojis] [--silence] [--crop 9:16] [--zoom] [--clips [n]] [--repeats] [--set action.setting=value\u2026]",
467
528
  help: [
468
529
  "Creates a reusable style. Order of edits: crop, silence, repeats, captions, zoom, AI clips.",
469
530
  " --captions Auto captions (word-by-word highlight)",
@@ -473,29 +534,57 @@ var commands = {
473
534
  " --crop <ratio> Crop every video, e.g. 9:16, 1:1, 4:5",
474
535
  " --zoom Zoom in on key moments",
475
536
  " --clips [n] Cut long videos into n short clips (default 10; 1 credit per clip)",
476
- 'Example: batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom'
537
+ SET_HELP,
538
+ 'Example: batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom --set captions.position=center'
477
539
  ].join("\n"),
478
540
  async run({ api, positionals, flags }) {
479
541
  const name = positionals.join(" ").trim();
480
542
  if (!name) throw new CliError("Give the style a name.", EXIT.USAGE, 'Example: batchedits style create "Shorts" --captions --silence');
481
- const { actions } = await api.call("list_actions");
482
- const byName = new Map(actions.map((a) => [a.name, a.id]));
483
- const wanted = [];
484
- if (typeof flags.crop === "string") wanted.push({ name: "crop_video", customParams: { aspectRatio: flags.crop } });
485
- if (flags.silence) wanted.push({ name: "remove_silence" });
486
- if (flags.repeats) wanted.push({ name: "remove_repeated_sentences" });
487
- if (flags.captions || flags.emojis) wanted.push({ name: "add_captions", customParams: flags.emojis ? { emojis: true } : void 0 });
488
- if (flags.zoom) wanted.push({ name: "punch_zoom" });
489
- if (flags.clips) wanted.push({ name: "ai_clips", customParams: typeof flags.clips === "string" ? { maxClips: Number(flags.clips) } : void 0 });
543
+ const ids = await actionIds(api);
544
+ const wanted = applySets(applyFlagActions([], flags), flags.set);
490
545
  if (wanted.length === 0) throw new CliError("Pick at least one edit.", EXIT.USAGE, "See: batchedits style create --help");
491
- const missing = wanted.filter((w) => !byName.has(w.name)).map((w) => w.name);
492
- if (missing.length) throw new CliError(`This server doesn't support: ${missing.join(", ")}.`);
493
- const res = await api.call("create_style", {
494
- name,
495
- actions: wanted.map((w) => ({ id: byName.get(w.name), ...w.customParams ? { customParams: w.customParams } : {} }))
496
- });
497
- if (isJson()) printJson({ styleId: res.styleId, name });
498
- else say(`Created style "${name}" (${res.styleId}). Use it with: --style "${name}"`);
546
+ const res = await api.call("create_style", { name, actions: toToolActions(wanted, ids) });
547
+ if (isJson()) printJson({ styleId: res.styleId, name, actions: wanted });
548
+ else say(`Created style "${name}" (${res.styleId}): ${describeActions(wanted)}.
549
+ Use it with: --style "${name}"`);
550
+ return EXIT.OK;
551
+ }
552
+ },
553
+ "style edit": {
554
+ summary: "Change a style's edits and settings",
555
+ usage: "batchedits style edit <name|id> [same flags as style create] [--set action.setting=value\u2026] [--remove <action>\u2026] [--name <new name>]",
556
+ help: [
557
+ "Changes a saved style in place. Flags add an edit (or update it, e.g. --crop 1:1); other edits stay as they are.",
558
+ " --remove <action> Remove an edit, e.g. --remove zoom (repeatable)",
559
+ " --name <name> Rename the style",
560
+ SET_HELP,
561
+ "Videos already edited keep their look; the next ones use the new settings.",
562
+ 'Example: batchedits style edit "Shorts" --set captions.fontColor=#FFFF00 --remove zoom'
563
+ ].join("\n"),
564
+ async run({ api, positionals, flags }) {
565
+ const ref = positionals.join(" ").trim();
566
+ if (!ref) throw new CliError("Give the style to edit.", EXIT.USAGE, 'Example: batchedits style edit "Shorts" --set captions.position=center');
567
+ const style = await resolveStyle(api, ref);
568
+ const ids = await actionIds(api);
569
+ let actions = (style.actions ?? []).map((a) => ({ name: a.name, ...a.customParams ? { customParams: { ...a.customParams } } : {} }));
570
+ const removes = list(flags.remove).map(actionName);
571
+ const missing = removes.filter((r) => !actions.some((a) => a.name === r));
572
+ if (missing.length) throw new CliError(`"${style.name}" has no ${missing.join(", ")} to remove.`, EXIT.USAGE, `It has: ${describeActions(actions) || "no edits"}.`);
573
+ actions = actions.filter((a) => !removes.includes(a.name));
574
+ actions = applySets(applyFlagActions(actions, flags), flags.set);
575
+ const newName = typeof flags.name === "string" ? flags.name.trim() : void 0;
576
+ if (actions.length === 0) throw new CliError("A style needs at least one edit.", EXIT.USAGE, "Remove fewer edits, or add one with a flag like --captions.");
577
+ await api.call("edit_style", { styleId: style.id, ...newName ? { name: newName } : {}, actions: toToolActions(actions, ids) });
578
+ const saved = (await api.styles()).find((s) => s.id === style.id);
579
+ const savedNames = (saved?.actions ?? []).map((a) => a.name).join(",");
580
+ const setKeys = list(flags.set).map((e) => e.split("=")[0].split(".")).map(([a, k]) => [actionName(a), k]);
581
+ const keysSaved = setKeys.every(([a, k]) => saved?.actions?.find((x) => x.name === a)?.customParams?.[k] !== void 0);
582
+ if (savedNames !== actions.map((a) => a.name).join(",") || !keysSaved) {
583
+ throw new CliError("The server didn't save the new edits.", EXIT.ERROR, "It may not support editing styles yet. Create a new style with: batchedits style create");
584
+ }
585
+ const finalName = newName || style.name;
586
+ if (isJson()) printJson({ styleId: style.id, name: finalName, actions });
587
+ else say(`Updated style "${finalName}": ${describeActions(actions)}.`);
499
588
  return EXIT.OK;
500
589
  }
501
590
  },
@@ -610,7 +699,7 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
610
699
  };
611
700
 
612
701
  // src/cli.ts
613
- var VERSION = true ? "1.1.0" : "dev";
702
+ var VERSION = true ? "1.2.0" : "dev";
614
703
  var FLAG_OPTIONS = {
615
704
  help: { type: "boolean", short: "h" },
616
705
  version: { type: "boolean", short: "v" },
@@ -627,7 +716,10 @@ var FLAG_OPTIONS = {
627
716
  repeats: { type: "boolean" },
628
717
  crop: { type: "string" },
629
718
  zoom: { type: "boolean" },
630
- clips: { type: "string" }
719
+ clips: { type: "string" },
720
+ set: { type: "string", multiple: true },
721
+ remove: { type: "string", multiple: true },
722
+ name: { type: "string" }
631
723
  };
632
724
  function mainHelp() {
633
725
  const names = Object.keys(commands);
@@ -646,6 +738,7 @@ function mainHelp() {
646
738
  "Quick start:",
647
739
  " batchedits login",
648
740
  ' batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom',
741
+ ' batchedits style edit "Shorts" --set captions.position=center',
649
742
  ' batchedits run ./raw --style "Shorts" --out ./edited',
650
743
  "",
651
744
  "Exit codes: 0 ok \xB7 1 error \xB7 2 usage \xB7 3 sign in needed \xB7 4 some videos failed \xB7 5 needs --yes"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "batchedits",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "BatchEdits CLI, SDK and React widgets: edit up to 50 videos at once from your terminal or AI agent (Claude Code, OpenClaw, Hermes)",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",