UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

190 lines • 8.88 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 { ContentType, FileStat, FileType } from "../../types.js"; import { keptText, unsearchedText } from "../../utils/record_search.js"; import { stringOf as pickString } from "../../utils/records.js"; import { cutListing, makeReaddir } from "../hierarchy/readdir.js"; import { entryStat } from "../hierarchy/stat.js"; import { jsonBytes, jsonlBytesByCreatedAt } from "../render/json.js"; import { listIssueComments, listTeamCycles, listTeamDocuments, listTeamIssues, listTeamMembers, listTeamProjects, listTeams, } from "./client.js"; import { normalizeComment, normalizeCycle, normalizeDocument, normalizeIssue, normalizeProject, normalizeTeam, normalizeUser, projectIssueRows, SEARCHED_FIELDS, } from "./normalize.js"; import { cycleFilename, documentFilename, issueDirname, memberFilename, projectFilename, teamDirname, } from "./pathing.js"; import { detectScope } from "./scope.js"; export const TEAM_DIRS = ['members', 'issues', 'projects', 'cycles', 'documents']; function extraSize(entry) { const value = entry.extra.json_size; return typeof value === 'number' ? value : null; } function extraString(entry, key) { const value = entry.extra[key]; return typeof value === 'string' ? value : ''; } /** The teams the mount shows: all of them, or those in `teamIds`. Mirrors Python's `filtered_teams`. */ export async function filteredTeams(accessor) { let teams = await listTeams(accessor.transport); if (accessor.teamIds !== null && accessor.teamIds.length > 0) { const allowed = new Set(accessor.teamIds); teams = teams.filter((t) => allowed.has(pickString(t, 'id'))); } return teams; } export const TEAM = entryStat('team_id', FileType.DIRECTORY); export const TEAM_JSON = entryStat('team_id', ContentType.JSON); export const MEMBER = entryStat('member_id', ContentType.JSON); export const ISSUE = entryStat('issue_id', FileType.DIRECTORY); export const ISSUE_JSON = entryStat('issue_id', ContentType.JSON); export const COMMENTS = entryStat('issue_id', ContentType.TEXT); export const PROJECT = entryStat('project_id', ContentType.JSON); export const CYCLE = entryStat('cycle_id', ContentType.JSON); export const DOCUMENT = entryStat('document_id', ContentType.JSON); async function listTeamsDir(accessor, _match) { // team.json renders the team object this listing already fetched, so its // exact size rides the directory's row, and the key/name ride along for // the project renders below it. return (await filteredTeams(accessor)).map((team) => TEAM(teamDirname(team), pickString(team, 'id'), { modified: pickString(team, 'updatedAt'), extra: { team_key: pickString(team, 'key'), team_name: pickString(team, 'name'), json_size: jsonBytes(normalizeTeam(team)).length, }, })); } function listTeam(_accessor, match, entry) { const rows = [ TEAM_JSON('team.json', match.slots.team_id ?? '', { size: extraSize(entry), modified: entry.modified, }), ]; for (const name of TEAM_DIRS) { // A project render carries its team's key and name, which only the // team listing knows. const extra = name === 'projects' ? { team_key: extraString(entry, 'team_key'), team_name: extraString(entry, 'team_name') } : {}; rows.push(new FileStat({ name, type: FileType.DIRECTORY, extra })); } return Promise.resolve(rows); } async function listMembers(accessor, match, _entry) { const users = await listTeamMembers(accessor.transport, match.slots.team_id ?? '', match.limit); const rows = users.map((user) => { const rendered = jsonBytes(normalizeUser(user)); return MEMBER(memberFilename(user), pickString(user, 'id'), { size: rendered.length, modified: pickString(user, 'updatedAt'), extra: keptText(rendered), }); }); return cutListing(rows, match.limit); } async function listIssues(accessor, match, _entry) { const issues = await listTeamIssues(accessor.transport, match.slots.team_id ?? '', match.limit); const rows = issues.map((issue) => ISSUE(issueDirname(issue), pickString(issue, 'id'), { modified: pickString(issue, 'updatedAt'), extra: { issue_key: pickString(issue, 'identifier'), json_size: jsonBytes(normalizeIssue(issue)).length, ...(accessor.capped() ? { unsearched: unsearchedText(Object.entries(normalizeIssue(issue)), SEARCHED_FIELDS) } : {}), }, })); return cutListing(rows, match.limit); } async function listIssue(accessor, match, entry) { // issue.json renders the issue the team listing already fetched; // comments.jsonl costs the one bounded comments call, paid only when // this directory is entered. const issueId = match.slots.issue_id ?? ''; const issueKey = extraString(entry, 'issue_key'); const comments = await listIssueComments(accessor.transport, issueId); const rendered = comments.map((c) => normalizeComment(c, issueId, issueKey !== '' ? issueKey : null)); let commentsTime = ''; for (const row of rendered) { const updated = typeof row.updated_at === 'string' ? row.updated_at : ''; if (updated > commentsTime) commentsTime = updated; } return [ ISSUE_JSON('issue.json', issueId, { size: extraSize(entry), modified: entry.modified }), COMMENTS('comments.jsonl', issueId, { size: jsonlBytesByCreatedAt(rendered).length, modified: commentsTime !== '' ? commentsTime : entry.modified, }), ]; } async function listProjects(accessor, match, entry) { const teamId = match.slots.team_id ?? ''; const projects = await listTeamProjects(accessor.transport, teamId, match.limit); const teamIssues = await listTeamIssues(accessor.transport, teamId, undefined, projects.map((project) => pickString(project, 'id'))); const rows = projects.map((project) => { const projectId = pickString(project, 'id'); const projectIssues = projectIssueRows(teamIssues, projectId); const rendered = jsonBytes(normalizeProject(project, { teamId, teamKey: extraString(entry, 'team_key') || null, teamName: extraString(entry, 'team_name') || null, issues: projectIssues, })); return PROJECT(projectFilename(project), projectId, { size: rendered.length, modified: pickString(project, 'updatedAt'), extra: keptText(rendered), }); }); return cutListing(rows, match.limit); } async function listCycles(accessor, match, _entry) { const teamId = match.slots.team_id ?? ''; const cycles = await listTeamCycles(accessor.transport, teamId, match.limit); const rows = cycles.map((cycle) => { const rendered = jsonBytes(normalizeCycle(cycle, teamId)); return CYCLE(cycleFilename(cycle), pickString(cycle, 'id'), { size: rendered.length, modified: pickString(cycle, 'updatedAt'), extra: keptText(rendered), }); }); return cutListing(rows, match.limit); } async function listDocuments(accessor, match, _entry) { const documents = await listTeamDocuments(accessor.transport, match.slots.team_id ?? '', match.limit); const rows = documents.map((document) => { const rendered = jsonBytes(normalizeDocument(document)); return DOCUMENT(documentFilename(document), pickString(document, 'id'), { size: rendered.length, modified: pickString(document, 'updatedAt'), extra: keptText(rendered), }); }); return cutListing(rows, match.limit); } export const readdir = makeReaddir(detectScope, { listers: { teams: listTeamsDir, }, entryListers: { team: listTeam, members: listMembers, issues: listIssues, issue: listIssue, projects: listProjects, cycles: listCycles, documents: listDocuments, }, staticRoot: ['teams'], }); //# sourceMappingURL=readdir.js.map