Recipe: folder navigation (resolve a path, list folders/files, create+verify)

Recipe: folder navigation (resolve a path, list folders/files, create+verify)

Recipe: folder navigation (resolve a path, list folders/files, create+verify)

💡

Prompt example

List the folders in Content/Adam, list the.txt files in it, and create three named folders then confirm they exist.

Vibe prompts name folders by path ("the Content/Adam folder") but every component takes a <folder-uuid>. These helpers bridge that gap with the list_children, create_folder, and search components. All reads are Class A (safe anywhere); the create helper is only for a confined parent (your run-root anchor).

Key facts

  • list_children (get_asset_child_nodes) is a folder-navigation tree: it returns only child folders — uploaded files never appear, even after they settle. Items expose name (not displayName), parentId, assetType (1 == folder), and hasChildren. Use it for folders only.
  • To list files, use search scoped by parentId Equals <folder-id> — it returns both files and folders under the parent (names live under item["identifiers"]). There is no server-side filename/extension filter, so match the name client-side.
  • Top-level bucket names repeat on a deployment (several Content/content). A path resolver must follow every matching branch and require a unique leaf, else it can silently pick the wrong tree. resolve_folder_path raises on ambiguity (fail-closed).

Python

# components: list_children, create_folder, search
ROOT_ID = "00000000-0000-0000-0000-000000000000"  # asset-tree root sentinel

def resolve_folder_path(sdk, path, root_id=ROOT_ID):
    """Resolve a '/'-separated folder path to one id; None if absent.

    Follows every branch whose folder name matches each segment and requires a
    unique leaf, because top-level bucket names can repeat. Raises on ambiguity.
    """
    frontier = [root_id]
    for seg in (s for s in path.strip("/").split("/") if s):
        nxt = []
        for cur in frontier:
            for c in list_children(sdk, cur):
                if c.get("assetType") == 1 and (c.get("name") or c.get("displayName")) == seg:
                    nxt.append(c["id"])
        frontier = nxt
    unique = list(dict.fromkeys(frontier))
    if len(unique) > 1:
        raise ValueError(f"Ambiguous folder path {path!r}: {len(unique)} matches")
    return unique[0] if unique else None

def list_folders(sdk, folder_id):
    """Immediate sub-folders of folder_id (assetType == 1)."""
    return [c for c in list_children(sdk, folder_id) if c.get("assetType") == 1]

def list_files_by_extension(sdk, folder_id, ext):
    """Immediate files whose name ends with ext (e.g. '.txt'); case-insensitive.

    Files are NOT in the navigation tree, so list them via search scoped to the
    folder (parentId Equals), then filter on the name client-side (no server-side
    extension filter). Freshly uploaded files appear only once indexed.
    """
    ext = ext.lower()
    flt = [{"fieldName": "parentId", "operator": "Equals", "values": folder_id}]
    items, offset, page = [], 0, 100
    while True:
        batch = (search(sdk, filters=flt, size=page, offset=offset) or {}).get("items", [])
        items.extend(batch)
        if len(batch) < page:
            break
        offset += 1  # offset is in PAGES, not records (see search-patterns.md)
    return [
        it for it in items
        if (it.get("identifiers") or {}).get("assetType") != 1
        and ((it.get("identifiers") or {}).get("displayName") or "").lower().endswith(ext)
    ]

def create_folders_and_verify(sdk, parent_id, names):
    """Create each name under parent_id, then confirm all exist. Returns {name: id}.

    Confinement: parent_id MUST be your run-root anchor (or a folder beneath it) —
    never an arbitrary shared folder. Raises if any expected folder is missing.
    """
    created = {name: create_folder(sdk, parent_id, name) for name in names}
    present = {(c.get("name") or c.get("displayName")) for c in list_folders(sdk, parent_id)}
    missing = [n for n in names if n not in present]
    if missing:
        raise AssertionError(f"folders not found after create: {missing}")
    return created

JavaScript

// components: listChildren, createFolder, search
const ROOT_ID = "00000000-0000-0000-0000-000000000000"; // asset-tree root sentinel

export async function resolveFolderPath(sdk, path, rootId = ROOT_ID) {
    let frontier = [rootId];
    for (const seg of path.split("/").filter(Boolean)) {
        const nxt = [];
        for (const cur of frontier) {
            for (const c of await listChildren(sdk, cur)) {
                if (c.assetType === 1 && (c.name ?? c.displayName) === seg) nxt.push(c.id);
            }
        }
        frontier = nxt;
    }
    const unique = [...new Set(frontier)];
    if (unique.length > 1) throw new Error(`Ambiguous folder path '${path}': ${unique.length} matches`);
    return unique.length ? unique[0] : null;
}

export async function listFolders(sdk, folderId) {
    return (await listChildren(sdk, folderId)).filter((c) => c.assetType === 1);
}

export async function listFilesByExtension(sdk, folderId, ext) {
    // Files are NOT in the navigation tree — list them via search (parentId Equals),
    // then filter on the name client-side (no server-side extension filter).
    const e = ext.toLowerCase();
    const flt = [{ fieldName: "parentId", operator: "Equals", values: folderId }];
    const items = [];
    for (let offset = 0; ; offset += 1) {
        const res = await search(sdk, null, flt, null, 100, offset);
        const batch = res ? res.items : []; // JS returns false when empty
        items.push(...batch);
        if (batch.length < 100) break;
    }
    return items.filter(
        (it) => it.identifiers?.assetType !== 1 &&
            (it.identifiers?.displayName ?? "").toLowerCase().endsWith(e),
    );
}

export async function createFoldersAndVerify(sdk, parentId, names) {
    // Confinement: parentId MUST be your run-root anchor — never a shared folder.
    const created = {};
    for (const name of names) created[name] = await createFolder(sdk, parentId, name);
    const present = new Set((await listFolders(sdk, parentId)).map((c) => c.name ?? c.displayName));
    const missing = names.filter((n) => !present.has(n));
    if (missing.length) throw new Error(`folders not found after create: ${missing}`);
    return created;
}

Notes

  • Folders vs files: list_folders reads the navigation tree (folders appear instantly, no indexing lag); list_files_by_extension uses search, so freshly uploaded files surface only once indexed — poll if you just uploaded.
  • resolve_folder_path is list_children per path segment — fine for shallow paths; for deep trees prefer caching the resolved id and reusing it.
  • To resolve under a known bucket, pass its id as root_id/rootId to skip the ambiguous top level entirely.
  • Pair this with recipes/search-patterns.md once you hold the folder id (e.g. sort or page large folders via search with a parentId Equals filter).