UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

251 lines • 10.7 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 { epochToIso } from "../../utils/dates.js"; import { contentTypeForMime } from "../../utils/filetype.js"; import { cutListing, makeReaddir } from "../hierarchy/readdir.js"; import { jsonlBytes } from "../render/json.js"; import { listChannels, listDms } from "./channels.js"; import { refusalsDenied } from "./client.js"; import { channelDirname, dmDirname, fileBlobName, userFilename } from "./formatters.js"; import { fetchMessagesForDay } from "./history.js"; import { cursorPages } from "./paginate.js"; import { detectScope } from "./scope.js"; import { listUsers, userJsonBytes } from "./users.js"; import { globSpan, hasGlobSpan } from "../../utils/glob_walk.js"; export const VIRTUAL_ROOTS = ['channels', 'dms', 'users']; export async function latestMessageTs(accessor, channelId) { const data = await accessor.transport.call('conversations.history', { channel: channelId, limit: '1', }); const messages = data.messages ?? []; if (messages.length === 0) return null; return Number.parseFloat(messages[0]?.ts ?? '0'); } /** * Discover the history start when conversation metadata omits creation. * * Only history before the scope's end is paged: a message after it is out * of scope, and the first message overall precedes the end whenever * anything in scope exists at all. */ async function earliestMessageTs(accessor, channelId, latest) { const params = { channel: channelId, limit: '200' }; if (accessor.timeRange.end !== null) params.latest = accessor.timeRange.end.toFixed(6); let first = latest; for await (const page of cursorPages(accessor.transport, 'conversations.history', params, 'messages')) { for (const message of page) first = Math.min(first, Number.parseFloat(message.ts)); } return first; } /** * The channel's day directories, newest first. * * A day dir is real for any date the channel has existed for, so the bare * listing is a window: the last `maxDays` up to the newest message. A glob * names its own window instead, and then the cap does not apply, because the * span is already bounded by what was typed. */ export function dateRange(latestTs, created, maxDays = 90, span = null) { const endMs = Math.floor(latestTs * 1000); const startMs = Math.floor(created * 1000); const endDate = new Date(endMs); const startDate = new Date(startMs); let endUtc = Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()); let startUtc = Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()); const dayMs = 86_400_000; if (span !== null) { startUtc = Math.max(startUtc, Date.parse(`${span[0]}T00:00:00Z`)); endUtc = Math.min(endUtc, Date.parse(`${span[1]}T00:00:00Z`) - dayMs); } else if ((endUtc - startUtc) / dayMs > maxDays) { startUtc = endUtc - (maxDays - 1) * dayMs; } const dates = []; for (let cursor = endUtc; cursor >= startUtc; cursor -= dayMs) { const d = new Date(cursor); const yyyy = d.getUTCFullYear().toString().padStart(4, '0'); const mm = (d.getUTCMonth() + 1).toString().padStart(2, '0'); const dd = d.getUTCDate().toString().padStart(2, '0'); dates.push(`${yyyy}-${mm}-${dd}`); } return dates; } function slackModified(remoteTime) { if (remoteTime === null || remoteTime === undefined || remoteTime === '' || remoteTime === 0) { return null; } const ts = typeof remoteTime === 'number' ? remoteTime : Number.parseFloat(remoteTime); if (Number.isNaN(ts) || ts <= 0) return null; return epochToIso(ts); } function conversation(dirname, convo, name) { const created = convo.created ?? 0; return new FileStat({ name: dirname, type: FileType.DIRECTORY, modified: slackModified(created), extra: { channel_id: convo.id, name, created }, }); } async function listChannelsRoot(accessor, match) { const channels = await listChannels(accessor, { stopAt: match.limit }); return cutListing(channels.map((ch) => conversation(channelDirname(ch), ch, ch.name ?? '')), match.limit); } async function listDmsRoot(accessor, _match) { const dms = await listDms(accessor); const users = await listUsers(accessor); const userMap = {}; for (const u of users) userMap[u.id] = u.name ?? u.id; return dms.map((dm) => { const uid = dm.user ?? ''; return conversation(dmDirname(dm, userMap), dm, userMap[uid] ?? uid); }); } async function listUsersRoot(accessor, match) { const users = await listUsers(accessor, { stopAt: match.limit }); const rows = users.map((u) => new FileStat({ name: userFilename(u), type: FileType.FILE, content: ContentType.JSON, size: userJsonBytes(u).byteLength, extra: { user_id: u.id }, })); return cutListing(rows, match.limit); } async function listChannelDays(accessor, match, own) { const channelId = String(own.extra.channel_id); const created = Number(own.extra.created ?? 0) || 0; const span = globSpan(match.pattern); const latestTs = await latestMessageTs(accessor, channelId); let dates; if (latestTs !== null && accessor.timeRange.bounded) { const start = created || accessor.timeRange.start; let first; if (start !== null) first = new Date(start * 1000).toISOString().slice(0, 10); else if (span !== null) first = span[0]; else { const earliest = await earliestMessageTs(accessor, channelId, latestTs); first = new Date(earliest * 1000).toISOString().slice(0, 10); } dates = accessor.timeRange .listingDays(first, new Date(latestTs * 1000).toISOString().slice(0, 10), span) .reverse(); } else if (latestTs !== null && created > 0) { dates = dateRange(latestTs, created, 90, span); } else if (latestTs !== null) { dates = dateRange(latestTs, Math.floor(latestTs), 90, span); } else { dates = []; } const rows = dates.map((d) => new FileStat({ name: d, type: FileType.DIRECTORY, extra: { channel_id: channelId } })); return new DirListing({ entries: rows, 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 fetchMessagesForDay(accessor, channelId, dateStr); const blobs = []; for (const msg of messages) { const files = msg.files ?? []; for (const fmeta of files) { const meta = fmeta; // Tombstoned (deleted) and access-restricted file payloads carry an // id but no download URL and no byte size; read() ENOENTs on them, so // listing them would both surface phantom files and break the // sizesAlwaysKnown contract. if (meta.id === undefined || meta.id === '') continue; if (meta.size === undefined || !meta.url_private_download) continue; const mimetype = meta.mimetype ?? ''; blobs.push(new FileStat({ name: fileBlobName(meta), type: FileType.FILE, content: contentTypeForMime(mimetype), size: meta.size, modified: slackModified(meta.timestamp), extra: { file_id: meta.id, mimetype, url_private_download: meta.url_private_download ?? '', filetype: meta.filetype ?? '', ts: typeof msg.ts === 'string' ? msg.ts : '', 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: { channels_root: listChannelsRoot, dms_root: listDmsRoot, users_root: listUsersRoot, }, entryListers: { channel: listChannelDays, day: listDay, files: listFiles, }, staticRoot: VIRTUAL_ROOTS, guards: { day: guardDay, files: guardDay }, patternKinds: { channel: hasGlobSpan }, }); /** List a Slack directory; a conversation Slack 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