@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
268 lines • 11 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 { enoent } from "../../errors/fs.js";
import { apiRequest } from "../api/client.js";
import { ApiError } from "../api/errors.js";
import { COMMENT_CREATE_MUTATION, COMMENT_UPDATE_MUTATION, ISSUE_COMMENTS_QUERY, ISSUE_CREATE_MUTATION, ISSUE_LOOKUP_QUERY, ISSUE_QUERY, ISSUE_SEARCH_QUERY, ISSUE_UPDATE_MUTATION, TEAM_CYCLES_QUERY, TEAM_DOCUMENTS_QUERY, TEAM_ISSUES_QUERY, TEAM_LABELS_QUERY, TEAM_LIST_QUERY, TEAM_MEMBERS_QUERY, TEAM_PROJECTS_QUERY, USER_LOOKUP_QUERY, } from "./queries.js";
export class LinearApiError extends ApiError {
errors;
constructor(message, errors = [], status = null) {
super(message, status);
this.errors = errors;
this.name = 'LinearApiError';
}
}
function errorMessage(errors) {
if (errors === undefined || errors.length === 0)
return null;
const first = errors[0];
return first?.message ?? null;
}
function errorOf(response, text) {
let errors = [];
try {
errors = JSON.parse(text).errors ?? [];
}
catch {
// a non-JSON error body: fall through to the bare status code
}
const msg = errorMessage(errors) ?? `Linear API error: HTTP ${String(response.status)}`;
return new LinearApiError(msg, errors, response.status);
}
export class HttpLinearTransport {
fetch = globalThis.fetch.bind(globalThis);
apiKey;
baseUrl;
timeout;
constructor(opts) {
this.apiKey = opts.apiKey;
this.baseUrl = opts.baseUrl ?? 'https://api.linear.app/graphql';
this.timeout = opts.timeout;
}
async graphql(query, variables = {}) {
const data = ((await apiRequest('POST', this.baseUrl, {
fetchFn: this.fetch,
timeout: this.timeout,
headers: { Authorization: this.apiKey, 'Content-Type': 'application/json' },
json: { query, variables },
errorOf: errorOf,
})) ?? {});
if (data.errors !== undefined && data.errors.length > 0) {
const msg = errorMessage(data.errors) ?? 'Linear API error';
throw new LinearApiError(msg, data.errors);
}
return data.data ?? {};
}
}
function navigate(root, path) {
let cursor = root;
for (const key of path) {
if (cursor === null || typeof cursor !== 'object') {
throw new LinearApiError(`unexpected GraphQL shape at ${path.join('.')}`);
}
cursor = cursor[key];
}
if (cursor === null || typeof cursor !== 'object') {
throw new LinearApiError(`unexpected GraphQL shape at ${path.join('.')}`);
}
return cursor;
}
async function paginateConnection(transport, query, variables, path, limit) {
const merged = { first: 50, after: null, ...variables };
const nodes = [];
for (;;) {
const data = await transport.graphql(query, merged);
const conn = navigate(data, path);
for (const item of conn.nodes)
nodes.push(item);
if (!conn.pageInfo.hasNextPage)
return nodes;
if (limit !== undefined && nodes.length >= limit)
return nodes;
merged.after = conn.pageInfo.endCursor;
}
}
export async function listTeams(transport) {
return paginateConnection(transport, TEAM_LIST_QUERY, {}, ['teams']);
}
export async function listTeamMembers(transport, teamId, limit) {
return paginateConnection(transport, TEAM_MEMBERS_QUERY, { teamId }, ['team', 'members'], limit);
}
export async function listTeamIssues(transport, teamId, limit, projects) {
if (projects?.length === 0)
return [];
const variables = projects === undefined ? { teamId } : { teamId, filter: { project: { id: { in: projects } } } };
return paginateConnection(transport, TEAM_ISSUES_QUERY, variables, ['team', 'issues'], limit);
}
export async function listTeamProjects(transport, teamId, limit) {
return paginateConnection(transport, TEAM_PROJECTS_QUERY, { teamId }, ['team', 'projects'], limit);
}
export async function listTeamCycles(transport, teamId, limit) {
return paginateConnection(transport, TEAM_CYCLES_QUERY, { teamId }, ['team', 'cycles'], limit);
}
export async function listTeamLabels(transport, teamId) {
return paginateConnection(transport, TEAM_LABELS_QUERY, { teamId }, ['team', 'labels']);
}
export async function listTeamDocuments(transport, teamId, limit) {
return paginateConnection(transport, TEAM_DOCUMENTS_QUERY, { teamId }, ['documents'], limit);
}
export async function resolveTeam(transport, keyOrId) {
const teams = await listTeams(transport);
for (const team of teams) {
if (team.id === keyOrId || team.key === keyOrId)
return team;
}
throw enoent(keyOrId);
}
export async function getIssue(transport, issueId) {
const data = await transport.graphql(ISSUE_QUERY, { issueId });
const issue = data.issue;
if (issue === null || issue === undefined || typeof issue !== 'object') {
throw new LinearApiError(`issue not found: ${issueId}`);
}
return issue;
}
export async function listIssueComments(transport, issueId) {
return paginateConnection(transport, ISSUE_COMMENTS_QUERY, { issueId }, ['issue', 'comments']);
}
export async function resolveIssueId(transport, issueId, issueKey) {
if (issueId !== null && issueId !== '')
return issueId;
if (issueKey === null || issueKey === '') {
throw new Error('issue id or issue key is required');
}
const dash = issueKey.indexOf('-');
if (dash === -1)
throw new Error(`invalid issue key: ${issueKey}`);
const teamKey = issueKey.slice(0, dash);
const numberStr = issueKey.slice(dash + 1);
if (!/^\d+$/.test(numberStr))
throw new Error(`invalid issue key: ${issueKey}`);
const data = await transport.graphql(ISSUE_LOOKUP_QUERY, {
teamKey,
number: Number.parseFloat(numberStr),
});
const issuesField = data.issues;
const nodes = issuesField !== null && typeof issuesField === 'object'
? issuesField.nodes
: undefined;
const first = nodes?.[0];
if (first === undefined)
throw enoent(issueKey);
const id = first.id;
if (typeof id !== 'string')
throw enoent(issueKey);
return id;
}
export async function resolveUserId(transport, assigneeId, assigneeEmail) {
if (assigneeId !== null && assigneeId !== '')
return assigneeId;
if (assigneeEmail === null || assigneeEmail === '') {
throw new Error('assignee id or assignee email is required');
}
const data = await transport.graphql(USER_LOOKUP_QUERY, { email: assigneeEmail });
const usersField = data.users;
const nodes = usersField !== null && typeof usersField === 'object'
? usersField.nodes
: undefined;
const first = nodes?.[0];
if (first === undefined)
throw enoent(assigneeEmail);
const id = first.id;
if (typeof id !== 'string')
throw enoent(assigneeEmail);
return id;
}
export async function issueCreate(transport, input) {
if (input.teamId === '') {
throw new Error('teamId is required');
}
const payload = { title: input.title, teamId: input.teamId };
if (input.description !== undefined && input.description !== null && input.description !== '') {
payload.description = input.description;
}
const data = await transport.graphql(ISSUE_CREATE_MUTATION, { input: payload });
const created = data.issueCreate;
const id = created?.issue?.id;
if (id === undefined)
throw new LinearApiError('issueCreate returned no id');
return getIssue(transport, id);
}
export async function issueUpdate(transport, input) {
const payload = {};
if (input.title !== undefined && input.title !== null)
payload.title = input.title;
if (input.description !== undefined && input.description !== null) {
payload.description = input.description;
}
if (input.stateId !== undefined && input.stateId !== null)
payload.stateId = input.stateId;
if (input.assigneeId !== undefined && input.assigneeId !== null) {
payload.assigneeId = input.assigneeId;
}
if (input.priority !== undefined && input.priority !== null)
payload.priority = input.priority;
if (input.projectId !== undefined && input.projectId !== null) {
payload.projectId = input.projectId;
}
if (input.labelIds !== undefined && input.labelIds !== null) {
payload.labelIds = [...input.labelIds];
}
if (Object.keys(payload).length === 0) {
throw new Error('no updates provided');
}
await transport.graphql(ISSUE_UPDATE_MUTATION, { id: input.issueId, input: payload });
return getIssue(transport, input.issueId);
}
export async function commentCreate(transport, issueId, body) {
await transport.graphql(COMMENT_CREATE_MUTATION, { input: { issueId, body } });
const comments = await listIssueComments(transport, issueId);
const last = comments[comments.length - 1];
if (last === undefined) {
throw new LinearApiError('comment was created but no comments were returned');
}
return last;
}
export async function commentUpdate(transport, commentId, body) {
const data = await transport.graphql(COMMENT_UPDATE_MUTATION, {
id: commentId,
input: { body },
});
const updated = data.commentUpdate;
const comment = updated?.comment ?? {};
const issueId = comment.issue?.id;
if (issueId !== undefined) {
const comments = await listIssueComments(transport, issueId);
for (const item of comments) {
if (item.id === commentId)
return item;
}
}
return comment;
}
export async function searchIssues(transport, query, limit = 50) {
const data = await transport.graphql(ISSUE_SEARCH_QUERY, { term: query, first: limit });
const search = data.searchIssues;
const nodes = search !== null && typeof search === 'object'
? search.nodes
: undefined;
const valid = Array.isArray(nodes) &&
nodes.every((node) => node !== null &&
typeof node === 'object' &&
typeof node.id === 'string');
if (!valid)
throw new LinearApiError('unexpected response from searchIssues');
return nodes;
}
//# sourceMappingURL=client.js.map