UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

131 lines • 5.54 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 { isFsError } from "../../errors/fs.js"; import { mountedPath } from "../../utils/key_prefix.js"; import { mayHold, recordQueries } from "../../utils/record_search.js"; import { compareCodePoints } from "../../utils/sort.js"; import { rowAt, rowsOf } from "../hierarchy/probe.js"; import { ROOT } from "../hierarchy/scope.js"; import { TrelloApiError, searchCards } from "./client.js"; import { readdir } from "./readdir.js"; import { detectScope } from "./scope.js"; import { rstripSlash } from "../../utils/slash.js"; // The most cards one search answers; a full page may have left some out. export const SEARCH_LIMIT = 1000; // The words a card's JSON holds whatever its text says: its letter-only key // names and the JSON literals. Every other field Trello does not search is // checked against the listing (`unsearchedText`). export const RECORD_KEYS = new Set([ 'due', 'closed', 'desc', 'url', 'true', 'false', 'null', ]); // The directories above a card, walked down to its cards. export const WALKED = new Set([ ROOT, 'workspaces', 'workspace', 'boards', 'board', 'lists', 'list', ]); // Each card directory under `scope`, with the row its list's listing names; // null for a scope inside a card. async function cards(accessor, scope, entry, parent) { const kind = detectScope(scope).kind; if (kind === 'card') return [[scope, entry]]; if (kind === 'card_json' || kind === 'comments_jsonl') return null; if (!WALKED.has(kind) && kind !== 'cards') return []; const base = rstripSlash(scope.mountPath); const found = []; for (const row of rowsOf(await readdir(accessor, scope, entry, parent))) { const below = await cards(accessor, mountedPath(scope, `${base}/${row.name}`), row, entry); if (below === null) return null; for (const item of below) found.push(item); } return found; } /** * The `card.json` files under `under` whose card Trello's search names, or * whose other fields hold `text`. * * Trello searches a card's name and description by word, so each word is * asked on its own, and a card is read when each word is either named there * or held by the text the listing kept (`unsearchedText`): the rest of its * JSON (ids, due date, flags, link), and the words of those two the search * may split otherwise. The walk lists each directory from Trello, so the kept * text is checked against the cards as they are now. The text goes through * `recordQueries` for its key names and escapes. Null when the text cannot be * searched, a scope lies inside a card, a search answers a full page or fails, * or nothing is found, since Trello indexes a write some time after it lands. * Mirrors Python's `files_containing`. */ export async function filesContaining(accessor, text, under) { const queries = recordQueries(text, RECORD_KEYS, true); if (queries === null) return null; const asked = queries.map((query) => query.toLowerCase().split(/\s+/).filter(Boolean)); const found = []; try { const named = new Map(); for (const word of [...new Set(asked.flat())].sort(compareCodePoints)) { const hits = await searchCards(accessor.transport, word, SEARCH_LIMIT); if (hits.length >= SEARCH_LIMIT) return null; named.set(word, new Set(hits.map((hit) => String(hit.id)))); } for (const scope of under) { const [entry, parent] = await rowAt(readdir, accessor, scope); const walked = await cards(accessor, scope, entry, parent); if (walked === null) return null; for (const [path, row] of walked) { if (row !== null) { const kept = row.extra.unsearched; if (typeof kept !== 'string') return null; if (!mayHold(asked, named, String(row.extra.card_id), kept)) continue; } found.push(mountedPath(path, `${rstripSlash(path.mountPath)}/card.json`)); } } } catch (err) { // fetch rejects with a TypeError when the connection fails and a // DOMException when its timeout aborts it; a body that is not JSON // rejects with a SyntaxError const failed = isFsError(err) || err instanceof TrelloApiError || err instanceof TypeError || err instanceof DOMException || err instanceof SyntaxError; if (!failed) throw err; console.debug(`trello search failed (${String(err)}); reading every file`); return null; } return found.length > 0 ? found : null; } //# sourceMappingURL=search.js.map