UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

268 lines • 11 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 { 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