@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
214 lines (213 loc) • 5.62 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { logger } from "../../core/monitoring/logger.js";
import { IntegrationError, ErrorCode } from "../../core/errors/index.js";
class LinearRestClient {
apiKey;
baseUrl = "https://api.linear.app/graphql";
taskCache = /* @__PURE__ */ new Map();
lastSync = 0;
cacheTTL = 5 * 60 * 1e3;
// 5 minutes
constructor(apiKey) {
this.apiKey = apiKey;
}
/**
* Get all tasks and store in memory
*/
async getAllTasks(forceRefresh = false) {
const now = Date.now();
if (!forceRefresh && now - this.lastSync < this.cacheTTL && this.taskCache.size > 0) {
return Array.from(this.taskCache.values());
}
try {
const allTasks = [];
let hasNextPage = true;
let cursor;
while (hasNextPage) {
const query = `
query($after: String) {
issues(
filter: { team: { key: { eq: "ENG" } } }
first: 100
after: $after
) {
nodes {
id
identifier
title
description
state {
name
type
}
priority
assignee {
id
name
}
estimate
createdAt
updatedAt
url
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const variables = cursor ? { after: cursor } : {};
const response = await this.makeRequest(
query,
variables
);
const tasks = response.data.issues.nodes;
allTasks.push(...tasks);
tasks.forEach((task) => {
this.taskCache.set(task.id, task);
});
hasNextPage = response.data.issues.pageInfo.hasNextPage;
cursor = response.data.issues.pageInfo.endCursor;
logger.info(`Fetched ${tasks.length} tasks, total: ${allTasks.length}`);
}
this.lastSync = now;
logger.info(`Cached ${allTasks.length} Linear tasks in memory`);
return allTasks;
} catch (error) {
logger.error("Failed to fetch Linear tasks:", error);
return Array.from(this.taskCache.values());
}
}
/**
* Get tasks by status
*/
async getTasksByStatus(status) {
const tasks = await this.getAllTasks();
return tasks.filter((task) => task.state.type === status);
}
/**
* Get tasks assigned to current user
*/
async getMyTasks() {
try {
const viewer = await this.getViewer();
const tasks = await this.getAllTasks();
return tasks.filter((task) => task.assignee?.id === viewer.id);
} catch (error) {
logger.error("Failed to get assigned tasks:", error);
return [];
}
}
/**
* Get task count by status
*/
async getTaskCounts() {
const tasks = await this.getAllTasks();
const counts = {};
tasks.forEach((task) => {
const status = task.state.type;
counts[status] = (counts[status] || 0) + 1;
});
return counts;
}
/**
* Search tasks by title or description
*/
async searchTasks(query) {
const tasks = await this.getAllTasks();
const searchTerm = query.toLowerCase();
return tasks.filter(
(task) => task.title.toLowerCase().includes(searchTerm) || task.description?.toLowerCase().includes(searchTerm) || task.identifier.toLowerCase().includes(searchTerm)
);
}
/**
* Get current viewer info
*/
async getViewer() {
const query = `
query {
viewer {
id
name
email
}
}
`;
const response = await this.makeRequest(query);
return response.data.viewer;
}
/**
* Get team info
*/
async getTeam() {
const query = `
query {
teams(filter: { key: { eq: "ENG" } }, first: 1) {
nodes {
id
name
key
}
}
}
`;
const response = await this.makeRequest(query);
if (response.data.teams.nodes.length === 0) {
throw new IntegrationError(
"ENG team not found",
ErrorCode.LINEAR_API_ERROR
);
}
return response.data.teams.nodes[0];
}
/**
* Get cache stats
*/
getCacheStats() {
const now = Date.now();
return {
size: this.taskCache.size,
lastSync: this.lastSync,
age: now - this.lastSync,
fresh: now - this.lastSync < this.cacheTTL
};
}
/**
* Clear cache
*/
clearCache() {
this.taskCache.clear();
this.lastSync = 0;
logger.info("Linear task cache cleared");
}
/**
* Make GraphQL request
*/
async makeRequest(query, variables = {}) {
const response = await fetch(this.baseUrl, {
method: "POST",
headers: {
Authorization: this.apiKey,
"Content-Type": "application/json"
},
body: JSON.stringify({ query, variables })
});
const result = await response.json();
if (!response.ok || result.errors) {
const errorMsg = result.errors?.[0]?.message || `${response.status} ${response.statusText}`;
throw new IntegrationError(
`Linear API error: ${errorMsg}`,
ErrorCode.LINEAR_API_ERROR
);
}
return result;
}
}
export {
LinearRestClient
};
//# sourceMappingURL=rest-client.js.map