@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
128 lines • 5.81 kB
JavaScript
// ========= 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 { DirListing, FileStat, FileType } from "../../types.js";
import { asArray, asObject, stringOf as strOf } from "../../utils/records.js";
import { makeReaddir } from "../hierarchy/readdir.js";
import { entryStat } from "../hierarchy/stat.js";
import { jsonBytes } from "../render/json.js";
import { databaseSegmentName, dataSourceSegmentName, normalizeDatabase, normalizeDataSource, pageSegmentName, } from "./normalize.js";
import { getChildPages, getDatabase, getDataSource, searchDataSources, searchPages, } from "./pages.js";
import { formatSegment } from "./pathing.js";
import { guardRow } from "./resolve.js";
import { detectScope } from "./scope.js";
export const PAGE = entryStat('page_id', FileType.DIRECTORY);
export const DATABASE = entryStat('database_id', FileType.DIRECTORY);
export const DATA_SOURCE = entryStat('data_source_id', FileType.DIRECTORY);
async function listPagesRoot(accessor, _match) {
const pages = await searchPages(accessor.transport);
return pages
.filter((page) => asObject(page.parent).type === 'workspace')
.map((page) => PAGE(pageSegmentName(page), strOf(page, 'id'), {
modified: strOf(page, 'last_edited_time'),
}));
}
async function listDatabasesRoot(accessor, _match) {
// Search answers with data sources since 2025-09-03, so the set of
// databases is their distinct parents. Each one still costs a retrieve,
// because only the database object carries the title and url this
// directory is named and rendered from.
const owners = [];
for (const dataSource of await searchDataSources(accessor.transport)) {
const owner = strOf(asObject(dataSource.parent), 'database_id');
if (owner !== '' && !owners.includes(owner))
owners.push(owner);
}
const rows = [];
for (const databaseId of owners) {
const database = await getDatabase(accessor.transport, databaseId);
rows.push(DATABASE(databaseSegmentName(database), databaseId, {
modified: strOf(database, 'last_edited_time'),
}));
}
return rows;
}
async function listPage(accessor, match, _entry) {
const pageId = match.slots.page_id ?? match.slots.row_id ?? '';
const refs = await getChildPages(accessor.transport, pageId);
// page.json renders from getPage plus the *recursive* block tree while
// this listing only holds one level of children, so sizing it here would
// cost an extra call pair per page. It stays size-unknown until a read
// hydrates it.
const rows = [new FileStat({ name: 'page.json', type: FileType.FILE })];
for (const ref of refs) {
rows.push(PAGE(formatSegment({ id: ref.id, title: ref.title }), ref.id, {
modified: ref.lastEditedTime,
}));
}
return rows;
}
async function listDatabase(accessor, match, _entry) {
const databaseId = match.slots.database_id ?? '';
const database = await getDatabase(accessor.transport, databaseId);
// database.json renders the database object this listing already
// fetched, so its exact size is free here.
const rows = [
new FileStat({
name: 'database.json',
type: FileType.FILE,
size: jsonBytes(normalizeDatabase(database)).byteLength,
extra: { database_id: databaseId },
}),
];
for (const stub of asArray(database.data_sources)) {
const record = asObject(stub);
rows.push(DATA_SOURCE(dataSourceSegmentName(record), strOf(record, 'id'), {
modified: strOf(database, 'last_edited_time'),
}));
}
return rows;
}
async function listDataSource(accessor, match, _entry) {
const dataSourceId = match.slots.data_source_id ?? '';
const dataSource = await getDataSource(accessor.transport, dataSourceId);
// The rows are one file, not one directory each: a query answers a hundred
// rows' cells a call, while a row directory costs calls of its own to
// enter, so a walk over a table stays a walk over its pages of query
// results. rows.jsonl stays size-unknown until a read renders it. A row is
// still a directory its path reaches, which this listing does not name, so
// the listing proves nothing absent.
const extra = { data_source_id: dataSourceId };
return new DirListing({
entries: [
new FileStat({
name: 'data_source.json',
type: FileType.FILE,
size: jsonBytes(normalizeDataSource(dataSource)).byteLength,
extra,
}),
new FileStat({ name: 'rows.jsonl', type: FileType.FILE, extra }),
],
window: true,
});
}
export const readdir = makeReaddir(detectScope, {
listers: {
pages: listPagesRoot,
databases: listDatabasesRoot,
},
parentEntryListers: { row: listPage },
entryListers: {
page: listPage,
database: listDatabase,
data_source: listDataSource,
},
staticRoot: ['pages', 'databases'],
guards: { row: guardRow, page: guardRow },
});
//# sourceMappingURL=readdir.js.map