UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

170 lines • 7.54 kB
// ========= 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