@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
166 lines (155 loc) • 5.91 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 { GSlidesAccessor } from "../../accessor/gslides.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 { MIME } from "../../core/gslides/constants.js";
import { read as gslidesRead } from "../../core/gslides/read.js";
import { readdir as gslidesReaddir } from "../../core/gslides/readdir.js";
import { rename as gslidesRename } from "../../core/gslides/rename.js";
import { stat as gslidesStat } from "../../core/gslides/stat.js";
import { unlink as gslidesUnlink } from "../../core/gslides/unlink.js";
import { VFSName } from "../../types.js";
import { SLIDES_WORDS } from "../../utils/generated/google_vocabulary.js";
import { sliceWindow } from "../../utils/ranges.js";
import { BaseVFS } from "../base.js";
import { GSlidesConfig } from "./config.js";
import { makeFilename } from "./slide_entry.js";
export const VFS_MD = `\`\`\`text
{prefix}
owned/ # presentations you created
<YYYY-MM-DD>_<title>__<presentation-id>.gslide.json
shared/ # presentations others shared with you
<YYYY-MM-DD>_<title>__<presentation-id>.gslide.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.
- \`<presentation-id>\` is the Google Slides presentation id.
- \`shared/\` holds presentations shared with you by others; ones you own
and shared with others are still in \`owned/\`.
A \`.gslide.json\` matches the Google Slides API \`presentations.get\`
response:
\`\`\`jsonc
{
"presentationId": "...",
"title": "...",
"pageSize": { "width": {...}, "height": {...} },
"slides": [
{ // one element per slide
"objectId": "...",
"pageElements": [
{
"objectId": "...",
"shape": {
"shapeType": "TEXT_BOX",
"text": {
"textElements": [
{ "textRun": { "content": "the actual text\\n",
"style": {...} } }
]
}
}
},
{ "image": {...} },
{ "table": {...} }
]
}
],
"masters": [...],
"layouts": [...]
}
\`\`\`
Useful \`jq\` paths:
\`\`\`jq
.title
.slides | length # slide count
.slides[].pageElements[].shape.text.textElements[].textRun.content # all text
.slides[0].objectId
\`\`\`
Delete, on a \`write\` path:
\`\`\`sh
rm {prefix}/owned/<file>.gslide.json # deletes it from Drive for good
rm -f <path> # ignores a missing file
\`\`\`
Only \`.gslide.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>.gslide.json \\
{prefix}/owned/<date>_<new>__<id>.gslide.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 GSlidesVFS extends BaseVFS {
name = VFSName.GSLIDES;
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 GSlidesAccessor({ tokenManager: tm });
}
handed = {
unlink: ['entry'],
rename: ['entry'],
};
renderers = { '.gslide.json': 'readDeck' };
/**
* 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, gslidesReaddir, MIME, SLIDES_WORDS, text, [path]);
}
readdir(path) {
return gslidesReaddir(this.accessor, path);
}
/** Render the file as the JSON its `.gslide.json` name holds. */
async readDeck(path, _handed, offset = 0, size = null) {
const data = await gslidesRead(this.accessor, path);
return offset === 0 && size === null ? data : sliceWindow(data, offset, size);
}
stat(path) {
return gslidesStat(this.accessor, path);
}
unlink(path, handed) {
return gslidesUnlink(this.accessor, path, handed?.entry);
}
rename(src, dst, handed) {
return gslidesRename(this.accessor, src, dst, handed?.entry ?? null);
}
deltaHook() {
return appDeltaHook(this.accessor, MIME, makeFilename);
}
getState() {
return Promise.resolve(this.configState(GSlidesConfig, this.config));
}
}
//# sourceMappingURL=gslides.js.map