Miraiclip SDK
Fundamentals

Commands

Every edit is a validated, serializable, undoable command.

Commands are the only way to change the document. A command is plain JSON: a type and a payload.

project.dispatch({ type: "clip/move", payload: { clipId: "intro", startUs: 1_000_000 } });
PropertyWhat it gives you
ValidatedPayloads are checked against a Zod schema before anything runs
AtomicA failed command throws and leaves state untouched
DeterministicSame state + same command = same result. Required for collaboration and replay
UndoableEach command records its inverse; see History
SerializableLog, sync, store or generate them with an LLM

Built-in commands

GroupCommands
Projectproject/set-settings
Assetsasset/add, asset/remove, asset/set-property → Assets
Trackstrack/add, track/remove, track/reorder, track/rename, track/set-property → Tracks
Clipsclip/add, clip/remove, clip/move, clip/trim, clip/split, clip/duplicate, clip/set-property → Clips
Keyframeskeyframe/set, keyframe/remove, keyframe/clear → Keyframes
Effectseffect/add, effect/update, effect/remove, effect/reorder → Effects
Transitionstransition/add, transition/update, transition/remove → Transitions

project.commandCatalog() returns a JSON Schema for every registered command. It's the reference for every payload field, and you can hand it to an LLM as its tools: Tools from the command catalog.

Errors

dispatch throws one of three errors. State is never changed when it throws.

ErrorWhenUseful fields
UnknownCommandErrorNo command with that type is registeredcommandType
CommandValidationErrorThe payload doesn't match the schemacommandType, issues (Zod issues)
CommandRejectedErrorThe payload is valid but can't apply to the current statecommandType, code
import { CommandRejectedError, CommandValidationError } from "@miraiclip/core";

try {
  project.dispatch({ type: "asset/remove", payload: { id: "intro-mp4" } });
} catch (error) {
  if (error instanceof CommandRejectedError && error.code === "asset-in-use") {
    // remove the clips that use it first
  } else if (error instanceof CommandValidationError) {
    console.error(error.issues);
  } else {
    throw error;
  }
}

Common rejection codes:

CodeCause
track-not-found, clip-not-foundThe id doesn't exist
duplicate-idAn entity with that id already exists
kind-mismatchThe track doesn't accept this clip kind
asset-kind-mismatchE.g. a video clip pointing at an audio asset
asset-in-useRemoving an asset that clips still use

Without exceptions

tryDispatch returns a result instead of throwing. Its failures are plain data, which suits AI agents and form validation.

import { tryDispatch } from "@miraiclip/core";

const result = tryDispatch(project, { type: "clip/move", payload: { clipId: "nope", startUs: 0 } });
if (!result.ok) {
  result.error.kind;    // "unknown-command" | "invalid-payload" | "rejected"
  result.error.message; // 'Command "clip/move" rejected (clip-not-found): no clip "nope"'
}

Several commands as one step

Wrap related commands in a transaction. They undo and redo together, and if one throws, the earlier ones are rolled back. More in History.

project.transaction(() => {
  project.dispatch({ type: "clip/split", payload: { clipId: "intro", atUs: 2_000_000, newClipId: "intro-b" } });
  project.dispatch({ type: "clip/remove", payload: { clipId: "intro-b" } });
}, "Trim end");

Custom commands

Register your own command types. They get the same validation, history, patches and catalog entry as built-ins.

import { CommandRejectedError } from "@miraiclip/core";
import { z } from "zod";

project.registerCommand({
  type: "clip/nudge",
  schema: z.object({ clipId: z.string(), byUs: z.number().int() }),
  handler: (doc, { clipId, byUs }) => {
    const clip = doc.clips[clipId];
    if (!clip) throw new CommandRejectedError("clip/nudge", "clip-not-found", `No clip ${clipId}`);
    clip.startUs = Math.max(0, clip.startUs + byUs);
  },
});

project.dispatch({ type: "clip/nudge", payload: { clipId: "intro", byUs: 500_000 } });
RuleWhy
The handler mutates doc, a draftChanges are recorded as patches and inverses (Immer)
Throw CommandRejectedError to refuseThe draft is discarded and state stays untouched
Keep handlers pure: no Date.now(), Math.random() or I/OReplays and collaborators must get the same result
Register on every project instanceRegistration is per project, not global

Custom commands use zod v4 schemas (the version @miraiclip/core uses).

On this page