@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
81 lines • 3.54 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 { callerSignal } from "../../context/session_context.js";
import { enoent } from "../../errors/fs.js";
import { FileType } from "../../types.js";
import { resolveGlobWith } from "../../utils/glob_walk.js";
import { mountPrefixOf } from "../../utils/key_prefix.js";
import { rstripSlash, stripSlash } from "../../utils/slash.js";
import { listingAt, treeKey } from "./tree.js";
export function validateQuery(query, topK) {
if (query === '')
throw new Error('query is required');
if (query.length > 250)
throw new Error('query cannot exceed 250 characters');
if (topK <= 0)
throw new Error('top-k must be positive');
}
/** Every file a search scope covers, keyed by its slug. */
export async function targetEntries(tree, accessor, paths) {
const rows = await tree.load(accessor);
const targets = new Map();
for (const path of paths) {
const key = treeKey(path);
if (rows.has(key)) {
const stem = key === '/' ? '' : key;
for (const [folder, children] of rows) {
if (folder !== key && !folder.startsWith(stem + '/'))
continue;
for (const row of children) {
if (row.type !== FileType.DIRECTORY) {
targets.set(stripSlash(`${folder === '/' ? '' : folder}/${row.name}`), row);
}
}
}
continue;
}
const cut = key.lastIndexOf('/');
const name = key.slice(cut + 1);
const found = (rows.get(key.slice(0, cut) || '/') ?? []).find((row) => row.name === name);
if (found === undefined)
throw enoent(path.virtual);
targets.set(stripSlash(key), found);
}
return targets;
}
/**
* The paths a batch search covers and the prefix its hits print under. A
* scope at the mount root covers the whole collection, which the backend
* searches unfiltered, so it resolves to no paths at all. Globs expand
* against one load of the tree.
*/
export async function searchScope(tree, accessor, paths) {
const first = paths[0];
if (first === undefined)
throw new Error('at least one scope is required');
const prefix = mountPrefixOf(first.virtual, first.vfsPath);
if (paths.some((p) => stripSlash(p.vfsPath) === ''))
return [[], prefix];
const signal = callerSignal() ?? null;
const rows = await tree.load(accessor);
const listed = (_accessor, path) => {
const base = rstripSlash(path.virtual);
return Promise.resolve(listingAt(rows, path).entries.map((row) => `${base}/${row.name}`));
};
return [
await resolveGlobWith(listed, accessor, paths, undefined, -1, undefined, undefined, undefined, undefined, signal),
prefix,
];
}
//# sourceMappingURL=search.js.map