Cloud Drive - 2DAICommunity/2dai-cloud-sdk GitHub Wiki

Cloud Drive

Organise your library from code: folders (collections) and sidebar groups, moving creations, likes, bulk actions, trash/restore, permanent delete, and publishing to the public feed.

  • Reading (list/search folders and creations, the feed, random) needs the read scope.
  • Organising (folders, groups, move, like, reorder, batch, trash, restore, delete) needs manage.
  • Publishing / unpublishing needs publish.

Folders (collections)

// List β€” newest first, one page at a time (limit default 20, max 100)
let page = await client.folders.list({ limit: 50 });
const all = [...page.folders];
while (page.nextBeforeDate) {
  page = await client.folders.list({ limit: 50, beforeDate: page.nextBeforeDate });
  all.push(...page.folders);
}

// Create β€” optionally straight into one of your sidebar groups
const folder = await client.folders.create({ title: 'Brand assets', description: 'logos + refs' });

// Rename / re-describe
await client.folders.update(folder.folderId, { title: 'Brand kit' });

// Pin a poster, star it in the sidebar, move it between groups
await client.folders.update(folder.folderId, {
  posterCreationId: gen.creationId,  // null clears the poster
  isFavorite: true,                  // feeds the smart 'favorites' lens
  groupId: group.groupId,            // null detaches from its group
});

// Delete β€” contained creations detach to the drive root…
await client.folders.delete(folder.folderId);
// …or send them to trash instead:
await client.folders.delete(folder.folderId, { trashContents: true });

An account holds up to 500 folders β€” creating past the cap answers 409 FOLDER_LIMIT_REACHED.

list() hands back a page β€” { folders, nextBeforeDate } β€” not a bare array, so an account with hundreds of collections never pays for all of them at once. The cursor works exactly like the creation list's: nextBeforeDate comes back only while a further page may exist. Calling list(signal) with a bare AbortSignal still works.

list also takes a sort β€” index (the sidebar's drag-reorder order), date-desc, date-asc, name-asc, name-desc, update-desc β€” the same vocabulary the studio uses. Only the default and date-desc orderings emit nextBeforeDate (the cursor filters on the creation date, so it can't page any other order without skipping rows); for the rest, raise limit β€” at 100 it covers any realistic drive.

const byName = await client.folders.list({ sort: 'name-asc', limit: 100 });

Folder groups

Groups are the collapsible sections the studio's cloud-drive sidebar organises collections into. They hold folders, never creations directly:

const groups = await client.folders.groups.list();            // sidebar order
const group = await client.folders.groups.create('Client work');
await client.folders.groups.rename(group.groupId, 'Clients');
await client.folders.groups.remove(group.groupId);            // detaches member folders β€” nothing else is deleted

Move a folder in or out with folders.update(id, { groupId }) β€” a group id to set, null to detach β€” or create it grouped from the start with folders.create({ title, groupId }). An account holds up to 100 groups (409 GROUP_LIMIT_REACHED), and a Folder row carries its groupId (absent when ungrouped) plus isFavorite.

Moving creations

// Into a folder
await client.creations.move(gen, folder.folderId);

// Back to the drive root
await client.creations.move(gen, 'root');   // or: move(gen, null)

Every method accepts a creation id or any object that carries one (a Creation, a generation result, a QueueState).

Browsing the drive

creations.list speaks the same lenses the studio's cloud drive uses. Pick one mode per call β€” the default flat listing (folderId / trashed), an activity lens, a smart collection, a sharedFolderId, or a groupId β€” then compose any of the cross-cutting filters on top. Mixing two modes rejects with 400 CONFLICTING_FILTERS.

// A folder's contents
const inFolder = await client.creations.list({ folderId: folder.folderId });

// Ungrouped root
const root = await client.creations.list({ folderId: 'root' });

// The trash
const trash = await client.creations.list({ trashed: true });

// Activity lenses β€” the studio's fixed collections:
// all | history | uploaded | likes | public | trash | sdk | mcp | portfolio
const uploads = await client.creations.list({ activity: 'uploaded' });

// Smart collections: faces (portraits), videos, crop (cropped images),
// alpha (transparent PNGs from erase edits), favorites (creations inside
// starred folders)
const faces = await client.creations.list({ smart: 'faces' });

// A folder someone shared WITH you (view access)
const shared = await client.creations.list({ sharedFolderId: theirFolderId });

// A folder group (the sidebar groups) β€” every folder filed in it, one
// listing; `creations.random({ groupId })` samples across the whole group
const grouped = await client.creations.list({ groupId: group.groupId });

// Everything BUILT FROM a given creation β€” any reference slot (image refs,
// face/character refs, style sources) or lineage parent (retry, crop, erase,
// clone, edit) counts, and clone families are expanded server-side, exactly
// like the studio's filter-by-reference lens.
const derived = await client.creations.list({ usedRef: gen });

Search, sort and filters

The cross-cutting filters compose with any lens:

const hits = await client.creations.list({
  search: 'red fox*',          // whole-word AND over descriptions + tags; trailing * = prefix
  sort: 'size-desc',           // newest|oldest|updated|size-desc|size-asc|type-asc|type-desc
  ratioFilter: 'portrait',     // square|landscape|portrait β€” or a comma set: 'square,portrait'
  from: '2026-07-01',          // date range β€” ISO or epoch ms
  to: Date.now(),
});

// hideFiled: skip creations already filed into a folder β€” meaningful on the
// history / uploaded / likes / sdk / mcp lenses
const unfiled = await client.creations.list({ activity: 'history', hideFiled: true });

// The studio's cloud filters (SDK β‰₯ 3.1.0). likeFilter: 'liked' keeps the
// creations YOU (the key owner) liked, 'unliked' the others β€” on every lens,
// shared folders included. mediaKind: 'image' (GIFs included), 'video' (clips)
// or 'audio' (MP3 uploads). blockNsfw hides NSFW rows on this call even when
// the account shows them.
const likedClips = await client.creations.list({
  folderId: folder.folderId,
  likeFilter: 'liked',
  mediaKind: 'video',
});

search is capped at 128 characters / 8 tokens. An unknown likeFilter or mediaKind rejects with 400 INVALID_LIKE_FILTER / INVALID_MEDIA_KIND; on activity: 'likes', likeFilter: 'unliked' naturally returns nothing. Your account's own NSFW preference always applies. Rows come back enriched: description, folderId, inTrash, isPublicShared, likes, isLiked, isOwner, nsfwFlagged, nsfwRate (own rows), and username on rows you don't own.

Pagination

// Cursor (newest first) β€” prefer it for deep walks
let page = await client.creations.list({ limit: 50 });
while (page.nextBeforeDate) {
  page = await client.creations.list({ limit: 50, beforeDate: page.nextBeforeDate });
}

// Or 1-based offset paging β€” the page echoes { page, limit, count }
const p3 = await client.creations.list({ sort: 'size-desc', page: 3, limit: 50 });

nextBeforeDate is only emitted when rows are in newest-first date order (the default sort, no relevance-ranked search) β€” a date cursor can't walk any other ordering without skipping or repeating rows, so everything else pages with page instead.

Random pick

creations.random returns one uniformly-sampled creation from the whole filtered collection (or null when nothing matches). Same filters as list, no paging. It draws images and videos only: the trash and mediaKind: 'audio' are refused (400 RANDOM_NOT_SUPPORTED):

const surprise = await client.creations.random({ smart: 'favorites' });
const anyFox = await client.creations.random({ search: 'fox' });
const still = await client.creations.random({ smart: 'favorites', mediaKind: 'image', blockNsfw: true });

The public feed

creations.feed pages the cross-user public feed, newest-published first β€” what the studio's community wall shows. Rows you don't own hide their prompts; includeNsfw opts into flagged rows:

const feed = await client.creations.feed({ limit: 48 });          // limit 1..48, default 24
if (feed.hasMore) await client.creations.feed({ limit: 48, page: 2 });

Likes

like is an idempotent SET, never a toggle β€” a retry can't double-toggle, which is what you want in automation. changed is false when the state already matched. Any live creation is likeable (your own or a public one); trashed/deleted rows answer 410:

const r = await client.creations.like(gen, true);    // { liked: true, likeCount, changed }
await client.creations.like(gen, false);             // unlike β€” same idempotent contract

Your liked creations are the activity: 'likes' lens.

Cloning

clone makes a byte-identical duplicate (images only) with its own CDN file β€” trashing either copy never breaks the other. The copy inherits the original's description, tags and moderation verdict (same bytes, nothing is re-scored), lands private in the same folder as the original (move it afterwards to relocate), and costs no credit. It rides the same tier + storage-quota gates as an upload, and needs the manage scope:

const copy = await client.creations.clone(gen);
copy.isCloned;               // true
copy.clonedFromCreationId;   // gen's id β€” chains are flattened: cloning a
                             // clone still points at the ORIGINAL

Typical use: keep one master library and fan copies out into per-project folders, without re-uploading bytes or re-running moderation. Video rows reject with CLONE_UNSUPPORTED_MEDIA.

Trash, restore, delete

await client.creations.trash(gen);      // reversible
await client.creations.restore(gen);    // undo

// Permanent delete β€” must be trashed first (else 409 NOT_IN_TRASH)
await client.creations.trash(gen);
await client.creations.delete(gen);     // purges the CDN copy too

Bulk actions

batch runs one action over up to 1000 ids in a single request β€” one call instead of a thousand:

const r = await client.creations.batch('trash', ids);
// { action: 'trash', requested: 812, processed: 810, skipped: 2 }

Actions: 'trash', 'untrash', 'delete'. It's owner-only, and rows whose state doesn't fit the action are counted in skipped rather than failing the call β€” delete requires rows to already be in trash, so the safe two-step purge is batch('trash', ids) then batch('delete', ids).

Reorder

reorder stamps an explicit drag-order: creationIds[i] gets sort index i (≀1000 ids; ids you don't own are silently skipped). This is the order the studio's drag-reorder shows:

await client.creations.reorder([id3, id1, id2]);   // β†’ { reordered: 3 }

Where API creations land (source)

Creations made through the SDK / raw REST / MCP are stamped with a source ('sdk', 'api', 'mcp'; studio creations are 'web') and surface in the dashboard's Cloud Drive β†’ Activities β†’ SDK / MCP collections, on top of the usual All / History / Uploads views. The field is on every Creation row (SDK β‰₯ 2.0.1):

const page = await client.creations.list({ limit: 50 });
const mine = page.creations.filter(c => c.source === 'sdk');

// Or ask for the collection directly β€” the same lens the dashboard shows:
const sdkWork = await client.creations.list({ activity: 'sdk' });

Publish / unpublish

await client.creations.publish(gen);    // β†’ { isPublicShared: true }
await client.creations.unpublish(gen);  // β†’ { isPublicShared: false }

Only your own generated creations can be published; uploaded source files cannot. Publishing a trashed creation returns 409 CREATION_IN_TRASH β€” restore it first.