@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
141 lines (135 loc) • 5.24 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 { GmailAccessor } from "../../accessor/gmail.js";
import { buildDeltaHook } from "../../core/gmail/events.js";
import { read as gmailRead } from "../../core/gmail/read.js";
import { readdir as gmailReaddir } from "../../core/gmail/readdir.js";
import { filesContaining as gmailFilesContaining } from "../../core/gmail/search.js";
import { stat as gmailStat } from "../../core/gmail/stat.js";
import { TokenManager } from "../../core/google/client.js";
import { VFSName } from "../../types.js";
import { BaseVFS } from "../base.js";
import { GmailConfig } from "./config.js";
export const VFS_MD = `\`\`\`text
{prefix}
<label>/
<yyyy-mm-dd>/
<subject>__<message-id>.gmail.json # the email, body included
<subject>__<message-id>/ # its attachments, if any
<attachment-filename>
\`\`\`
- \`<label>\` is a Gmail label: \`INBOX\`, \`SENT\`, \`DRAFT\`, \`IMPORTANT\`,
\`STARRED\`, \`TRASH\`, \`SPAM\` or any user label.
- \`<yyyy-mm-dd>\` is the date the message was received. Listing one
(\`ls {prefix}/INBOX/2026-05-03/\`) pushes \`after:\`/\`before:\` into the
Gmail query, much cheaper than scanning the whole label.
- \`<subject>\` is sanitized: \`ls\` the date directory rather than
construct it. \`<message-id>\` is the Gmail message id.
- The email body is in the \`.gmail.json\` file; read it with \`cat\`,
\`head\` or \`jq\`, keeping the suffix. The sibling directory, the same
name without the extension, holds attachments only: \`cat\` one to
download its bytes. \`grep -r\` over a date directory skips these binary
files.
- A \`grep\` or \`rg\` hit names the \`.gmail.json\` file, not the attachments
directory. To list a message's attachments, \`ls\` its path with
\`.gmail.json\` stripped; \`ENOENT\` means it has none:
\`ls {prefix}/INBOX/2026-05-03/<subject>__<message-id>/\`
A \`.gmail.json\` is processed by Mirage, not the raw Gmail API response:
\`\`\`jsonc
{
"id": "...",
"thread_id": "...",
"from": { "name": "...", "email": "..." },
"to": [ { "name": "...", "email": "..." } ],
"cc": [ ... ],
"subject": "...",
"date": "Mon, 3 May 2026 10:00:00 -0700",
"body_text": "decoded plain-text body",
"snippet": "first ~200 chars from Gmail",
"labels": [ "INBOX", "IMPORTANT", ... ],
"attachments": [
{ "id": "...", "filename": "invoice.pdf",
"mime_type": "application/pdf", "size": 12345 }
]
}
\`\`\`
Useful \`jq\` paths:
\`\`\`jq
.subject
.from.email
.body_text
.labels[]
.attachments[] | .filename
\`\`\`
To act on messages (read by id, triage, raw API calls), use the \`gws\`
CLI if installed (\`gws gmail --help\`).`;
export class GmailVFS extends BaseVFS {
name = VFSName.GMAIL;
cachesReads = true;
// Every listed file carries an exact size: .gmail.json is rendered at
// readdir from the full message the listing already fetched, and
// attachments carry the decoded byte count.
sizesAlwaysKnown = true;
indexTtl = 86_400;
vfsMd = VFS_MD;
// The listing keeps each message's text; an attachment is read as usual.
searchable = ['*/*/*.gmail.json'];
handed = {
readdir: ['entry'],
read: ['entry'],
stat: ['entry', 'parent'],
};
config;
accessor;
constructor(config) {
super();
this.config = config;
const tm = new TokenManager(config);
this.accessor = new GmailAccessor({
tokenManager: tm,
config,
capped: () => this.walkLimit >= 0,
});
}
readdir(path, handed) {
return gmailReaddir(this.accessor, path, handed?.entry);
}
async read(path, handed) {
return gmailRead(this.accessor, path, handed?.entry);
}
deltaHook() {
return buildDeltaHook(this.accessor);
}
stat(path, handed) {
return gmailStat(this.accessor, path, handed?.entry, handed?.parent);
}
/**
* 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 (this.walkLimit < 0)
return Promise.resolve(null);
return gmailFilesContaining(this.accessor, text, [path]);
}
getState() {
return Promise.resolve(this.configState(GmailConfig, this.config));
}
}
//# sourceMappingURL=gmail.js.map