carouselbot 0.3.4 → 0.3.5

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
@@ -100,3 +100,5 @@ Any number of agents can share one connected browser tab, including editing the
100
100
  Browser writes use revision-checked IndexedDB transactions and cross-tab synchronization. A stale tab cannot replace a newer project snapshot; it reloads the canonical copy and returns `STALE_PROJECT` so the agent can inspect and retry.
101
101
 
102
102
  Paths support exactly two folder levels: `/Client/Account` appears as Home → Client → Account. Use this when the user asks to organize multiple accounts for a client; do not reorganize projects without being asked. Create or move projects with `folderPath: "/Client/Account"`; the parent and subfolder appear automatically. `/Client/Account/Campaign` is rejected. `inspect_editor` includes parent folders and `parentPath`; folder project counts include descendants. Renaming a parent in the UI preserves its subfolders.
103
+
104
+ `duplicate_project` copies a complete carousel into an optional destination `folderPath` in one local operation. Images, fonts, slides and editable layers are preserved with independent IDs. It returns `createdProjectId`; the source `projectId` and edit session remain unchanged. Batch two variants together with `apply_operations`, then start sessions on their new IDs to restyle them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carouselbot",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Local-first MCP companion for the hosted CarouselBot editor",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,6 +67,10 @@ When `add_slide` uses `backgroundPath` without `aspectRatio`, CarouselBot derive
67
67
 
68
68
  Dashboard folders are implicit and use exact canonical slash paths such as `/campaigns`. Set `folderPath` when calling `create_project` to create the project inside a folder. Call `move_project` with another slash path to move it between folders, or with `folderPath: null` to move it back to the dashboard root. Paths support exactly two folder levels: `/Client/Account` appears as Home → Client → Account. Use this when the user asks to organize multiple accounts for a client; do not reorganize projects without being asked. Create or move projects with `folderPath: "/Client/Account"`; the parent and subfolder appear automatically. `/Client/Account/Campaign` is rejected. `inspect_editor` includes parent folders and `parentPath`; folder project counts include descendants. Renaming a parent in the UI preserves its subfolders. Moving the final project out of a folder removes that empty folder automatically. Use `inspect_editor` to read each project's current `folderPath`.
69
69
 
70
+ To duplicate whole carousels into another folder, use `duplicate_project({ projectId, folderPath: "/Client/Account", name, editSessionId })`. This copies all slides, assets, fonts, and editable layers locally in one call; do not reconstruct layers or transfer images through the agent. The source is unchanged. Omit `name` for a “copy” suffix or supply the original name for parallel variants in separate folders. The response's `createdProjectId` identifies the copy; `projectId` and the session stay bound to the source. Begin a new session on the copy to edit it. Two duplicates of one source can share one `apply_operations` call and source-bound session.
71
+
72
+ For restyling many carousels, inspect one project at a time with `includeAllProjects: false`, use explicit slide IDs, and batch up to 100 edits per `apply_operations` call within its project session. `update_text` accepts up to 100 `{ id, ...stylePatch }` entries in its `updates` array for one slide. `render_slide` returns actual pixels (540px wide by default); it does not require an editor screenshot or navigation. Inspect representative renders, but check every affected slide when font or geometry changes could cause clipping. There is no automatic project-wide style selector: first inspect the affected layers, then apply targeted patches.
73
+
70
74
  Use readable role-based type ranges: title `92–124`, subtitle `68–84`, body `54–68`, caption `44–52`. Do not solve dense copy by dropping below the body range; shorten it or split it across slides. `add_text` and `update_text` automatically preserve width and fit height around every wrapped line with safe padding. For highlighted text, prefer `style: "boxed"` with `backgroundShape: "lines"`. Use `backgroundShape: "full"` only for a deliberate card. Call `fit_text_boxes` with `mode: "both"` only when you intentionally want the width to shrink too.
71
75
 
72
76
  After each meaningful composition or after a short batch, call `render_slide` and inspect the returned image. Fix clipping, spacing, contrast, unsafe overlay placement, and weak hierarchy before claiming the slide is finished. Use `export_slide` or `export_project` only when local files are requested; do not overwrite existing files unless authorized.
@@ -68,7 +68,7 @@ function textResult(value, summary = value) {
68
68
  }
69
69
 
70
70
  function compactMutation(value) {
71
- const keys = ["id", "editSessionId", "editorId", "projectId", "aspectRatio", "canvasWidth", "canvasHeight", "folderPath", "slideId", "revision", "leaseExpiresAt", "purpose", "released", "opened", "createdSlideId", "createdTextId", "fittedTextBox", "createdImageId", "createdLayers", "assetId", "fontId", "localFontId", "existing", "repaired", "deletedAssetId", "deletedProjectId", "deletedSlideId", "deletedLayerIds", "updatedTextIds", "fittedTextBoxes", "updatedImageIds", "applied", "path", "bytes"];
71
+ const keys = ["id", "editSessionId", "editorId", "projectId", "aspectRatio", "canvasWidth", "canvasHeight", "folderPath", "slideId", "revision", "leaseExpiresAt", "purpose", "released", "opened", "createdProjectId", "createdSlideId", "createdTextId", "fittedTextBox", "createdImageId", "createdLayers", "assetId", "fontId", "localFontId", "existing", "repaired", "deletedAssetId", "deletedProjectId", "deletedSlideId", "deletedLayerIds", "updatedTextIds", "fittedTextBoxes", "updatedImageIds", "applied", "path", "bytes"];
72
72
  return Object.fromEntries(keys.flatMap((key) => {
73
73
  if (key === "folderPath" && Object.hasOwn(value || {}, key)) return [[key, value[key] ?? null]];
74
74
  return value?.[key] == null ? [] : [[key, value[key]]];
@@ -89,7 +89,7 @@ async function pathExists(value) {
89
89
 
90
90
  function operationLabel(toolName) {
91
91
  return ({
92
- create_project: "Creating a project…", update_project: "Updating the project…", move_project: "Moving the project…", delete_project: "Deleting a project…",
92
+ create_project: "Creating a project…", duplicate_project: "Duplicating a project…", update_project: "Updating the project…", move_project: "Moving the project…", delete_project: "Deleting a project…",
93
93
  open_project: "Opening a project…", add_slide: "Adding a slide…", update_slide: "Updating a slide…",
94
94
  duplicate_slide: "Duplicating a slide…", reorder_slides: "Reordering slides…", delete_slide: "Deleting a slide…",
95
95
  add_text: "Adding text…", update_text: "Updating text…", fit_text_boxes: "Fitting text boxes…", import_font: "Adding a local font…", import_asset: "Importing a local image…",
@@ -125,7 +125,7 @@ async function prepareOperation(companion, toolName, args, editSessionId = null)
125
125
  operation.fontMediaId = prepared.fontMediaId;
126
126
  }
127
127
  const type = ({
128
- create_project: "project.create", open_project: "project.open", update_project: "project.update", move_project: "project.move", delete_project: "project.delete",
128
+ create_project: "project.create", duplicate_project: "project.duplicate", open_project: "project.open", update_project: "project.update", move_project: "project.move", delete_project: "project.delete",
129
129
  add_slide: "slide.add", update_slide: "slide.update", duplicate_slide: "slide.duplicate", reorder_slides: "slide.reorder", delete_slide: "slide.delete",
130
130
  add_text: "text.add", update_text: "text.update", fit_text_boxes: "text.fit", import_font: "font.import", list_project_fonts: "font.list", import_asset: "asset.import", update_asset: "asset.update", delete_asset: "asset.delete",
131
131
  add_image: "image.add", update_image: "image.update", delete_layers: "layer.delete", duplicate_layers: "layer.duplicate", reorder_layers: "layer.reorder",
@@ -199,6 +199,7 @@ export async function createCarouselBotMcpServer(companion) {
199
199
  register("create_project", "Create an empty project without changing the user's current browser view. Choose an optional aspect ratio: use a documented preset or a positive integer W:H value; legacy/default projects use 9:16. Pass a canonical folderPath such as /Client or /Client/Account to create it inside that folder (maximum two levels); omit it or use null for the dashboard root.", z.object({ editSessionId, name: z.string().min(1).max(160), aspectRatio: aspectRatio.optional(), folderPath: folderPath.nullable().optional() }).strict(), (args) => browserOperation(companion, "create_project", args), { destructiveHint: false });
200
200
  register("open_project", "Explicitly navigate the browser to a project and optionally a specific slide without changing content. Use only when the user asks to show it.", z.object({ editSessionId, projectId: id, slideId: optionalId }).strict(), (args) => browserOperation(companion, "open_project", args), { destructiveHint: false, idempotentHint: true });
201
201
  register("update_project", "Rename a project.", z.object({ ...targetProject, name: z.string().min(1).max(160) }).strict(), (args) => browserOperation(companion, "update_project", args), { destructiveHint: true });
202
+ register("duplicate_project", "Duplicate a complete carousel, including all slides, image assets, fonts and editable layers, without changing the source or navigating the browser. Omit folderPath to keep the source folder, pass /Client/Account for another folder, or null for Home. Returns createdProjectId for the independent copy; projectId and the edit session remain bound to the source. Use a new session to edit the copy.", z.object({ ...targetProject, projectId: id, name: z.string().min(1).max(160).optional(), folderPath: folderPath.nullable().optional() }).strict(), (args) => browserOperation(companion, "duplicate_project", args), { destructiveHint: false });
202
203
  register("move_project", "Move a project into a folder by canonical slash path (/Client or /Client/Account, maximum two levels), move it between folders, or move it back to the dashboard root with folderPath=null. Folder cards are derived from project membership, so empty folders disappear.", z.object({ ...targetProject, projectId: id, folderPath: folderPath.nullable() }).strict(), (args) => browserOperation(companion, "move_project", args), { destructiveHint: true });
203
204
  register("delete_project", "Delete a project from browser storage.", z.object({ ...targetProject, projectId: id }).strict(), (args) => browserOperation(companion, "delete_project", args), { destructiveHint: true });
204
205