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 thereadscope. - 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.