@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
125 lines • 5.66 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 { record, recordStream, startOp } from "../../observe/context.js";
import { FileType, VFSName } from "../../types.js";
import { eisdir, enoent } from "../../errors/fs.js";
import { sha1Hex } from "../../utils/hash.js";
import { stripSlash } from "../../utils/slash.js";
import { fetchBlob, streamBlob } from "./client.js";
import { BLOB_TEXT } from "./constants.js";
import { graphqlData } from "./repo.js";
const ENC = new TextEncoder();
/**
* Read a file's blob and record the sha it was fetched by.
*
* The sha comes from the file's row, as its folder's listing names it. The
* blob endpoint is content-addressed, so the recorded sha names exactly the
* bytes returned however old the listing is. That is also the documented
* limit of `read: fresh` here: a file read for the first time comes from the
* listing, and the next read's probe corrects it.
*
* Mirrors Python's `read`.
*/
export async function read(accessor, path, entry) {
const sha = blobSha(path, entry);
const timer = startOp();
const data = await fetchBlob(accessor.account.transport, accessor.owner, accessor.repo, sha);
record('read', path.virtual, VFSName.GITHUB, data.length, timer, { fingerprint: sha });
return data;
}
/**
* Several files' blobs in one GraphQL query.
*
* Each blob is asked by the sha its row names, as `read` asks it. GraphQL
* hands a blob over as text, so a file is kept only when that text, encoded
* as UTF-8, hashes to the sha the way git hashes a blob, which proves the
* bytes are the ones a read returns. A binary, truncated or non-UTF-8 blob, a
* file past `limit` bytes, and a path with no file row are left out for
* their own read. Each file kept is recorded as `read` records it. Mirrors
* Python's `read_many`.
*/
export async function readMany(accessor, paths, limit, handed = []) {
const asked = new Map();
for (const [i, path] of paths.entries()) {
const entry = handed[i]?.entry ?? null;
if (entry === null || entry.type === FileType.DIRECTORY || (entry.size ?? 0) > limit)
continue;
asked.set(`f${String(asked.size)}`, [path.virtual, String(entry.extra.sha)]);
}
const answers = new Map();
if (asked.size === 0)
return answers;
const aliases = [...asked.keys()];
const declared = aliases.map((alias) => `, $${alias}: GitObjectID!`).join('');
const fields = aliases
.map((alias) => `${alias}: object(oid: $${alias}) { ${BLOB_TEXT} }`)
.join(' ');
const timer = startOp();
const data = await graphqlData(accessor.account.transport, `query($owner: String!, $repo: String!${declared}) ` +
`{ repository(owner: $owner, name: $repo) { ${fields} } }`, {
owner: accessor.owner,
repo: accessor.repo,
...Object.fromEntries([...asked].map(([alias, [, sha]]) => [alias, sha])),
});
const blobs = (data.repository ?? {});
for (const [alias, [virtual, sha]] of asked) {
const blob = blobs[alias];
if (blob === null || blob === undefined || blob.isTruncated === true)
continue;
if (typeof blob.text !== 'string')
continue;
const body = ENC.encode(blob.text);
if (body.byteLength > limit)
continue;
const header = ENC.encode(`blob ${String(body.byteLength)}\0`);
const framed = new Uint8Array(header.byteLength + body.byteLength);
framed.set(header);
framed.set(body, header.byteLength);
if ((await sha1Hex(framed)) !== sha)
continue;
record('read', virtual, VFSName.GITHUB, body.byteLength, timer, { fingerprint: sha });
answers.set(virtual, body);
}
return answers;
}
/**
* Stream a file's blob as it downloads, recorded by its sha. The blob comes
* as GitHub's raw media type, the bytes themselves rather than base64 inside
* JSON. The sha is the row's, as `read` takes it, and recorded up front: the
* blob endpoint is content-addressed, so it names the bytes however far the
* stream is read. Mirrors Python's `read_stream`.
*/
export async function* readStream(accessor, path, entry) {
const sha = blobSha(path, entry);
const rec = recordStream('read', path.virtual, VFSName.GITHUB, { fingerprint: sha });
const { account, owner, repo } = accessor;
for await (const chunk of streamBlob(account.transport, owner, repo, sha)) {
if (rec !== null)
rec.bytes += chunk.byteLength;
yield chunk;
}
if (rec !== null)
rec.complete = true;
}
/** The blob sha of the file at `path`, off its row. Mirrors Python's `_blob_sha`. */
function blobSha(path, entry) {
if (stripSlash(path.mountPath) === '')
throw eisdir(path.virtual);
if (entry === undefined || entry === null)
throw enoent(path.virtual);
if (entry.type === FileType.DIRECTORY)
throw eisdir(path.virtual);
return entry.extra.sha;
}
//# sourceMappingURL=read.js.map