chittycan
Version:
Your completely autonomous network that grows with you - DNA ownership platform with encrypted vaults, PDX portability, and ChittyFoundation governance
274 lines (235 loc) • 6.91 kB
text/typescript
/**
* Linear Extension for ChittyTracker
* Manage issues, projects, and sync with Notion
*/
import type { ChittyPlugin } from "../../lib/plugin.js";
import type { Config } from "../../lib/config.js";
export interface LinearRemote {
type: "linear-workspace";
apiKey: string;
workspaceId?: string;
teamId?: string;
}
class LinearClient {
private apiKey: string;
constructor(remote: LinearRemote) {
this.apiKey = remote.apiKey;
}
private async query(query: string, variables: any = {}) {
const response = await fetch("https://api.linear.app/graphql", {
method: "POST",
headers: {
"Authorization": this.apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`Linear API error: ${response.statusText}`);
}
const result: any = await response.json();
if (result.errors) {
throw new Error(`Linear GraphQL error: ${result.errors[0].message}`);
}
return result.data;
}
// Issues
async listIssues(teamId?: string) {
const query = `
query Issues($teamId: String) {
issues(filter: { team: { id: { eq: $teamId } } }) {
nodes {
id
title
description
state {
name
}
assignee {
name
}
dueDate
url
}
}
}
`;
const data = await this.query(query, { teamId });
return data.issues.nodes;
}
async createIssue(title: string, description?: string, teamId?: string) {
const query = `
mutation CreateIssue($title: String!, $description: String, $teamId: String!) {
issueCreate(input: { title: $title, description: $description, teamId: $teamId }) {
success
issue {
id
title
url
}
}
}
`;
const data = await this.query(query, { title, description, teamId });
return data.issueCreate.issue;
}
async updateIssue(issueId: string, updates: any) {
const query = `
mutation UpdateIssue($issueId: String!, $updates: IssueUpdateInput!) {
issueUpdate(id: $issueId, input: $updates) {
success
issue {
id
title
}
}
}
`;
const data = await this.query(query, { issueId, updates });
return data.issueUpdate.issue;
}
// Teams
async listTeams() {
const query = `
query Teams {
teams {
nodes {
id
name
key
}
}
}
`;
const data = await this.query(query);
return data.teams.nodes;
}
// Projects
async listProjects(teamId?: string) {
const query = `
query Projects($teamId: String) {
projects(filter: { team: { id: { eq: $teamId } } }) {
nodes {
id
name
description
state
}
}
}
`;
const data = await this.query(query, { teamId });
return data.projects.nodes;
}
}
// Command handlers
async function listIssues(args: any, config: Config) {
const remoteName = args.remote || "linear";
const remote = config.remotes[remoteName] as unknown as LinearRemote;
if (!remote || remote.type !== "linear-workspace") {
throw new Error(`Remote ${remoteName} not found or not a Linear workspace`);
}
const client = new LinearClient(remote);
const issues = await client.listIssues(remote.teamId);
console.log("\nLinear Issues:\n");
issues.forEach((issue: any) => {
console.log(` ${issue.title}`);
console.log(` State: ${issue.state.name}`);
if (issue.assignee) {
console.log(` Assignee: ${issue.assignee.name}`);
}
if (issue.dueDate) {
console.log(` Due: ${issue.dueDate}`);
}
console.log(` URL: ${issue.url}`);
console.log();
});
}
async function createIssue(args: any, config: Config) {
const remoteName = args.remote || "linear";
const remote = config.remotes[remoteName] as unknown as LinearRemote;
if (!remote || remote.type !== "linear-workspace") {
throw new Error(`Remote ${remoteName} not found or not a Linear workspace`);
}
if (!args.title) {
throw new Error("Issue title is required");
}
if (!remote.teamId) {
throw new Error("Team ID not configured for this remote");
}
const client = new LinearClient(remote);
const issue = await client.createIssue(args.title, args.description, remote.teamId);
console.log(`\n✓ Created issue: ${issue.title}`);
console.log(` URL: ${issue.url}`);
}
async function listTeams(args: any, config: Config) {
const remoteName = args.remote || "linear";
const remote = config.remotes[remoteName] as unknown as LinearRemote;
if (!remote || remote.type !== "linear-workspace") {
throw new Error(`Remote ${remoteName} not found or not a Linear workspace`);
}
const client = new LinearClient(remote);
const teams = await client.listTeams();
console.log("\nLinear Teams:\n");
teams.forEach((team: any) => {
console.log(` ${team.name} (${team.key})`);
console.log(` ID: ${team.id}`);
console.log();
});
}
// Plugin definition
const LinearPlugin: ChittyPlugin = {
metadata: {
name: "@chitty/linear",
version: "1.0.0",
description: "Manage Linear issues and sync with Notion",
author: "ChittyTracker",
homepage: "https://github.com/chittytracker/chittytracker",
},
remoteTypes: [
{
type: "linear-workspace",
schema: {
apiKey: { type: "string", required: true },
workspaceId: { type: "string", required: false },
teamId: { type: "string", required: false },
},
validate: (config: any) => {
if (!config.apiKey) return "apiKey is required";
return true;
},
},
],
commands: [
{
name: "linear issues",
description: "List all Linear issues",
handler: listIssues,
options: {
remote: { type: "string", description: "Remote name", default: "linear" },
},
},
{
name: "linear issue create",
description: "Create a new issue",
handler: createIssue,
options: {
remote: { type: "string", description: "Remote name", default: "linear" },
title: { type: "string", description: "Issue title", required: true },
description: { type: "string", description: "Issue description" },
},
},
{
name: "linear teams",
description: "List all teams",
handler: listTeams,
options: {
remote: { type: "string", description: "Remote name", default: "linear" },
},
},
],
async init(config: Config) {
console.log("[chitty] Linear extension loaded");
},
};
export default LinearPlugin;