UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

115 lines • 4.9 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 { mountedPath } from "../../utils/key_prefix.js"; import { GoogleApiError } from "../google/client.js"; import { rowAt, rowsOf } from "../hierarchy/probe.js"; import { ROOT } from "../hierarchy/scope.js"; import { readdir } from "./readdir.js"; import { detectScope } from "./scope.js"; import { rstripSlash } from "../../utils/slash.js"; // The directories above a message, walked down to its files. const WALKED = new Set([ROOT, 'label', 'day']); // Gmail's system label ids, lowercased. With the account's own labels, // `Label_<n>`, they are all a message's labels field spells, and a message's // labels change after the listing kept its text. export const SYSTEM_LABELS = new Set([ 'inbox', 'spam', 'trash', 'unread', 'starred', 'important', 'sent', 'draft', 'chat', 'category_personal', 'category_social', 'category_promotions', 'category_updates', 'category_forums', ]); export const USER_LABEL = /^[label_]*\d*$/; function childOf(directory, name) { return mountedPath(directory, `${rstripSlash(directory.mountPath)}/${name}`); } // The account's label ids, lowercased, from the mount root's listing. async function labelIds(accessor, scope) { const listed = await readdir(accessor, mountedPath(scope, '/')); return new Set(rowsOf(listed).map((row) => String(row.extra.label_id).toLowerCase())); } // Whether `needle` may lie in a message's labels field: across its quotes and // commas, in a `Label_<n>` id, or in a label id. function inLabels(needle, labels) { return (/["[\],]/.test(needle) || USER_LABEL.test(needle) || [...SYSTEM_LABELS, ...labels].some((label) => label.includes(needle))); } /** * The message files under `under` whose text holds `text` in any case. * * Gmail's search reads a message's HTML part and can miss a word of the * text/plain body a .gmail.json renders, such as one inside a link or one only * that part holds, so it is not asked. The listing fetches every message in * full anyway and, under a `walkLimit`, keeps each one's rendered text, * lowercased, without its labels; the files are picked from that. null for a * text that is not ASCII, whose case a lowercased copy may not keep, a text * that may lie in a labels field (`inLabels`), which changes after the * listing, when a listing kept no text, or when a listing fails. Mirrors * Python's `files_containing`. */ export async function filesContaining(accessor, text, under) { const first = under[0]; if (/[\u0080-\uffff]/.test(text) || first === undefined) return null; const needle = text.toLowerCase(); const found = []; try { const labels = await labelIds(accessor, first); if (inLabels(needle, labels)) return null; const pending = []; for (const scope of under) { if (WALKED.has(detectScope(scope).kind)) { pending.push([scope, (await rowAt(readdir, accessor, scope))[0]]); } } for (let next = pending.pop(); next !== undefined; next = pending.pop()) { const [current, row] = next; for (const childRow of rowsOf(await readdir(accessor, current, row))) { const child = childOf(current, childRow.name); const kind = detectScope(child).kind; if (WALKED.has(kind)) pending.push([child, childRow]); else if (kind === 'message') { const kept = childRow.extra.text; if (typeof kept !== 'string') return null; if (kept.includes(needle)) found.push(child); } } } } catch (err) { // fetch rejects with a TypeError when the connection fails and a // DOMException when its timeout aborts it if (!(err instanceof GoogleApiError || err instanceof TypeError || err instanceof DOMException)) { throw err; } console.debug(`gmail listing failed (${String(err)}); reading every file`); return null; } return found; } //# sourceMappingURL=search.js.map