@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
105 lines • 4.57 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 { isFsError } from "../../errors/fs.js";
import { DirListing } from "../../types.js";
import { mountedPath } from "../../utils/key_prefix.js";
import { recordQueries } from "../../utils/record_search.js";
import { stripSlash } from "../../utils/slash.js";
import { GoogleApiError } from "./client.js";
import { RENDERED_WORDS, TOP_LEVEL_DIRS } from "./constants.js";
import { rstripSlash } from "../../utils/slash.js";
/**
* The whole words Drive is asked for `text` over an app mount's JSON, or null
* when its index cannot stand in for grep: the `recordQueries` of the text,
* with the app's API names and the words every rendering carries as keys. A
* digit, a JSON literal or a word an id can hold is never asked, and a word a
* JSON escape can run into is asked without its first letter too. Mirrors
* Python's `drive_queries`.
*/
export function driveQueries(text, vocabulary) {
return recordQueries(text, new Set([...vocabulary, ...RENDERED_WORDS]), true);
}
/**
* The Docs, Sheets or Slides files under `under` Drive full-text search names.
*
* One query asks for every file of the app's MIME type holding the word, and
* the answer is matched by id against the owned and shared listings grep walks
* next. Null for a word `driveQueries` turns down, a scope below a corpus,
* a listing or search that fails or that Drive did not finish, or an empty
* answer.
*
* Drive searches a file's text, so the answer can leave out one whose JSON
* holds the word only in a value that is not text (a font, a time zone, a link
* or image address, a tab title, an id or a formula), or only as part of a word
* styling splits into text runs (`snow` bold in `snowman`). Mirrors Python's
* `files_containing`.
*/
export async function filesContaining(accessor, readdir, mime, vocabulary, text, under) {
const queries = driveQueries(text, vocabulary);
if (queries === null)
return null;
const corpora = [];
for (const directory of under) {
const key = stripSlash(directory.vfsPath);
if (key === '') {
for (const corpus of TOP_LEVEL_DIRS)
corpora.push(mountedPath(directory, `/${corpus}`));
}
else if (TOP_LEVEL_DIRS.includes(key)) {
corpora.push(directory);
}
else {
return null;
}
}
const found = [];
try {
const named = new Set();
for (const query of queries) {
const answer = await accessor.drive.searchFiles(query, [], mime);
if (!answer.complete)
return null;
for (const id of answer.ids)
named.add(id);
}
for (const corpus of corpora) {
const listed = await readdir(accessor, corpus);
// An unfinished listing is not the directory, and grep's own walk lists
// it again.
if (listed instanceof DirListing && listed.partial)
return null;
const rows = listed instanceof DirListing ? listed.entries : listed;
const base = rstripSlash(corpus.mountPath);
for (const row of rows) {
if (named.has(String(row.extra.doc_id))) {
found.push(mountedPath(corpus, `${base}/${row.name}`));
}
}
}
}
catch (err) {
// fetch rejects with a TypeError when the connection fails and a
// DOMException when its timeout aborts it
const failed = isFsError(err) ||
err instanceof GoogleApiError ||
err instanceof TypeError ||
err instanceof DOMException;
if (!failed)
throw err;
console.debug(`drive search failed (${String(err)}); reading every file`);
return null;
}
return found.length > 0 ? found : null;
}
//# sourceMappingURL=search.js.map