@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
110 lines • 5.42 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 { chunks } from "../../io/cooperative.js";
import { dayChannelId, followedDay, guardDay, readDays } from "../time_range.js";
import { enoent } from "../../errors/fs.js";
import { makeRead, makeReadRange, perKind } from "../hierarchy/read.js";
import { fetchMessagesForDay, getHistoryJsonl, streamMessagesForDay, } from "./history.js";
import { refusalsDenied } from "./client.js";
import { getUserProfile, userJsonBytes } from "./users.js";
import { detectScope } from "./scope.js";
async function readChat(accessor, match, path, entry, parent) {
await guardDay(accessor, match, path.virtual);
const channelId = dayChannelId(path, entry, parent);
return refusalsDenied(path, () => getHistoryJsonl(accessor, channelId, match.slots.day ?? ''));
}
async function readUser(accessor, _match, path, entry) {
if (entry === null)
throw enoent(path);
const user = await getUserProfile(accessor, String(entry.extra.user_id));
return userJsonBytes(user);
}
async function blobUrl(accessor, path, entry) {
await guardDay(accessor, detectScope(path), path.virtual);
if (entry === null)
throw enoent(path);
const url = typeof entry.extra.url_private_download === 'string' ? entry.extra.url_private_download : '';
if (url === '')
throw enoent(path);
return url;
}
async function downloadBlob(accessor, url, offset, size) {
if (accessor.transport.downloadFile === undefined) {
throw new Error('slack transport does not support file downloads');
}
return accessor.transport.downloadFile(url, offset, size);
}
async function readBlob(accessor, _match, path, entry) {
return downloadBlob(accessor, await blobUrl(accessor, path, entry), 0, null);
}
async function readBlobRange(accessor, _match, path, entry, _parent, offset, size) {
return downloadBlob(accessor, await blobUrl(accessor, path, entry), offset, size);
}
export const read = makeRead(detectScope, {
messages: readChat,
user: readUser,
file_blob: readBlob,
});
// Only an uploaded file has a remote range to ask for. A channel's history
// and a user profile are rendered here into JSON, so their bytes do not
// exist until we make them and the window can only be taken afterwards.
export const readRange = makeReadRange(detectScope, read, {
file_blob: readBlobRange,
});
/** Stream attachments; preserve the rendered order of JSON records. */
export async function* readStream(accessor, path, entry = null, parent = null, signal) {
if (detectScope(path).kind === 'file_blob') {
const url = await blobUrl(accessor, path, entry);
const source = accessor.transport.downloadFileStream === undefined
? await downloadBlob(accessor, url, 0, null)
: accessor.transport.downloadFileStream(url, signal);
yield* chunks(source, signal);
}
else {
yield* chunks(await read(accessor, path, entry, parent), signal);
}
}
/**
* A channel or DM day's last `n` messages, then each one posted to it, asked for every
* `interval` seconds while the day lasts (`followedDay`). Mirrors Python's
* `_follow_chat`.
*/
async function followChat(accessor, match, path, n, interval, entry = null, parent = null) {
await guardDay(accessor, match, path.virtual);
const channelId = dayChannelId(path, entry, parent);
const day = match.slots.day ?? '';
const [, end] = accessor.timeRange.dayBounds(day);
return followedDay(await refusalsDenied(path, () => fetchMessagesForDay(accessor, channelId, day)), n, end, interval, (last) => refusalsDenied(path, () => fetchMessagesForDay(accessor, channelId, day, last === null ? undefined : last.ts)));
}
export const follow = perKind(detectScope, { messages: followChat });
/** A run of a channel's days, fetched as one. Mirrors Python's `_fetch_run`. */
function runPages(accessor, channelId, first, last) {
return streamMessagesForDay(accessor, channelId, first, { lastDay: last });
}
/** A message's place among its day's lines. Mirrors Python's `_sort_key`. */
function sortKey(message) {
return Number.parseFloat(message.ts);
}
/** Whether a message was posted in a day's bounds. Mirrors Python's `_posted_within`. */
function postedWithin(message, start, end) {
return Number(message.ts) >= start && Number(message.ts) < end;
}
/**
* Several chat.jsonl files, one history walk per run of a channel's
* consecutive days (`readDays`). Mirrors Python's `read_many`.
*/
export function readMany(accessor, paths, limit, handed = []) {
return readDays(accessor.timeRange, paths, handed, detectScope, (channelId, first, last) => runPages(accessor, channelId, first, last), sortKey, postedWithin, limit);
}
//# sourceMappingURL=read.js.map