UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

186 lines • 8.68 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { ContentType, FileStat, FileType } from "../../types.js"; import { unsearchedText } from "../../utils/record_search.js"; import { stringOf as pickString } from "../../utils/records.js"; import { makeReaddir } from "../hierarchy/readdir.js"; import { entryStat } from "../hierarchy/stat.js"; import { jsonBytes } from "../render/json.js"; import { getBoard, listBoardLabels, listBoardLists, listBoardMembers, listListCards, listWorkspaceBoards, listWorkspaces, } from "./client.js"; import { normalizeBoard, normalizeCard, normalizeLabel, normalizeList, normalizeMember, normalizeWorkspace, SEARCHED_FIELDS, } from "./normalize.js"; import { boardDirname, cardDirname, labelFilename, listDirname, memberFilename, workspaceDirname, } from "./pathing.js"; import { detectScope } from "./scope.js"; function extraSize(entry) { const value = entry.extra.json_size; return typeof value === 'number' ? value : null; } /** The workspaces the mount shows: the member's, narrowed to `workspaceId` when it is set. */ export async function filteredWorkspaces(accessor) { let workspaces = await listWorkspaces(accessor.transport); if (accessor.workspaceId !== null && accessor.workspaceId !== '') { workspaces = workspaces.filter((w) => pickString(w, 'id') === accessor.workspaceId); } return workspaces; } /** A workspace's open boards the mount shows, narrowed to `boardIds` when it is set. */ export async function filteredBoards(accessor, workspaceId) { let boards = await listWorkspaceBoards(accessor.transport, workspaceId); if (accessor.boardIds !== null && accessor.boardIds.length > 0) { const allowed = new Set(accessor.boardIds); boards = boards.filter((b) => allowed.has(pickString(b, 'id'))); } return boards; } /** Whether `workspaceId` or `boardIds` narrows the mount. */ export function scopeIsNarrowed(accessor) { const byWorkspace = accessor.workspaceId !== null && accessor.workspaceId !== ''; const byBoard = accessor.boardIds !== null && accessor.boardIds.length > 0; return byWorkspace || byBoard; } /** * Whether the mount's scope admits a board addressed by id. * * The two knobs that narrow the listing narrow an id too: a board * `boardIds` leaves out, or one outside `workspaceId`, is not this * mount's to read or write. Unnarrowed, every id is admitted without a * call; `workspaceId` costs one board fetch. Mirrors python's * `board_in_scope`. */ export async function boardInScope(accessor, boardId) { if (!scopeIsNarrowed(accessor)) return true; if (boardId === '') return false; if (accessor.boardIds !== null && accessor.boardIds.length > 0) { if (!accessor.boardIds.includes(boardId)) return false; } if (accessor.workspaceId === null || accessor.workspaceId === '') return true; const board = await getBoard(accessor.transport, boardId); return pickString(board, 'idOrganization') === accessor.workspaceId; } export const WORKSPACE = entryStat('workspace_id', FileType.DIRECTORY); export const WORKSPACE_JSON = entryStat('workspace_id', ContentType.JSON); export const BOARD = entryStat('board_id', FileType.DIRECTORY); export const BOARD_JSON = entryStat('board_id', ContentType.JSON); export const MEMBER = entryStat('member_id', ContentType.JSON); export const LABEL = entryStat('label_id', ContentType.JSON); export const LIST = entryStat('list_id', FileType.DIRECTORY); export const LIST_JSON = entryStat('list_id', ContentType.JSON); export const CARD = entryStat('card_id', FileType.DIRECTORY); export const CARD_JSON = entryStat('card_id', ContentType.JSON); export const COMMENTS = entryStat('card_id', ContentType.TEXT); function dir(name) { return new FileStat({ name, type: FileType.DIRECTORY }); } async function listWorkspacesDir(accessor, _match) { // workspace.json renders the workspace object this listing already // fetched, so its exact size rides the directory's row for the child // listing to read back without another call. return (await filteredWorkspaces(accessor)).map((workspace) => WORKSPACE(workspaceDirname(workspace), pickString(workspace, 'id'), { extra: { json_size: jsonBytes(normalizeWorkspace(workspace)).byteLength }, })); } function listWorkspace(_accessor, match, entry) { return Promise.resolve([ WORKSPACE_JSON('workspace.json', match.slots.workspace_id ?? '', { size: extraSize(entry) }), dir('boards'), ]); } async function listBoards(accessor, match, _entry) { // board.json's normalizer only uses fields the board listing already // carries, so its exact size is free here. return (await filteredBoards(accessor, match.slots.workspace_id ?? '')).map((board) => BOARD(boardDirname(board), pickString(board, 'id'), { modified: pickString(board, 'dateLastActivity'), extra: { json_size: jsonBytes(normalizeBoard(board)).byteLength }, })); } function listBoard(_accessor, match, entry) { return Promise.resolve([ BOARD_JSON('board.json', match.slots.board_id ?? '', { size: extraSize(entry), modified: entry.modified, }), dir('members'), dir('labels'), dir('lists'), ]); } async function listMembers(accessor, match, _entry) { const members = await listBoardMembers(accessor.transport, match.slots.board_id ?? ''); return members.map((member) => MEMBER(memberFilename(member), pickString(member, 'id'), { size: jsonBytes(normalizeMember(member)).byteLength, })); } async function listLabels(accessor, match, _entry) { const labels = await listBoardLabels(accessor.transport, match.slots.board_id ?? ''); return labels.map((label) => LABEL(labelFilename(label), pickString(label, 'id'), { size: jsonBytes(normalizeLabel(label)).byteLength, })); } async function listLists(accessor, match, _entry) { const lists = await listBoardLists(accessor.transport, match.slots.board_id ?? ''); // list.json's normalizer only uses fields the list listing already // carries, so its exact size is free here. return lists.map((lst) => LIST(listDirname(lst), pickString(lst, 'id'), { extra: { json_size: jsonBytes(normalizeList(lst)).byteLength }, })); } function listList(_accessor, match, entry) { return Promise.resolve([ LIST_JSON('list.json', match.slots.list_id ?? '', { size: extraSize(entry) }), dir('cards'), ]); } async function listCards(accessor, match, _entry) { const cards = await listListCards(accessor.transport, match.slots.list_id ?? ''); // card.json's normalizer only uses fields the card listing already // carries, so its exact size is free here. return cards.map((card) => CARD(cardDirname(card), pickString(card, 'id'), { modified: pickString(card, 'dateLastActivity'), extra: { json_size: jsonBytes(normalizeCard(card)).byteLength, ...(accessor.capped() ? { unsearched: unsearchedText(Object.entries(normalizeCard(card)), SEARCHED_FIELDS) } : {}), }, })); } function listCard(_accessor, match, entry) { // comments.jsonl needs a per-card actions call and stays size-unknown. const cardId = match.slots.card_id ?? ''; return Promise.resolve([ CARD_JSON('card.json', cardId, { size: extraSize(entry), modified: entry.modified }), COMMENTS('comments.jsonl', cardId, { modified: entry.modified }), ]); } export const readdir = makeReaddir(detectScope, { listers: { workspaces: listWorkspacesDir, }, entryListers: { workspace: listWorkspace, boards: listBoards, board: listBoard, members: listMembers, labels: listLabels, lists: listLists, list: listList, cards: listCards, card: listCard, }, staticRoot: ['workspaces'], }); //# sourceMappingURL=readdir.js.map