@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
170 lines • 7.54 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 { guardDay } from "../time_range.js";
import { ContentType, DirListing, FileStat, FileType } from "../../types.js";
import { contentTypeForMime } from "../../utils/filetype.js";
import { cutListing, makeReaddir } from "../hierarchy/readdir.js";
import { listChannels } from "./channels.js";
import { refusalsDenied } from "./client.js";
import { channelEntry, guildEntry, historyEntry, memberEntry, snowflakeToDate } from "./entry.js";
import { fileBlobName, isListed } from "./files.js";
import { listGuilds } from "./guilds.js";
import { listedMessagesForDay } from "./history.js";
import { listMembers } from "./members.js";
import { jsonlBytes } from "../render/json.js";
import { detectScope } from "./scope.js";
import { globSpan, hasGlobSpan } from "../../utils/glob_walk.js";
/**
* The channel's day directories, newest first.
*
* A day dir is real for any well-formed date under the channel, so the bare
* listing is a window: the last `days` up to the newest message. A glob names
* its own window instead, clipped at the newest message because nothing was
* posted after it.
*/
export function dateRangeDescending(endDate, days = 30, span = null) {
const [y, m, d] = endDate.split('-').map((n) => Number.parseInt(n, 10));
if (y === undefined || m === undefined || d === undefined)
return [];
const dayMs = 86_400_000;
let end = Date.UTC(y, m - 1, d);
let count = days;
if (span !== null) {
const first = Date.parse(`${span[0]}T00:00:00Z`);
end = Math.min(end, Date.parse(`${span[1]}T00:00:00Z`) - dayMs);
count = Math.floor((end - first) / dayMs) + 1;
if (count <= 0)
return [];
}
const dates = [];
for (let i = 0; i < count; i++) {
const cursor = new Date(end - i * dayMs);
const yy = cursor.getUTCFullYear().toString().padStart(4, '0');
const mm = (cursor.getUTCMonth() + 1).toString().padStart(2, '0');
const dd = cursor.getUTCDate().toString().padStart(2, '0');
dates.push(`${yy}-${mm}-${dd}`);
}
return dates;
}
function containerEntry(name, guildId) {
return new FileStat({ name, type: FileType.DIRECTORY, extra: { guild_id: guildId } });
}
async function listRoot(accessor, _match) {
return (await listGuilds(accessor)).map(guildEntry);
}
function listGuildContainers(_accessor, _match, own) {
const guildId = String(own.extra.guild_id);
return Promise.resolve([containerEntry('channels', guildId), containerEntry('members', guildId)]);
}
async function listChannelsDir(accessor, _match, own) {
return (await listChannels(accessor, String(own.extra.guild_id))).map(channelEntry);
}
async function listMembersDir(accessor, match, own) {
const members = await listMembers(accessor, String(own.extra.guild_id), 1000, match.limit);
const rows = members.filter((m) => m.user !== undefined && m.user.id !== '').map(memberEntry);
return cutListing(rows, match.limit);
}
function listChannelDays(accessor, match, own) {
const channelId = String(own.extra.channel_id);
const lastMsgId = typeof own.extra.last_message_id === 'string' ? own.extra.last_message_id : '';
const endDate = lastMsgId !== '' ? snowflakeToDate(lastMsgId) : new Date().toISOString().slice(0, 10);
const span = globSpan(match.pattern);
const dates = accessor.timeRange.bounded
? accessor.timeRange.listingDays(snowflakeToDate(channelId), endDate, span)
: dateRangeDescending(endDate, 30, span);
return Promise.resolve(new DirListing({
entries: dates.map((d) => historyEntry(channelId, d)),
partial: span !== null,
window: true,
}));
}
/** One history fetch, answering the day dir and its files subdir. */
async function dayListing(accessor, channelId, dateStr) {
const messages = await listedMessagesForDay(accessor, channelId, dateStr);
// The day's messages are already in hand, so chat.jsonl's exact rendered
// size is free here; read() renders the same messages the same way.
const blobs = [];
for (const msg of messages) {
const atts = (msg.attachments ?? []);
for (const att of atts) {
if (!isListed(att))
continue;
const mimetype = att.content_type ?? '';
blobs.push(new FileStat({
name: fileBlobName(att),
type: FileType.FILE,
content: contentTypeForMime(mimetype),
size: att.size,
extra: {
attachment_id: att.id,
content_type: mimetype,
url: att.url,
proxy_url: att.proxy_url ?? '',
message_id: msg.id,
author: msg.author?.username ?? '',
channel_id: channelId,
date: dateStr,
},
}));
}
}
return new DirListing({
entries: [
new FileStat({
name: 'chat.jsonl',
type: FileType.FILE,
content: ContentType.TEXT,
size: jsonlBytes(messages).byteLength,
extra: { channel_id: channelId },
}),
new FileStat({
name: 'files',
type: FileType.DIRECTORY,
extra: { channel_id: channelId, date: dateStr },
}),
],
seeds: new Map([['files', blobs]]),
});
}
function listDay(accessor, match, own) {
// The day's row carries its channel: one the bounded window lists, or one
// stat makes for any well-formed date under a real channel, so a date
// outside the window fetches too.
return dayListing(accessor, String(own.extra.channel_id), match.slots.day ?? '');
}
export async function listFiles(accessor, match, own) {
// Normally served from the day lister's seed; reached only when the index
// evicted the files listing while the day's rows survived.
const listing = await dayListing(accessor, String(own.extra.channel_id), match.slots.day ?? '');
return [...(listing.seeds.get('files') ?? [])];
}
const listing = makeReaddir(detectScope, {
listers: { root: listRoot },
entryListers: {
guild: listGuildContainers,
channels_dir: listChannelsDir,
members_dir: listMembersDir,
channel: listChannelDays,
day: listDay,
files: listFiles,
},
guards: { day: guardDay, files: guardDay },
patternKinds: { channel: hasGlobSpan },
leafError: 'enotdir',
});
/** List a Discord directory; a channel Discord will not show is EACCES. */
export function readdir(accessor, pathSpec, entry = null, parent = null, limit) {
return refusalsDenied(pathSpec, () => listing(accessor, pathSpec, entry, parent, limit));
}
//# sourceMappingURL=readdir.js.map