anya/tools/linear-manager.ts

1240 lines
40 KiB
TypeScript
Raw Permalink Normal View History

2025-01-09 16:27:26 +05:30
import { z } from "zod";
import { zodFunction } from ".";
import { LinearClient } from "@linear/sdk";
import { Message } from "../interfaces/message";
import { userConfigs } from "../config";
import { ask } from "./ask";
import { RunnableToolFunction } from "openai/lib/RunnableFunction.mjs";
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
// Parameter Schemas
export const IssueParams = z.object({
teamId: z.string(),
title: z.string(),
description: z.string().optional(),
assigneeId: z.string().optional(),
2025-01-12 20:05:43 +05:30
projectId: z.string().optional(),
2025-01-09 16:27:26 +05:30
priority: z.number().optional(),
labelIds: z.array(z.string()).optional(),
});
export const UpdateIssueParams = z.object({
issueId: z.string().describe("The ID of the issue to update"),
2025-01-12 20:05:43 +05:30
// Basic fields
2025-01-09 16:27:26 +05:30
title: z.string().optional().describe("The issue title"),
2025-01-12 20:05:43 +05:30
description: z.string().optional().describe("The issue description in markdown format"),
descriptionData: z.any().optional().describe("The issue description as a Prosemirror document"),
priority: z.number().min(0).max(4).optional()
.describe("The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"),
// Assignee and subscribers
assigneeId: z.string().optional().describe("The identifier of the user to assign the issue to"),
subscriberIds: z.array(z.string()).optional().describe("The identifiers of the users subscribing to this ticket"),
// Labels
labelIds: z.array(z.string()).optional()
.describe("The complete set of label IDs to set on the issue (replaces existing labels)"),
addedLabelIds: z.array(z.string()).optional()
2025-01-09 16:27:26 +05:30
.describe("The identifiers of the issue labels to be added to this issue"),
2025-01-12 20:05:43 +05:30
removedLabelIds: z.array(z.string()).optional()
.describe("The identifiers of the issue labels to be removed from this issue"),
// Status and workflow
stateId: z.string().optional().describe("The team state of the issue"),
estimate: z.number().optional().describe("The estimated complexity of the issue"),
// Dates and scheduling
dueDate: z.string().optional().describe("The date at which the issue is due (YYYY-MM-DD format)"),
snoozedById: z.string().optional().describe("The identifier of the user who snoozed the issue"),
snoozedUntilAt: z.string().optional().describe("The time until an issue will be snoozed in Triage view"),
// Relationships
parentId: z.string().optional().describe("The identifier of the parent issue"),
projectId: z.string().optional().describe("The project associated with the issue"),
projectMilestoneId: z.string().optional().describe("The project milestone associated with the issue"),
teamId: z.string().optional().describe("The identifier of the team associated with the issue"),
cycleId: z.string().optional().describe("The cycle associated with the issue"),
// Sorting and positioning
sortOrder: z.number().optional().describe("The position of the issue related to other issues"),
boardOrder: z.number().optional().describe("The position of the issue in its column on the board view"),
subIssueSortOrder: z.number().optional().describe("The position of the issue in parent's sub-issue list"),
prioritySortOrder: z.number().optional().describe("[ALPHA] The position of the issue when ordered by priority"),
// Templates and automation
lastAppliedTemplateId: z.string().optional().describe("The ID of the last template applied to the issue"),
autoClosedByParentClosing: z.boolean().optional()
.describe("Whether the issue was automatically closed because its parent issue was closed"),
trashed: z.boolean().optional().describe("Whether the issue has been trashed"),
2025-01-09 16:27:26 +05:30
});
export const GetIssueParams = z.object({
issueId: z.string(),
});
export const SearchIssuesParams = z.object({
query: z.string().describe("Search query string"),
teamId: z.string().optional(),
limit: z.number().max(5).describe("Number of results to return (default: 1)"),
});
export const ListTeamsParams = z.object({
limit: z.number().max(20).describe("Number of teams to return (default 3)"),
});
2025-01-12 20:05:43 +05:30
// Add these type definitions before the parameter schemas
export const StringComparator = z.object({
eq: z.string().optional(),
neq: z.string().optional(),
in: z.array(z.string()).optional(),
nin: z.array(z.string()).optional(),
contains: z.string().optional(),
notContains: z.string().optional(),
startsWith: z.string().optional(),
notStartsWith: z.string().optional(),
endsWith: z.string().optional(),
notEndsWith: z.string().optional(),
containsIgnoreCase: z.string().optional(),
notContainsIgnoreCase: z.string().optional(),
startsWithIgnoreCase: z.string().optional(),
notStartsWithIgnoreCase: z.string().optional(),
endsWithIgnoreCase: z.string().optional(),
notEndsWithIgnoreCase: z.string().optional(),
});
export const DateComparator = z.object({
eq: z.string().optional(),
neq: z.string().optional(),
gt: z.string().optional(),
gte: z.string().optional(),
lt: z.string().optional(),
lte: z.string().optional(),
in: z.array(z.string()).optional(),
nin: z.array(z.string()).optional(),
});
export const NumberComparator = z.object({
eq: z.number().optional(),
neq: z.number().optional(),
gt: z.number().optional(),
gte: z.number().optional(),
lt: z.number().optional(),
lte: z.number().optional(),
in: z.array(z.number()).optional(),
nin: z.array(z.number()).optional(),
});
export const IdComparator = z.object({
eq: z.string().optional(),
neq: z.string().optional(),
in: z.array(z.string()).optional(),
nin: z.array(z.string()).optional(),
});
export const WorkflowStateFilter = z.object({
createdAt: DateComparator.optional(),
description: StringComparator.optional(),
id: IdComparator.optional(),
name: StringComparator.optional(),
position: NumberComparator.optional(),
type: StringComparator.optional(),
updatedAt: DateComparator.optional(),
});
2025-01-09 16:27:26 +05:30
export const AdvancedSearchIssuesParams = z.object({
2025-01-12 20:05:43 +05:30
// Text search
query: z.string().optional().describe("Search in title and description"),
title: z.string().optional().describe("Filter by exact or partial title match"),
description: z.string().optional().describe("Filter by description content"),
// Basic filters
teamId: z.string().optional().describe("Filter by team ID"),
assigneeId: z.string().optional().describe("Filter by assignee user ID"),
creatorId: z.string().optional().describe("Filter by creator user ID"),
priority: z.number().min(0).max(4).optional()
.describe("0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"),
// Status and state
stateId: z.string().optional().describe("Filter by specific workflow state ID (simplified)"),
// Dates
createdAfter: z.string().optional().describe("Issues created after this ISO datetime"),
createdBefore: z.string().optional().describe("Issues created before this ISO datetime"),
updatedAfter: z.string().optional().describe("Issues updated after this ISO datetime"),
updatedBefore: z.string().optional().describe("Issues updated before this ISO datetime"),
completedAfter: z.string().optional().describe("Issues completed after this ISO datetime"),
completedBefore: z.string().optional().describe("Issues completed before this ISO datetime"),
dueDate: z.string().optional().describe("Filter by due date (YYYY-MM-DD format)"),
dueDateAfter: z.string().optional().describe("Due date after (YYYY-MM-DD format)"),
dueDateBefore: z.string().optional().describe("Due date before (YYYY-MM-DD format)"),
startedAfter: z.string().optional().describe("Issues started after this ISO datetime"),
startedBefore: z.string().optional().describe("Issues started before this ISO datetime"),
// Relationships
projectId: z.string().optional().describe("Filter by project ID"),
parentId: z.string().optional().describe("Filter by parent issue ID"),
subscriberId: z.string().optional().describe("Filter by subscriber user ID"),
hasBlockedBy: z.boolean().optional().describe("Issues that are blocked by others"),
hasBlocking: z.boolean().optional().describe("Issues that are blocking others"),
hasDuplicates: z.boolean().optional().describe("Issues that have duplicates"),
// Labels and estimates
labelIds: z.array(z.string()).optional().describe("Filter by one or more label IDs"),
estimate: z.number().optional().describe("Filter by issue estimate points"),
// Other filters
number: z.number().optional().describe("Filter by issue number"),
snoozedById: z.string().optional().describe("Filter by user who snoozed the issue"),
snoozedUntilAfter: z.string().optional().describe("Issues snoozed until after this ISO datetime"),
snoozedUntilBefore: z.string().optional().describe("Issues snoozed until before this ISO datetime"),
// Result options
orderBy: z.enum(["createdAt", "updatedAt", "priority", "dueDate"])
2025-01-09 16:27:26 +05:30
.optional()
2025-01-12 20:05:43 +05:30
.describe("Sort order for results"),
limit: z.number().max(10)
.describe("Number of results to return (default: 2, max: 10)"),
2025-01-09 16:27:26 +05:30
});
2025-01-12 20:05:43 +05:30
// Modify SearchUsersParams schema to allow more specific search parameters
2025-01-09 16:27:26 +05:30
export const SearchUsersParams = z.object({
2025-01-12 20:05:43 +05:30
email: z.string().optional().describe("Search by exact email address"),
displayName: z.string().optional().describe("Search by display name"),
2025-01-09 16:27:26 +05:30
limit: z
.number()
.max(10)
.describe("Number of results to return (default: 5)"),
2025-01-12 20:05:43 +05:30
}).refine(
data => (data.email && !data.displayName) || (!data.email && data.displayName),
{
message: "Provide either email OR displayName, not both"
}
);
2025-01-09 16:27:26 +05:30
// Add new Project Parameter Schemas
export const ProjectParams = z.object({
name: z.string().describe("The name of the project"),
teamIds: z
.array(z.string())
.describe("The identifiers of the teams this project is associated with"),
description: z
.string()
.optional()
.describe("The description for the project"),
content: z.string().optional().describe("The project content as markdown"),
color: z.string().optional().describe("The color of the project"),
icon: z.string().optional().describe("The icon of the project"),
leadId: z.string().optional().describe("The identifier of the project lead"),
memberIds: z
.array(z.string())
.optional()
.describe("The identifiers of the members of this project"),
priority: z
.number()
.min(0)
.max(4)
.optional()
.describe(
"The priority of the project. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"
),
sortOrder: z
.number()
.optional()
.describe("The sort order for the project within shared views"),
prioritySortOrder: z
.number()
.optional()
.describe(
"[ALPHA] The sort order for the project within shared views, when ordered by priority"
),
startDate: z
.string()
.optional()
.describe("The planned start date of the project (TimelessDate format)"),
targetDate: z
.string()
.optional()
.describe("The planned target date of the project (TimelessDate format)"),
statusId: z.string().optional().describe("The ID of the project status"),
state: z
.string()
.optional()
.describe("[DEPRECATED] The state of the project"),
id: z.string().optional().describe("The identifier in UUID v4 format"),
convertedFromIssueId: z
.string()
.optional()
.describe("The ID of the issue from which that project is created"),
lastAppliedTemplateId: z
.string()
.optional()
.describe("The ID of the last template applied to the project"),
});
export const UpdateProjectParams = z.object({
projectId: z.string().describe("The ID of the project to update"),
name: z.string().optional(),
description: z.string().optional(),
state: z
.enum(["planned", "started", "paused", "completed", "canceled"])
.optional(),
startDate: z.string().optional(),
targetDate: z.string().optional(),
sortOrder: z.number().optional(),
icon: z.string().optional(),
});
export const GetProjectParams = z.object({
projectId: z.string(),
});
export const SearchProjectsParams = z.object({
2025-01-12 20:05:43 +05:30
// Text search
query: z.string().optional().describe("Search in project name and content"),
name: z.string().optional().describe("Filter by exact or partial project name"),
// Basic filters
teamId: z.string().optional().describe("Filter by team ID"),
creatorId: z.string().optional().describe("Filter by creator user ID"),
leadId: z.string().optional().describe("Filter by lead user ID"),
priority: z.number().min(0).max(4).optional()
.describe("0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low"),
// Status and state
health: z.string().optional().describe("Filter by project health status"),
state: z.string().optional().describe("[DEPRECATED] Filter by project state"),
status: z.string().optional().describe("Filter by project status ID"),
// Dates
startDate: z.string().optional().describe("Filter by start date"),
targetDate: z.string().optional().describe("Filter by target date"),
createdAfter: z.string().optional().describe("Projects created after this ISO datetime"),
createdBefore: z.string().optional().describe("Projects created before this ISO datetime"),
updatedAfter: z.string().optional().describe("Projects updated after this ISO datetime"),
updatedBefore: z.string().optional().describe("Projects updated before this ISO datetime"),
completedAfter: z.string().optional().describe("Projects completed after this ISO datetime"),
completedBefore: z.string().optional().describe("Projects completed before this ISO datetime"),
canceledAfter: z.string().optional().describe("Projects canceled after this ISO datetime"),
canceledBefore: z.string().optional().describe("Projects canceled before this ISO datetime"),
// Relationships
hasBlockedBy: z.boolean().optional().describe("Projects that are blocked by others"),
hasBlocking: z.boolean().optional().describe("Projects that are blocking others"),
hasRelated: z.boolean().optional().describe("Projects that have related items"),
hasViolatedDependencies: z.boolean().optional().describe("Projects with violated dependencies"),
// Result options
orderBy: z.enum(["createdAt", "updatedAt", "priority", "targetDate"])
.optional()
.describe("Sort order for results"),
limit: z.number().max(10).describe("Number of results to return (default: 1)"),
2025-01-09 16:27:26 +05:30
});
// Add new ListProjectsParams schema after other params
export const ListProjectsParams = z.object({
teamId: z.string().optional().describe("Filter projects by team ID"),
limit: z
.number()
.max(20)
.describe("Number of projects to return (default: 10)"),
state: z
.enum(["planned", "started", "paused", "completed", "canceled"])
.optional()
.describe("Filter projects by state"),
});
2025-01-12 20:05:43 +05:30
// Add after other parameter schemas
export const CreateCommentParams = z.object({
issueId: z.string().describe("The ID of the issue to comment on"),
body: z.string().describe("The comment text in markdown format"),
});
export const ListCommentsParams = z.object({
issueId: z.string().describe("The ID of the issue to get comments from"),
limit: z.number().max(20).describe("Number of comments to return (default: 10)"),
});
// Add new document parameter schemas
export const CreateDocumentParams = z.object({
title: z.string().describe("The title of the document"),
content: z.string().describe("The content of the document in markdown format"),
icon: z.string().optional().describe("The icon of the document"),
organizationId: z.string().optional().describe("The organization ID"),
projectId: z.string().optional().describe("The project ID to link the document to"),
});
export const UpdateDocumentParams = z.object({
documentId: z.string().describe("The ID of the document to update"),
title: z.string().optional().describe("The new title of the document"),
content: z.string().optional().describe("The new content in markdown format"),
icon: z.string().optional().describe("The new icon of the document"),
});
export const GetDocumentParams = z.object({
documentId: z.string().describe("The ID of the document to retrieve"),
});
export const SearchDocumentsParams = z.object({
query: z.string().describe("Search query string"),
projectId: z.string().optional().describe("Filter by project ID"),
limit: z.number().max(10).describe("Number of results to return (default: 5)"),
});
2025-01-09 16:27:26 +05:30
interface SimpleIssue {
2025-01-12 20:05:43 +05:30
id: string; // The internal UUID of the issue (e.g., "123e4567-e89b-12d3-a456-426614174000")
identifier: string; // The human-readable identifier (e.g., "XCE-205")
2025-01-09 16:27:26 +05:30
title: string;
status: string;
2025-01-12 20:05:43 +05:30
statusId: string;
2025-01-09 16:27:26 +05:30
priority: number;
assignee?: string;
dueDate?: string;
labels?: string[];
}
interface SimpleTeam {
id: string;
name: string;
key: string;
}
interface SimpleUser {
id: string;
name: string;
email: string;
displayName?: string;
avatarUrl?: string;
}
interface SimpleProject {
id: string;
name: string;
state: string;
startDate?: string;
targetDate?: string;
description?: string;
teamIds: string[];
priority?: number;
leadId?: string;
memberIds?: string[];
color?: string;
icon?: string;
statusId?: string;
}
2025-01-12 20:05:43 +05:30
// Add new document interface
interface SimpleDocument {
id: string;
title: string;
content: string;
icon?: string;
url: string;
createdAt: string;
updatedAt: string;
}
// Add after other interfaces
interface SimpleComment {
id: string;
body: string;
user?: {
id: string;
name: string;
};
createdAt: string;
}
2025-01-09 16:27:26 +05:30
function formatIssue(issue: any): SimpleIssue {
return {
id: issue.id,
2025-01-12 20:05:43 +05:30
identifier: issue.identifier,
2025-01-09 16:27:26 +05:30
title: issue.title,
status: issue.state?.name || "Unknown",
2025-01-12 20:05:43 +05:30
statusId: issue.state?.id,
2025-01-09 16:27:26 +05:30
priority: issue.priority,
assignee: issue.assignee?.name,
dueDate: issue.dueDate,
2025-01-12 20:05:43 +05:30
labels: issue.labels?.nodes?.map((l: any) => ({
name: l.name,
id: l.id
})) || [],
2025-01-09 16:27:26 +05:30
};
}
// Add after existing formatIssue function
function formatProject(project: any): SimpleProject {
return {
id: project.id,
name: project.name,
state: project.state,
startDate: project.startDate,
targetDate: project.targetDate,
description: project.description,
teamIds: project.teams?.nodes?.map((t: any) => t.id) || [],
priority: project.priority,
leadId: project.lead?.id,
memberIds: project.members?.nodes?.map((m: any) => m.id) || [],
color: project.color,
icon: project.icon,
statusId: project.status?.id,
};
}
2025-01-12 20:05:43 +05:30
// Add new document formatting function
function formatDocument(doc: any): SimpleDocument {
return {
id: doc.id,
title: doc.title,
content: doc.content,
icon: doc.icon,
url: doc.url,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
}
// Add after other formatting functions
function formatComment(comment: any): SimpleComment {
return {
id: comment.id,
body: comment.body,
user: comment.user ? {
id: comment.user.id,
name: comment.user.name,
} : undefined,
createdAt: comment.createdAt,
};
}
2025-01-09 16:27:26 +05:30
// API Functions
async function createIssue(
client: LinearClient,
params: z.infer<typeof IssueParams>
) {
try {
return await client.createIssue(params);
} catch (error) {
return `Error: ${error}`;
}
}
async function updateIssue(
client: LinearClient,
params: z.infer<typeof UpdateIssueParams>
) {
try {
const { issueId, ...updateData } = params;
2025-01-12 20:05:43 +05:30
// Create a new object for the properly typed data
const formattedData: any = { ...updateData };
// Convert date strings to proper format if provided
if (formattedData.dueDate) {
// Due date should be YYYY-MM-DD format
formattedData.dueDate = formattedData.dueDate.split('T')[0];
}
if (formattedData.snoozedUntilAt) {
// Convert to Date object for the API
formattedData.snoozedUntilAt = new Date(formattedData.snoozedUntilAt);
}
// Remove any undefined values to avoid API errors
Object.keys(formattedData).forEach(key => {
if (formattedData[key] === undefined) {
delete formattedData[key];
}
});
return await client.updateIssue(issueId, formattedData);
2025-01-09 16:27:26 +05:30
} catch (error) {
return `Error: ${error}`;
}
}
async function getIssue(
client: LinearClient,
{ issueId }: z.infer<typeof GetIssueParams>
) {
try {
return await client.issue(issueId);
} catch (error) {
return `Error: ${error}`;
}
}
async function searchIssues(
client: LinearClient,
{ query, teamId, limit }: z.infer<typeof SearchIssuesParams>
) {
try {
const searchParams: any = { first: limit };
if (teamId) {
searchParams.filter = { team: { id: { eq: teamId } } };
}
const issues = await client.issues({
...searchParams,
filter: {
or: [
{ title: { containsIgnoreCase: query } },
{ description: { containsIgnoreCase: query } },
],
},
});
return issues.nodes.map(formatIssue);
} catch (error) {
return `Error: ${error}`;
}
}
async function listTeams(
client: LinearClient,
{ limit }: z.infer<typeof ListTeamsParams>
) {
try {
const teams = await client.teams({ first: limit });
return teams.nodes.map(
(team): SimpleTeam => ({
id: team.id,
name: team.name,
key: team.key,
})
);
} catch (error) {
return `Error: ${error}`;
}
}
async function advancedSearchIssues(
client: LinearClient,
params: z.infer<typeof AdvancedSearchIssuesParams>
) {
try {
const filter: any = {};
2025-01-12 20:05:43 +05:30
// Text search filters
2025-01-09 16:27:26 +05:30
if (params.query) {
filter.or = [
{ title: { containsIgnoreCase: params.query } },
{ description: { containsIgnoreCase: params.query } },
];
}
2025-01-12 20:05:43 +05:30
if (params.title) filter.title = { containsIgnoreCase: params.title };
if (params.description) filter.description = { containsIgnoreCase: params.description };
// Basic filters
if (params.teamId) filter.team = { id: { eq: params.teamId } };
if (params.assigneeId) filter.assignee = { id: { eq: params.assigneeId } };
if (params.creatorId) filter.creator = { id: { eq: params.creatorId } };
if (params.priority !== undefined) filter.priority = { eq: params.priority };
// Status and state
if (params.stateId) {
filter.state = { id: { eq: params.stateId } };
}
// Date filters
if (params.createdAfter) filter.createdAt = { gt: params.createdAfter };
if (params.createdBefore) filter.createdAt = { lt: params.createdBefore };
if (params.updatedAfter) filter.updatedAt = { gt: params.updatedAfter };
if (params.updatedBefore) filter.updatedAt = { lt: params.updatedBefore };
if (params.completedAfter) filter.completedAt = { gt: params.completedAfter };
if (params.completedBefore) filter.completedAt = { lt: params.completedBefore };
if (params.startedAfter) filter.startedAt = { gt: params.startedAfter };
if (params.startedBefore) filter.startedAt = { lt: params.startedBefore };
// Due date filters
if (params.dueDate) filter.dueDate = { eq: params.dueDate };
if (params.dueDateAfter) filter.dueDate = { gt: params.dueDateAfter };
if (params.dueDateBefore) filter.dueDate = { lt: params.dueDateBefore };
// Relationship filters
if (params.projectId) filter.project = { id: { eq: params.projectId } };
if (params.parentId) filter.parent = { id: { eq: params.parentId } };
if (params.subscriberId) filter.subscribers = { some: { id: { eq: params.subscriberId } } };
if (params.hasBlockedBy) filter.hasBlockedByRelations = { eq: true };
if (params.hasBlocking) filter.hasBlockingRelations = { eq: true };
if (params.hasDuplicates) filter.hasDuplicateRelations = { eq: true };
// Labels
if (params.labelIds?.length) {
filter.labels = { some: { id: { in: params.labelIds } } };
}
// Other filters
if (params.estimate !== undefined) filter.estimate = { eq: params.estimate };
if (params.number !== undefined) filter.number = { eq: params.number };
if (params.snoozedById) filter.snoozedBy = { id: { eq: params.snoozedById } };
if (params.snoozedUntilAfter) filter.snoozedUntilAt = { gt: params.snoozedUntilAfter };
if (params.snoozedUntilBefore) filter.snoozedUntilAt = { lt: params.snoozedUntilBefore };
2025-01-09 16:27:26 +05:30
const issues = await client.issues({
first: params.limit,
filter,
2025-01-12 20:05:43 +05:30
orderBy: params.orderBy || "updatedAt" as any,
2025-01-09 16:27:26 +05:30
});
return issues.nodes.map(formatIssue);
} catch (error) {
return `Error: ${error}`;
}
}
2025-01-12 20:05:43 +05:30
// Modify searchUsers function to allow more specific search parameters
2025-01-09 16:27:26 +05:30
async function searchUsers(
client: LinearClient,
2025-01-12 20:05:43 +05:30
params: z.infer<typeof SearchUsersParams>
2025-01-09 16:27:26 +05:30
) {
try {
2025-01-12 20:05:43 +05:30
let filter: any = {};
if (params.email) {
filter = { email: { eq: params.email } };
} else if (params.displayName) {
filter = { displayName: { containsIgnoreCase: params.displayName } };
}
2025-01-09 16:27:26 +05:30
const users = await client.users({
2025-01-12 20:05:43 +05:30
filter,
first: params.limit,
2025-01-09 16:27:26 +05:30
});
return users.nodes.map(
(user): SimpleUser => ({
id: user.id,
name: user.name,
email: user.email,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
})
);
} catch (error) {
return `Error: ${error}`;
}
}
// Add new Project API Functions
async function createProject(
client: LinearClient,
params: z.infer<typeof ProjectParams>
) {
try {
return await client.createProject(params);
} catch (error) {
return `Error: ${error}`;
}
}
async function updateProject(
client: LinearClient,
params: z.infer<typeof UpdateProjectParams>
) {
try {
const { projectId, ...updateData } = params;
return await client.updateProject(projectId, updateData);
} catch (error) {
return `Error: ${error}`;
}
}
async function getProject(
client: LinearClient,
{ projectId }: z.infer<typeof GetProjectParams>
) {
try {
return await client.project(projectId);
} catch (error) {
return `Error: ${error}`;
}
}
// Modify searchProjects function to handle empty queries
async function searchProjects(
client: LinearClient,
2025-01-12 20:05:43 +05:30
params: z.infer<typeof SearchProjectsParams>
2025-01-09 16:27:26 +05:30
) {
try {
const filter: any = {};
2025-01-12 20:05:43 +05:30
// Text search filters
if (params.query) {
filter.or = [
{ name: { containsIgnoreCase: params.query } },
{ searchableContent: { contains: params.query } }
];
}
if (params.name) {
filter.name = { containsIgnoreCase: params.name };
}
// Basic filters
if (params.teamId) {
filter.accessibleTeams = { some: { id: { eq: params.teamId } } };
}
if (params.creatorId) {
filter.creator = { id: { eq: params.creatorId } };
}
if (params.leadId) {
filter.lead = { id: { eq: params.leadId } };
}
if (params.priority !== undefined) {
filter.priority = { eq: params.priority };
2025-01-09 16:27:26 +05:30
}
2025-01-12 20:05:43 +05:30
// Status and state filters
if (params.health) {
filter.health = { eq: params.health };
}
if (params.state) {
filter.state = { eq: params.state };
}
if (params.status) {
filter.status = { id: { eq: params.status } };
2025-01-09 16:27:26 +05:30
}
2025-01-12 20:05:43 +05:30
// Date filters
if (params.startDate) {
filter.startDate = { eq: params.startDate };
}
if (params.targetDate) {
filter.targetDate = { eq: params.targetDate };
}
if (params.createdAfter || params.createdBefore) {
filter.createdAt = {
...(params.createdAfter && { gt: params.createdAfter }),
...(params.createdBefore && { lt: params.createdBefore })
};
}
if (params.updatedAfter || params.updatedBefore) {
filter.updatedAt = {
...(params.updatedAfter && { gt: params.updatedAfter }),
...(params.updatedBefore && { lt: params.updatedBefore })
};
}
if (params.completedAfter || params.completedBefore) {
filter.completedAt = {
...(params.completedAfter && { gt: params.completedAfter }),
...(params.completedBefore && { lt: params.completedBefore })
};
}
if (params.canceledAfter || params.canceledBefore) {
filter.canceledAt = {
...(params.canceledAfter && { gt: params.canceledAfter }),
...(params.canceledBefore && { lt: params.canceledBefore })
};
2025-01-09 16:27:26 +05:30
}
2025-01-12 20:05:43 +05:30
// Relationship filters
if (params.hasBlockedBy) {
filter.hasBlockedByRelations = { eq: true };
}
if (params.hasBlocking) {
filter.hasBlockingRelations = { eq: true };
}
if (params.hasRelated) {
filter.hasRelatedRelations = { eq: true };
}
if (params.hasViolatedDependencies) {
filter.hasViolatedRelations = { eq: true };
}
const projects = await client.projects({
first: params.limit,
filter,
orderBy: params.orderBy || "updatedAt" as any,
});
2025-01-09 16:27:26 +05:30
return projects.nodes.map(formatProject);
} catch (error) {
return `Error: ${error}`;
}
}
// Add new listProjects function
async function listProjects(
client: LinearClient,
{ teamId, limit, state }: z.infer<typeof ListProjectsParams>
) {
try {
const filter: any = {};
if (teamId) {
filter.team = { id: { eq: teamId } };
}
if (state) {
filter.state = { eq: state };
}
const projects = await client.projects({
first: limit,
filter: Object.keys(filter).length > 0 ? filter : undefined,
orderBy: "updatedAt" as any,
});
return projects.nodes.map(formatProject);
} catch (error) {
return `Error: ${error}`;
}
}
2025-01-12 20:05:43 +05:30
// Add new comment API functions before the main manager function
async function createComment(
client: LinearClient,
params: z.infer<typeof CreateCommentParams>
) {
try {
const { issueId, body } = params;
const comment = await client.createComment({
issueId,
body,
});
return formatComment(comment);
} catch (error) {
return `Error: ${error}`;
}
}
async function listComments(
client: LinearClient,
params: z.infer<typeof ListCommentsParams>
) {
try {
const { issueId, limit } = params;
const issue = await client.issue(issueId);
const comments = await issue.comments({
first: limit,
orderBy: "createdAt" as any,
});
return comments.nodes.map(formatComment);
} catch (error) {
return `Error: ${error}`;
}
}
// Add new document API functions before the main manager function
async function createDocument(
client: LinearClient,
params: z.infer<typeof CreateDocumentParams>
) {
try {
const document = await client.createDocument(params);
return formatDocument(document);
} catch (error) {
return `Error: ${error}`;
}
}
async function updateDocument(
client: LinearClient,
params: z.infer<typeof UpdateDocumentParams>
) {
try {
const { documentId, ...updateData } = params;
const document = await client.updateDocument(documentId, updateData);
return formatDocument(document);
} catch (error) {
return `Error: ${error}`;
}
}
async function getDocument(
client: LinearClient,
{ documentId }: z.infer<typeof GetDocumentParams>
) {
try {
const document = await client.document(documentId);
return formatDocument(document);
} catch (error) {
return `Error: ${error}`;
}
}
async function searchDocuments(
client: LinearClient,
params: z.infer<typeof SearchDocumentsParams>
) {
try {
const filter: any = {
or: [
{ title: { containsIgnoreCase: params.query } },
{ content: { containsIgnoreCase: params.query } },
],
};
if (params.projectId) {
filter.project = { id: { eq: params.projectId } };
}
const documents = await client.documents({
first: params.limit,
filter,
orderBy: "updatedAt" as any,
});
return documents.nodes.map(formatDocument);
} catch (error) {
return `Error: ${error}`;
}
}
2025-01-09 16:27:26 +05:30
// Main manager function
export const LinearManagerParams = z.object({
request: z
.string()
.describe("User's request regarding Linear project management"),
});
export type LinearManagerParams = z.infer<typeof LinearManagerParams>;
export async function linearManager(
{ request }: LinearManagerParams,
context_message: Message
) {
2025-01-12 20:05:43 +05:30
2025-01-09 16:27:26 +05:30
const userConfig = context_message.author.config;
// console.log("User config", userConfig);
const linearApiKey = userConfig?.identities.find(
(i) => i.platform === "linear_key"
)?.id;
// console.log("Linear API Key", linearApiKey);
const linearEmail = userConfig?.identities.find(
(i) => i.platform === "linear_email"
)?.id;
if (!linearApiKey) {
return {
response: "Please configure your Linear API key to use this tool.",
};
}
const client = new LinearClient({ apiKey: linearApiKey });
2025-01-12 20:05:43 +05:30
2025-01-09 16:27:26 +05:30
const linear_tools: RunnableToolFunction<any>[] = [
zodFunction({
function: (params) => createIssue(client, params),
name: "linearCreateIssue",
schema: IssueParams,
description: "Create a new issue in Linear",
}),
zodFunction({
function: (params) => updateIssue(client, params),
name: "linearUpdateIssue",
schema: UpdateIssueParams,
description: "Update an existing issue in Linear",
}),
zodFunction({
function: (params) => getIssue(client, params),
name: "linearGetIssue",
schema: GetIssueParams,
description: "Get details of a specific issue",
}),
zodFunction({
function: (params) => searchUsers(client, params),
name: "linearSearchUsers",
schema: SearchUsersParams,
description:
"Search for users across the workspace by name, display name, or email. Use display name for better results.",
}),
zodFunction({
function: (params) => searchIssues(client, params),
name: "linearSearchIssues",
schema: SearchIssuesParams,
description:
"Search for issues in Linear using a query string. Optionally filter by team and limit results.",
}),
zodFunction({
function: (params) => listTeams(client, params),
name: "linearListTeams",
schema: ListTeamsParams,
description: "List all teams in the workspace with optional limit",
}),
zodFunction({
function: (params) => advancedSearchIssues(client, params),
name: "linearAdvancedSearchIssues",
schema: AdvancedSearchIssuesParams,
2025-01-12 20:05:43 +05:30
description: `Search for issues with advanced filters including:
- Status (backlog, todo, in_progress, done, canceled)
- Assignee
- Priority
- Date ranges for:
* Updated time
* Created time
* Completed time
Use ISO datetime format (e.g., "2024-01-18T00:00:00Z") for date filters.
Can find issues updated, created, or completed within specific time periods.`,
2025-01-09 16:27:26 +05:30
}),
zodFunction({
function: (params) => createProject(client, params),
name: "linearCreateProject",
schema: ProjectParams,
description: "Create a new project in Linear",
}),
zodFunction({
function: (params) => updateProject(client, params),
name: "linearUpdateProject",
schema: UpdateProjectParams,
description: "Update an existing project in Linear",
}),
zodFunction({
function: (params) => getProject(client, params),
name: "linearGetProject",
schema: GetProjectParams,
description: "Get details of a specific project",
}),
zodFunction({
function: (params) => searchProjects(client, params),
name: "linearSearchProjects",
schema: SearchProjectsParams,
description:
"Search for projects in Linear using a query string. Optionally filter by team and limit results.",
}),
zodFunction({
function: (params) => listProjects(client, params),
name: "linearListProjects",
schema: ListProjectsParams,
description:
"List projects in Linear, optionally filtered by team and state. Returns most recently updated projects first.",
}),
2025-01-12 20:05:43 +05:30
zodFunction({
function: (params) => createComment(client, params),
name: "linearCreateComment",
schema: CreateCommentParams,
description: "Create a new comment on a Linear issue",
}),
zodFunction({
function: (params) => listComments(client, params),
name: "linearListComments",
schema: ListCommentsParams,
description: "List comments on a Linear issue",
}),
zodFunction({
function: (params) => createDocument(client, params),
name: "linearCreateDocument",
schema: CreateDocumentParams,
description: "Create a new document in Linear",
}),
zodFunction({
function: (params) => updateDocument(client, params),
name: "linearUpdateDocument",
schema: UpdateDocumentParams,
description: "Update an existing document in Linear",
}),
zodFunction({
function: (params) => getDocument(client, params),
name: "linearGetDocument",
schema: GetDocumentParams,
description: "Get details of a specific document",
}),
zodFunction({
function: (params) => searchDocuments(client, params),
name: "linearSearchDocuments",
schema: SearchDocumentsParams,
description: "Search for documents in Linear using a query string",
}),
2025-01-09 16:27:26 +05:30
];
2025-01-12 20:05:43 +05:30
const organization = await client.organization
const workspace = organization?.name
2025-01-09 16:27:26 +05:30
// fetch all labels available in each team
const teams = await client.teams({ first: 10 });
const teamLabels = await client.issueLabels();
// list all the possible states of issues
const states = await client.workflowStates();
const state_values = states.nodes.map((state) => ({
id: state.id,
name: state.name,
}));
2025-01-12 20:05:43 +05:30
const organizationContext = `Organization:
Name: ${workspace}
Id: ${organization?.id}
`;
2025-01-09 16:27:26 +05:30
// Only include teams and labels in the context if they exist
const teamsContext =
teams.nodes.length > 0
2025-01-12 20:05:43 +05:30
? `Teams:\n${teams.nodes.map((team) => ` - ${team.name} id: ${team.id}`).join("\n")}`
2025-01-09 16:27:26 +05:30
: "";
const labelsContext =
teamLabels.nodes.length > 0
? `All Labels:\n${teamLabels.nodes
2025-01-12 20:05:43 +05:30
.map((label) => ` - ${label.name} (${label.color}) id: ${label.id}`)
.join("\n")}`
2025-01-09 16:27:26 +05:30
: "";
const issueStateContext =
state_values.length > 0
? `All Issue States:\n${state_values
2025-01-12 20:05:43 +05:30
.map((state) => ` - ${state.name} id: ${state.id}`)
.join("\n")}`
2025-01-09 16:27:26 +05:30
: "";
2025-01-12 20:05:43 +05:30
const workspaceContext = [organizationContext, teamsContext, labelsContext, issueStateContext]
2025-01-09 16:27:26 +05:30
.filter(Boolean)
.join("\n\n");
2025-01-12 20:05:43 +05:30
const userDetails = await client.users({ filter: { email: { eq: linearEmail } } });
2025-01-09 16:27:26 +05:30
const response = await ask({
2025-01-12 20:05:43 +05:30
model: "gpt-4o",
2025-01-09 16:27:26 +05:30
prompt: `You are a Linear project manager.
Your job is to understand the user's request and manage issues, teams, and projects using the available tools.
2025-01-12 20:05:43 +05:30
Important note about Linear issue identification:
- issueId: A UUID that uniquely identifies an issue internally (e.g., "123e4567-e89b-12d3-a456-426614174000")
- identifier: A human-readable issue reference (e.g., "XCE-205", "ENG-123")
When referring to issues in responses, always use the identifier format for better readability.
2025-01-09 16:27:26 +05:30
----
${memory_manager_guide("linear_manager", context_message.author.id)}
----
2025-01-12 20:05:43 +05:30
${workspaceContext
? `Here is some more context on current linear workspace:\n${workspaceContext}`
: ""
}
2025-01-09 16:27:26 +05:30
2025-01-12 20:05:43 +05:30
The user you are currently assisting has the following details (No need to search if the user is asking for their own related issues/projects):
2025-01-09 16:27:26 +05:30
- Name: ${userConfig?.name}
- Linear Email: ${linearEmail}
2025-01-12 20:05:43 +05:30
- Linear User ID: ${userDetails.nodes[0]?.id}
2025-01-09 16:27:26 +05:30
When responding make sure to link the issues when returning the value.
linear issue links look like: \`https://linear.app/xcelerator/issue/XCE-205\`
2025-01-12 20:05:43 +05:30
Where \`XCE-205\` is the identifier (not the issueId) and \`xcelerator\` is the team name.
2025-01-09 16:27:26 +05:30
`,
message: request,
seed: `linear-${context_message.channelId}`,
tools: linear_tools.concat(
memory_manager_init(context_message, "linear_manager")
) as any,
});
return { response };
}
export const linear_manager_tool = (context_message: Message) =>
zodFunction({
function: (args) => linearManager(args, context_message),
name: "linear_manager",
schema: LinearManagerParams,
description: `Linear Issue Manager.
This tool allows you to create, update, close, or assign issues in Linear.
Provide detailed information to perform the requested action.
Use this when user explicitly asks for Linear/project management.`,
});