UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

135 lines • 5.89 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 { DirListing, FileStat, FileType } from "../../types.js"; import { globPrefix, globStemPrefix } from "../../utils/glob_walk.js"; import { keptText } from "../../utils/record_search.js"; import { blobBytes } from "../vector/read.js"; import { filtersOf, tableOf } from "../vector/scope.js"; import { groupName, rowStem } from "./naming.js"; import { fieldValue } from "./payload.js"; import { distinctValues, resolveGroup, rowsMatching, tableExists } from "./query.js"; import { renderJson, renderText } from "./render.js"; function blobSize(value) { // A payload whose blob column holds something undecodable must not take // the whole listing down with it: leave the size unknown and let read() // throw the same error it always did. try { return blobBytes(value).byteLength; } catch (err) { console.warn(`qdrant: unsizeable blob value (${String(err)}); size stays unknown`); return null; } } function rowFiles(rows, config) { // The scroll already carries every payload, so each file's exact rendered // size and text are free here; the mount serves the size from the listing // instead of refetching one row per file, and grep checks the text instead // of reading it. const listed = []; for (const row of rows) { const stem = rowStem(row, config); const rendered = renderJson(row, config); listed.push(new FileStat({ name: `${stem}.json`, type: FileType.FILE, size: rendered.byteLength, extra: keptText(rendered), })); if (config.textField !== null && fieldValue(row, config.textField) !== null && fieldValue(row, config.textField) !== undefined) { const text = renderText(row, config); listed.push(new FileStat({ name: `${stem}.txt`, type: FileType.FILE, size: text.byteLength, extra: keptText(text), })); } if (config.blobField !== null && fieldValue(row, config.blobField) !== null && fieldValue(row, config.blobField) !== undefined) { listed.push(new FileStat({ name: `${stem}.${config.blobExt}`, type: FileType.FILE, size: blobSize(fieldValue(row, config.blobField)), })); } } return listed; } /** * The point-id prefix a leaf glob narrows the scroll to. * * A leaf is named `<pointId>` plus whichever suffix the renderer gave it, and * only the id half is a prefix the scroll can test. */ function rowPrefix(pattern, config) { const suffixes = ['.json']; if (config.textField !== null && config.textField !== '') suffixes.push('.txt'); if (config.blobField !== null && config.blobField !== '') suffixes.push(`.${config.blobExt}`); return globStemPrefix(pattern, suffixes); } async function resolvedFilters(accessor, table, filters) { const resolved = {}; for (const [column, value] of Object.entries(filters)) { if (!accessor.config.basenameFields.includes(column)) { resolved[column] = value; continue; } const sources = await resolveGroup(accessor, table, column, resolved, value, true); if (sources.length === 0) return null; if (sources.length > 1) { throw new Error(`qdrant: basename collision for ${JSON.stringify(column)}: ${JSON.stringify(value)}`); } resolved[column] = sources[0] ?? ''; } return resolved; } /** The entries under a collection or a group. */ export async function children(accessor, match) { const config = accessor.config; const table = tableOf(config.collection, match); const pattern = match.pattern; if (!(await tableExists(accessor, table))) return null; const filters = await resolvedFilters(accessor, table, filtersOf(config.groupBy, match)); if (filters === null) return null; const depth = Object.keys(filters).length; if (depth < config.groupBy.length) { const displayPrefix = globPrefix(pattern); const basename = config.basenameFields.includes(config.groupBy[depth] ?? ''); const names = await distinctValues(accessor, table, config.groupBy[depth] ?? '', filters, config.maxRows, displayPrefix, basename); const rendered = names .map((name) => groupName(name, basename)) .filter((name) => name.startsWith(displayPrefix)); if (new Set(rendered).size !== rendered.length) { throw new Error('qdrant: basenameFields produced a path collision'); } return new DirListing({ entries: rendered.map((name) => new FileStat({ name, type: FileType.DIRECTORY })), partial: displayPrefix !== '', window: true, }); } const prefix = rowPrefix(pattern, config); const rows = await rowsMatching(accessor, table, filters, config.maxRows, prefix); return new DirListing({ entries: rowFiles(rows, config), partial: prefix !== '', window: true }); } //# sourceMappingURL=readdir.js.map