UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

188 lines (175 loc) • 7 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 { GDocsAccessor } from "../../accessor/gdocs.js"; import { MIME } from "../../core/gdocs/constants.js"; import { read as gdocsRead } from "../../core/gdocs/read.js"; import { readdir as gdocsReaddir } from "../../core/gdocs/readdir.js"; import { rename as gdocsRename } from "../../core/gdocs/rename.js"; import { stat as gdocsStat } from "../../core/gdocs/stat.js"; import { unlink as gdocsUnlink } from "../../core/gdocs/unlink.js"; import { TokenManager } from "../../core/google/client.js"; import { appDeltaHook } from "../../core/google/events.js"; import { filesContaining as googleFilesContaining } from "../../core/google/search.js"; import { VFSName } from "../../types.js"; import { DOCS_WORDS } from "../../utils/generated/google_vocabulary.js"; import { sliceWindow } from "../../utils/ranges.js"; import { BaseVFS } from "../base.js"; import { GDocsConfig } from "./config.js"; import { makeFilename } from "./doc_entry.js"; export const VFS_MD = `\`\`\`text {prefix} owned/ # docs you created <YYYY-MM-DD>_<title>__<doc-id>.gdoc.json shared/ # docs others shared with you <YYYY-MM-DD>_<title>__<doc-id>.gdoc.json \`\`\` - \`<YYYY-MM-DD>\` is the file's \`modifiedTime\`, so a date glob such as \`2026-05-*\` narrows by it. - \`<title>\` is sanitized: spaces and any character outside \`[A-Za-z0-9_.-]\` become \`_\`, at most 100 characters. - \`<doc-id>\` is the Google Docs document id. - \`shared/\` holds docs shared with you by others; ones you own and shared with others are still in \`owned/\`. A \`.gdoc.json\` holds the Google Docs API \`documents.get\` response, read with \`includeTabsContent=true\` so every tab is present: \`\`\`jsonc { "documentId": "...", "title": "...", "tabs": [ // one entry per top-level tab { "tabProperties": { "tabId": "t.0", // names a tab for gws docs write "title": "Tab 1", "index": 0 }, "documentTab": { "body": { "content": [ { // one element per block "paragraph": { "elements": [ { "textRun": { "content": "the actual text\\n", "textStyle": {...} } } ], "paragraphStyle": {...} } }, { "table": {...} }, { "sectionBreak": {...} } ] }, "documentStyle": {...}, "namedStyles": {...} }, "childTabs": [ { ...same shape, nested to any depth... } ] // a nested tab also carries // parentTabId and nestingLevel; // a root tab carries neither } ], "revisionId": "..." } \`\`\` There is no top-level .body. A tab-aware response leaves the singleton fields (\`.body\`, \`.documentStyle\`, \`.namedStyles\`) empty and hangs every tab's content off \`.tabs[].documentTab\` instead. Tabs also nest, so text can sit at any depth under \`.childTabs\`; the recursive recipes below read a one-tab and a fifty-tab document alike. Useful \`jq\` paths: \`\`\`jq .title [.. | .tabProperties? // empty | .title] # every tab name [.. | .textRun? // empty | .content] | add # all text, any depth [.. | .tabProperties? // empty] | length # tab count [.tabs[0].documentTab.body.content[] | .paragraph?.elements[]?.textRun.content] | add # first tab only .revisionId \`\`\` The first block of a body is always a \`sectionBreak\`, which carries no \`.paragraph\`, so a path through \`.paragraph\` needs the \`?\` or it fails with \`Cannot iterate over null\`. Delete, on a \`write\` path: \`\`\`sh rm {prefix}/owned/<file>.gdoc.json # deletes it from Drive for good rm -f <path> # ignores a missing file \`\`\` Only \`.gdoc.json\` files can be removed; \`owned/\` and \`shared/\` cannot. Rename to a new title, on a \`write\` path: \`\`\`sh mv {prefix}/owned/<date>_<old>__<id>.gdoc.json \\ {prefix}/owned/<date>_<new>__<id>.gdoc.json \`\`\` Keep the folder, the date and the id, and spell the title the way the listing does (spaces become \`_\`); any other name is refused.`; export class GDocsVFS extends BaseVFS { name = VFSName.GDOCS; cachesReads = true; indexTtl = 86_400; // Reads stamp listing metadata; a fresh stat checks Drive by file ID. readRevalidatable = true; vfsMd = VFS_MD; wholeWordSearch = true; config; accessor; constructor(config) { super(); this.config = config; const tm = new TokenManager(config); this.accessor = new GDocsAccessor({ tokenManager: tm }); } handed = { unlink: ['entry'], rename: ['entry'], }; renderers = { '.gdoc.json': 'readDoc' }; /** * Whether this VFS answers `name`; with no walk limit it reads every * file rather than ask the service's search. */ supports(name) { if (name === 'files_containing' && this.walkLimit < 0) return false; return super.supports(name); } filesContaining(path, text, opts) { if (!opts.wholeWord || this.walkLimit < 0) return Promise.resolve(null); return googleFilesContaining(this.accessor, gdocsReaddir, MIME, DOCS_WORDS, text, [path]); } readdir(path) { return gdocsReaddir(this.accessor, path); } /** Render the file as the JSON its `.gdoc.json` name holds. */ async readDoc(path, _handed, offset = 0, size = null) { const data = await gdocsRead(this.accessor, path); return offset === 0 && size === null ? data : sliceWindow(data, offset, size); } stat(path) { return gdocsStat(this.accessor, path); } unlink(path, handed) { return gdocsUnlink(this.accessor, path, handed?.entry); } rename(src, dst, handed) { return gdocsRename(this.accessor, src, dst, handed?.entry ?? null); } deltaHook() { return appDeltaHook(this.accessor, MIME, makeFilename); } getState() { return Promise.resolve(this.configState(GDocsConfig, this.config)); } } //# sourceMappingURL=gdocs.js.map