@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
163 lines (152 loc) • 5.9 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 { GSheetsAccessor } from "../../accessor/gsheets.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/gsheets/constants.js";
import { read as gsheetsRead } from "../../core/gsheets/read.js";
import { readdir as gsheetsReaddir } from "../../core/gsheets/readdir.js";
import { rename as gsheetsRename } from "../../core/gsheets/rename.js";
import { stat as gsheetsStat } from "../../core/gsheets/stat.js";
import { unlink as gsheetsUnlink } from "../../core/gsheets/unlink.js";
import { VFSName } from "../../types.js";
import { SHEETS_WORDS } from "../../utils/generated/google_vocabulary.js";
import { sliceWindow } from "../../utils/ranges.js";
import { BaseVFS } from "../base.js";
import { GSheetsConfig } from "./config.js";
import { makeFilename } from "./sheet_entry.js";
export const VFS_MD = `\`\`\`text
{prefix}
owned/ # sheets you created
<YYYY-MM-DD>_<title>__<spreadsheet-id>.gsheet.json
shared/ # sheets others shared with you
<YYYY-MM-DD>_<title>__<spreadsheet-id>.gsheet.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.
- \`<spreadsheet-id>\` is the Google Sheets spreadsheet id.
- \`shared/\` holds sheets shared with you by others; ones you own
and shared with others are still in \`owned/\`.
A \`.gsheet.json\` matches the Google Sheets API \`spreadsheets.get\`
response:
\`\`\`jsonc
{
"spreadsheetId": "...",
"spreadsheetUrl": "...",
"properties": { "title": "...", "locale": "...", "timeZone": "..." },
"sheets": [
{ // one element per tab
"properties": {
"sheetId": 0, "title": "...", "index": 0,
"gridProperties": { "rowCount": 1000, "columnCount": 26 }
},
"data": [
{
"rowData": [
{ "values": [
{ "formattedValue": "...",
"userEnteredValue": {...},
"effectiveValue": {...} }
]} // an empty cell is {}, a trailing one absent
]
}
]
}
],
"namedRanges": [...]
}
\`\`\`
Useful \`jq\` paths:
\`\`\`jq
.properties.title
.sheets[].properties.title # tab names
.sheets[0].data[0].rowData[].values[].formattedValue # cell strings
.namedRanges[]
\`\`\`
Delete, on a \`write\` path:
\`\`\`sh
rm {prefix}/owned/<file>.gsheet.json # deletes it from Drive for good
rm -f <path> # ignores a missing file
\`\`\`
Only \`.gsheet.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>.gsheet.json \\
{prefix}/owned/<date>_<new>__<id>.gsheet.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 GSheetsVFS extends BaseVFS {
name = VFSName.GSHEETS;
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 GSheetsAccessor({ tokenManager: tm });
}
handed = {
unlink: ['entry'],
rename: ['entry'],
};
renderers = { '.gsheet.json': 'readSheet' };
/**
* 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, gsheetsReaddir, MIME, SHEETS_WORDS, text, [path]);
}
readdir(path) {
return gsheetsReaddir(this.accessor, path);
}
/** Render the file as the JSON its `.gsheet.json` name holds. */
async readSheet(path, _handed, offset = 0, size = null) {
const data = await gsheetsRead(this.accessor, path);
return offset === 0 && size === null ? data : sliceWindow(data, offset, size);
}
stat(path) {
return gsheetsStat(this.accessor, path);
}
unlink(path, handed) {
return gsheetsUnlink(this.accessor, path, handed?.entry);
}
rename(src, dst, handed) {
return gsheetsRename(this.accessor, src, dst, handed?.entry ?? null);
}
deltaHook() {
return appDeltaHook(this.accessor, MIME, makeFilename);
}
getState() {
return Promise.resolve(this.configState(GSheetsConfig, this.config));
}
}
//# sourceMappingURL=gsheets.js.map