@stackmemoryai/stackmemory
Version:
Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a
327 lines (326 loc) • 9.28 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 { LinearClient } from "../../linear/client.js";
import { logger } from "../../../core/monitoring/logger.js";
class LinearHandlers {
constructor(deps) {
this.deps = deps;
}
/**
* Create an authenticated LinearClient from the auth manager token
*/
async getClient() {
const token = await this.deps.linearAuthManager.getValidToken();
return new LinearClient({ apiKey: token, useBearer: true });
}
/**
* Sync tasks with Linear
*/
async handleLinearSync(args) {
try {
const { direction = "both", force = false } = args;
try {
await this.deps.linearAuthManager.getValidToken();
} catch {
return {
content: [
{
type: "text",
text: "Linear auth required. Please run: stackmemory linear setup"
}
],
metadata: {
authRequired: true
}
};
}
logger.info("Starting Linear sync", { direction, force });
const result = await this.deps.linearSync.sync();
const syncText = `Linear Sync Complete:
- To Linear: ${result.synced.toLinear} tasks
- From Linear: ${result.synced.fromLinear} tasks
- Updated: ${result.synced.updated} tasks
- Errors: ${result.errors.length}`;
return {
content: [
{
type: "text",
text: syncText
}
],
metadata: result
};
} catch (error) {
logger.error(
"Linear sync failed",
error instanceof Error ? error : new Error(String(error))
);
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage?.includes("unauthorized") || errorMessage?.includes("auth")) {
return {
content: [
{
type: "text",
text: "Linear authentication failed. Please run: stackmemory linear setup"
}
],
metadata: {
authError: true
}
};
}
throw error;
}
}
/**
* Update Linear issue directly via GraphQL API
*/
async handleLinearUpdateTask(args) {
try {
const { linear_id, status, assignee_id, priority, labels } = args;
if (!linear_id) {
throw new Error("Linear ID is required");
}
const client = await this.getClient();
const updateData = {};
if (status) updateData.stateId = status;
if (assignee_id) updateData.assigneeId = assignee_id;
if (priority !== void 0) updateData.priority = priority;
if (labels) {
updateData.labelIds = Array.isArray(labels) ? labels : [labels];
}
const issue = await client.updateIssue(linear_id, updateData);
return {
content: [
{
type: "text",
text: `Updated ${issue.identifier}: ${issue.title}
Status: ${issue.state.name} | Priority: ${issue.priority}`
}
],
metadata: {
id: issue.id,
identifier: issue.identifier,
state: issue.state.name,
url: issue.url
}
};
} catch (error) {
logger.error(
"Error updating Linear task",
error instanceof Error ? error : new Error(String(error))
);
throw error;
}
}
/**
* Get issues from Linear via GraphQL API
*/
async handleLinearGetTasks(args) {
try {
const { team_id, assignee_id, state = "active", limit = 20 } = args;
const client = await this.getClient();
const stateTypeMap = {
active: "started",
closed: "completed",
all: void 0
};
const issues = await client.getIssues({
teamId: team_id,
assigneeId: assignee_id,
stateType: stateTypeMap[state],
limit
});
const issueLines = issues.map(
(i) => `${i.identifier} [${i.state.name}] ${i.title}${i.assignee ? ` (@${i.assignee.name})` : ""}`
);
const text = issues.length > 0 ? `Found ${issues.length} issues:
${issueLines.join("\n")}` : "No issues found matching filters.";
return {
content: [{ type: "text", text }],
metadata: {
count: issues.length,
issues: issues.map((i) => ({
id: i.id,
identifier: i.identifier,
title: i.title,
state: i.state.name,
priority: i.priority,
assignee: i.assignee?.name,
url: i.url
}))
}
};
} catch (error) {
logger.error(
"Error getting Linear tasks",
error instanceof Error ? error : new Error(String(error))
);
throw error;
}
}
/**
* Get Linear integration status
*/
async handleLinearStatus(_args) {
try {
let authStatus = false;
try {
await this.deps.linearAuthManager.getValidToken();
authStatus = true;
} catch {
authStatus = false;
}
if (!authStatus) {
return {
content: [
{
type: "text",
text: "Linear: Not connected\nRun: stackmemory linear setup"
}
],
metadata: {
connected: false,
authRequired: true
}
};
}
const statusText = "Linear Integration Status:\n\u2713 Connected (authenticated)\n\nUse `stackmemory linear sync` for full sync details.";
return {
content: [
{
type: "text",
text: statusText
}
],
metadata: {
connected: true
}
};
} catch (error) {
logger.error(
"Error getting Linear status",
error instanceof Error ? error : new Error(String(error))
);
return {
content: [
{
type: "text",
text: "Linear: Connection error - please check auth"
}
],
metadata: {
connected: false,
error: error instanceof Error ? error.message : String(error)
}
};
}
}
/**
* Create a comment on a Linear issue
*/
async handleLinearCreateComment(args) {
try {
const { issue_id, body } = args;
if (!issue_id || !body) {
throw new Error("issue_id and body are required");
}
const client = await this.getClient();
const comment = await client.createComment(issue_id, body);
return {
content: [
{
type: "text",
text: `Comment created on ${issue_id}
ID: ${comment.id}
Preview: ${body.slice(0, 100)}${body.length > 100 ? "..." : ""}`
}
],
metadata: {
id: comment.id,
issueId: issue_id,
createdAt: comment.createdAt
}
};
} catch (error) {
logger.error(
"Error creating Linear comment",
error instanceof Error ? error : new Error(String(error))
);
throw error;
}
}
/**
* Update an existing comment on a Linear issue
*/
async handleLinearUpdateComment(args) {
try {
const { comment_id, body } = args;
if (!comment_id || !body) {
throw new Error("comment_id and body are required");
}
const client = await this.getClient();
const comment = await client.updateComment(comment_id, body);
return {
content: [
{
type: "text",
text: `Comment ${comment_id} updated
Preview: ${body.slice(0, 100)}${body.length > 100 ? "..." : ""}`
}
],
metadata: {
id: comment.id,
updatedAt: comment.updatedAt
}
};
} catch (error) {
logger.error(
"Error updating Linear comment",
error instanceof Error ? error : new Error(String(error))
);
throw error;
}
}
/**
* List comments on a Linear issue
*/
async handleLinearListComments(args) {
try {
const { issue_id } = args;
if (!issue_id) {
throw new Error("issue_id is required");
}
const client = await this.getClient();
const comments = await client.getComments(issue_id);
const lines = comments.map(
(c) => `${c.id.slice(0, 8)} | ${c.user?.name ?? "unknown"} | ${new Date(c.createdAt).toISOString().slice(0, 10)} | ${c.body.slice(0, 60).replace(/\n/g, " ")}${c.body.length > 60 ? "..." : ""}`
);
const text = comments.length > 0 ? `${comments.length} comments on ${issue_id}:
${lines.join("\n")}` : `No comments on ${issue_id}`;
return {
content: [{ type: "text", text }],
metadata: {
count: comments.length,
comments: comments.map((c) => ({
id: c.id,
author: c.user?.name,
createdAt: c.createdAt,
bodyPreview: c.body.slice(0, 200)
}))
}
};
} catch (error) {
logger.error(
"Error listing Linear comments",
error instanceof Error ? error : new Error(String(error))
);
throw error;
}
}
}
export {
LinearHandlers
};