@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
106 lines (104 loc) • 4.63 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 { AirtableAccessor } from "../../accessor/airtable.js";
import { AirtableConfig } from "../../core/airtable/config.js";
import { buildDeltaHook } from "../../core/airtable/events.js";
import { firstLines as airtableFirstLines, readStream as airtableStream, } from "../../core/airtable/read.js";
import { readdir as airtableReaddir } from "../../core/airtable/readdir.js";
import { linesContaining as airtableLinesContaining } from "../../core/airtable/search.js";
import { stat as airtableStat } from "../../core/airtable/stat.js";
import { VFSName } from "../../types.js";
import { BaseVFS } from "../base.js";
export const VFS_MD = `\`\`\`text
{prefix}
bases/
<base-name>__<base-id>/
base.json # base id, name, permission, tables
<table-name>__<table-id>/
table.json # typed fields (with options), views
records.jsonl # one record per line
views/
<view-name>__<view-id>.jsonl # records the view shows, in order
\`\`\`
- \`ls\` a directory first to find exact names: the name part of a segment
is sanitized, the id after the last \`__\` is exact.
- A \`records.jsonl\` line is a normalized record: \`record_id\`,
\`created_time\` and \`fields\`, keyed by field name with values as
Airtable returns them. \`table.json\` gives each field's type.
- Airtable omits empty cells, so a missing key means empty, a \`false\`
checkbox included. A link field is a list of record ids; an attachment
URL expires two hours after it was read.
- \`records.jsonl\` lists in the API's own order; a view file applies the
view's filter and sort.
- A file over \`max_read_records\` records stops with \`File too large\`
after that many, while \`head -n N\` fetches just N records.
- The \`airtable\` CLI, if installed, reaches what a file cannot:
server-side filters (\`airtable record list --formula\` or \`--view\`), one
record by id (\`airtable record get\`) and comments
(\`airtable comment list\`).
\`\`\`sh
head -n 20 {prefix}/bases/<base>/<table>/records.jsonl | jq .fields
\`\`\``;
/**
* Airtable bases as directories, tables as records.jsonl files. Records are
* live data another client may edit at any moment, so reads are never served
* from the file cache; the schema listings still ride the index for its TTL.
* The transport is plain fetch, so one class serves node and the browser.
*/
export class AirtableVFS extends BaseVFS {
name = VFSName.AIRTABLE;
cachesReads = false;
// records.jsonl and the view files render a paged read, so their size is
// unknown until the bytes exist.
sizesAlwaysKnown = false;
supportsSnapshot = false;
vfsMd = VFS_MD;
accessor;
handed = {
readdir: ['entry'],
read: ['entry', 'parent'],
stat: ['entry'],
linesContaining: ['entry'],
firstLines: ['entry', 'parent'],
};
config;
constructor(config, options = {}) {
super();
this.config = config;
this.accessor = new AirtableAccessor(config, options);
}
readdir(path, handed) {
return airtableReaddir(this.accessor, path, handed?.entry);
}
// eslint-disable-next-line @typescript-eslint/require-await
async read(path, handed) {
return airtableStream(this.accessor, path, handed?.entry, handed?.parent);
}
stat(path, handed) {
return airtableStat(this.accessor, path, handed?.entry);
}
linesContaining(path, text, _opts, handed) {
return airtableLinesContaining(this.accessor, path, text, handed?.entry);
}
firstLines(path, n, handed) {
return airtableFirstLines(this.accessor, path, n, handed?.entry, handed?.parent);
}
deltaHook() {
return buildDeltaHook(this.accessor);
}
getState() {
return this.configState(AirtableConfig, this.config);
}
}
//# sourceMappingURL=airtable.js.map