@bdsqqq/lnr-core 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bdsqqq/lnr-core",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "core business logic for lnr",
5
5
  "type": "module",
6
6
  "private": false,
package/src/client.ts CHANGED
@@ -10,16 +10,20 @@ export class NotAuthenticatedError extends Error {
10
10
  }
11
11
  }
12
12
 
13
- export function getClient(): LinearClient {
14
- if (clientInstance) {
15
- return clientInstance;
16
- }
17
-
18
- const apiKey = getApiKey();
13
+ export function getClient(apiKeyOverride?: string): LinearClient {
14
+ const apiKey = apiKeyOverride ?? getApiKey();
19
15
  if (!apiKey) {
20
16
  throw new NotAuthenticatedError();
21
17
  }
22
18
 
19
+ if (apiKeyOverride) {
20
+ return new LinearClient({ apiKey });
21
+ }
22
+
23
+ if (clientInstance) {
24
+ return clientInstance;
25
+ }
26
+
23
27
  clientInstance = new LinearClient({ apiKey });
24
28
  return clientInstance;
25
29
  }
@@ -0,0 +1,119 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { aggregateReactions, extractSyncMeta } from "./comments";
3
+
4
+ describe("aggregateReactions", () => {
5
+ test("empty array → empty", () => {
6
+ expect(aggregateReactions([])).toEqual([]);
7
+ });
8
+
9
+ test("single reaction → count 1", () => {
10
+ expect(aggregateReactions([{ emoji: "👍" }])).toEqual([
11
+ { emoji: "👍", count: 1 },
12
+ ]);
13
+ });
14
+
15
+ test("duplicates aggregate", () => {
16
+ const result = aggregateReactions([
17
+ { emoji: "👍" },
18
+ { emoji: "👍" },
19
+ { emoji: "👍" },
20
+ ]);
21
+ expect(result).toEqual([{ emoji: "👍", count: 3 }]);
22
+ });
23
+
24
+ test("different emojis get separate entries", () => {
25
+ const result = aggregateReactions([
26
+ { emoji: "👍" },
27
+ { emoji: "👎" },
28
+ { emoji: "👍" },
29
+ ]);
30
+ expect(result).toContainEqual({ emoji: "👍", count: 2 });
31
+ expect(result).toContainEqual({ emoji: "👎", count: 1 });
32
+ expect(result).toHaveLength(2);
33
+ });
34
+ });
35
+
36
+ describe("extractSyncMeta", () => {
37
+ test("slack with valid metadata", () => {
38
+ const result = extractSyncMeta("slack", {
39
+ channelName: "frontend",
40
+ messageUrl: "https://slack.com/msg/123",
41
+ });
42
+ expect(result).toEqual({
43
+ type: "slack",
44
+ channelName: "frontend",
45
+ messageUrl: "https://slack.com/msg/123",
46
+ });
47
+ });
48
+
49
+ test("slack with missing metadata", () => {
50
+ expect(extractSyncMeta("slack", null)).toEqual({
51
+ type: "slack",
52
+ channelName: undefined,
53
+ messageUrl: undefined,
54
+ });
55
+ });
56
+
57
+ test("slack with partial metadata", () => {
58
+ expect(extractSyncMeta("slack", { channelName: "general" })).toEqual({
59
+ type: "slack",
60
+ channelName: "general",
61
+ messageUrl: undefined,
62
+ });
63
+ });
64
+
65
+ test("github extracts owner/repo/number", () => {
66
+ const result = extractSyncMeta("github", {
67
+ owner: "bdsqqq",
68
+ repo: "lnr",
69
+ number: 42,
70
+ });
71
+ expect(result).toEqual({
72
+ type: "github",
73
+ owner: "bdsqqq",
74
+ repo: "lnr",
75
+ number: 42,
76
+ });
77
+ });
78
+
79
+ test("github partial (only repo)", () => {
80
+ expect(extractSyncMeta("github", { repo: "lnr" })).toEqual({
81
+ type: "github",
82
+ owner: undefined,
83
+ repo: "lnr",
84
+ number: undefined,
85
+ });
86
+ });
87
+
88
+ test("jira extracts issueKey/projectId", () => {
89
+ const result = extractSyncMeta("jira", {
90
+ issueKey: "PROJ-123",
91
+ projectId: "proj-id",
92
+ });
93
+ expect(result).toEqual({
94
+ type: "jira",
95
+ issueKey: "PROJ-123",
96
+ projectId: "proj-id",
97
+ });
98
+ });
99
+
100
+ test("unknown service → { type: 'unknown' }", () => {
101
+ expect(extractSyncMeta("notion", { some: "data" })).toEqual({
102
+ type: "unknown",
103
+ });
104
+ });
105
+
106
+ test("case insensitive", () => {
107
+ expect(extractSyncMeta("SLACK", { channelName: "test" })).toEqual({
108
+ type: "slack",
109
+ channelName: "test",
110
+ messageUrl: undefined,
111
+ });
112
+ expect(extractSyncMeta("GitHub", { owner: "x" })).toEqual({
113
+ type: "github",
114
+ owner: "x",
115
+ repo: undefined,
116
+ number: undefined,
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,193 @@
1
+ import type { LinearClient, ExternalEntityInfo } from "@linear/sdk";
2
+
3
+ /** aggregated reaction count for display (e.g., 👍 x 3) */
4
+ export interface CommentReaction {
5
+ emoji: string;
6
+ count: number;
7
+ }
8
+
9
+ /** slack-specific sync metadata */
10
+ export interface SlackSyncMeta {
11
+ type: "slack";
12
+ channelName?: string;
13
+ messageUrl?: string;
14
+ }
15
+
16
+ /** github-specific sync metadata */
17
+ export interface GithubSyncMeta {
18
+ type: "github";
19
+ owner?: string;
20
+ repo?: string;
21
+ number?: number;
22
+ }
23
+
24
+ /** jira-specific sync metadata */
25
+ export interface JiraSyncMeta {
26
+ type: "jira";
27
+ issueKey?: string;
28
+ projectId?: string;
29
+ }
30
+
31
+ /** generic sync metadata for unknown services */
32
+ export interface GenericSyncMeta {
33
+ type: "unknown";
34
+ }
35
+
36
+ export type SyncMeta = SlackSyncMeta | GithubSyncMeta | JiraSyncMeta | GenericSyncMeta;
37
+
38
+ /** external service sync info — extracted from linear's ExternalEntityInfo */
39
+ export interface CommentSyncInfo {
40
+ service: string;
41
+ meta: SyncMeta;
42
+ }
43
+
44
+ export interface CommentsResult {
45
+ comments: Comment[];
46
+ error?: string;
47
+ }
48
+
49
+ export interface Comment {
50
+ id: string;
51
+ body: string;
52
+ createdAt: Date;
53
+ updatedAt: Date;
54
+ /** linear user who authored the comment, null if external */
55
+ user: string | null;
56
+ /** parent comment id for threading, null if root */
57
+ parentId: string | null;
58
+ url: string;
59
+ reactions: CommentReaction[];
60
+ /** external services this comment syncs with (slack channels, etc.) */
61
+ syncedWith: CommentSyncInfo[];
62
+ /** bot that created this comment (linear integrations) */
63
+ botActor?: string | null;
64
+ /** external user (e.g., slack user) who authored the comment */
65
+ externalUser?: string | null;
66
+ }
67
+
68
+ export function aggregateReactions(reactions: readonly { emoji: string }[]): CommentReaction[] {
69
+ const counts = new Map<string, number>();
70
+ for (const r of reactions) {
71
+ counts.set(r.emoji, (counts.get(r.emoji) ?? 0) + 1);
72
+ }
73
+ return Array.from(counts.entries()).map(([emoji, count]) => ({ emoji, count }));
74
+ }
75
+
76
+ export function extractSyncMeta(service: string, metadata: unknown): SyncMeta {
77
+ const svc = service.toLowerCase();
78
+ const isObject = typeof metadata === "object" && metadata !== null;
79
+
80
+ if (svc === "slack") {
81
+ const channelName =
82
+ isObject && "channelName" in metadata && typeof metadata.channelName === "string"
83
+ ? metadata.channelName
84
+ : undefined;
85
+ const messageUrl =
86
+ isObject && "messageUrl" in metadata && typeof metadata.messageUrl === "string"
87
+ ? metadata.messageUrl
88
+ : undefined;
89
+ return { type: "slack", channelName, messageUrl } satisfies SyncMeta;
90
+ }
91
+
92
+ if (svc === "github") {
93
+ const owner =
94
+ isObject && "owner" in metadata && typeof metadata.owner === "string"
95
+ ? metadata.owner
96
+ : undefined;
97
+ const repo =
98
+ isObject && "repo" in metadata && typeof metadata.repo === "string"
99
+ ? metadata.repo
100
+ : undefined;
101
+ const number =
102
+ isObject && "number" in metadata && typeof metadata.number === "number"
103
+ ? metadata.number
104
+ : undefined;
105
+ return { type: "github", owner, repo, number } satisfies SyncMeta;
106
+ }
107
+
108
+ if (svc === "jira") {
109
+ const issueKey =
110
+ isObject && "issueKey" in metadata && typeof metadata.issueKey === "string"
111
+ ? metadata.issueKey
112
+ : undefined;
113
+ const projectId =
114
+ isObject && "projectId" in metadata && typeof metadata.projectId === "string"
115
+ ? metadata.projectId
116
+ : undefined;
117
+ return { type: "jira", issueKey, projectId } satisfies SyncMeta;
118
+ }
119
+
120
+ return { type: "unknown" } satisfies SyncMeta;
121
+ }
122
+
123
+ function extractSyncInfo(syncedWith?: ExternalEntityInfo[]): CommentSyncInfo[] {
124
+ if (!syncedWith) return [];
125
+ return syncedWith.map((s) => ({
126
+ service: s.service,
127
+ meta: extractSyncMeta(s.service, s.metadata),
128
+ }));
129
+ }
130
+
131
+ export async function getIssueComments(
132
+ client: LinearClient,
133
+ issueId: string
134
+ ): Promise<CommentsResult> {
135
+ try {
136
+ const issue = await client.issue(issueId);
137
+ if (!issue) return { comments: [] };
138
+
139
+ const commentsData = await issue.comments();
140
+ const comments = await Promise.all(
141
+ commentsData.nodes.map(async (c) => {
142
+ const user = await c.user;
143
+ const externalUser = c.externalUserId ? await c.externalUser : null;
144
+ const botActor = c.botActor;
145
+
146
+ return {
147
+ id: c.id,
148
+ body: c.body,
149
+ createdAt: c.createdAt,
150
+ updatedAt: c.updatedAt,
151
+ user: user?.name ?? null,
152
+ parentId: c.parentId ?? null,
153
+ url: c.url,
154
+ reactions: aggregateReactions(c.reactions ?? []),
155
+ syncedWith: extractSyncInfo(c.syncedWith),
156
+ botActor: botActor?.name ?? null,
157
+ externalUser: externalUser?.name ?? null,
158
+ };
159
+ })
160
+ );
161
+ return { comments };
162
+ } catch (err) {
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ return { comments: [], error: message };
165
+ }
166
+ }
167
+
168
+ export async function updateComment(
169
+ client: LinearClient,
170
+ commentId: string,
171
+ body: string
172
+ ): Promise<boolean> {
173
+ const result = await client.updateComment(commentId, { body });
174
+ return result.success;
175
+ }
176
+
177
+ export async function replyToComment(
178
+ client: LinearClient,
179
+ issueId: string,
180
+ parentId: string,
181
+ body: string
182
+ ): Promise<boolean> {
183
+ const result = await client.createComment({ issueId, body, parentId });
184
+ return result.success;
185
+ }
186
+
187
+ export async function deleteComment(
188
+ client: LinearClient,
189
+ commentId: string
190
+ ): Promise<boolean> {
191
+ const result = await client.deleteComment(commentId);
192
+ return result.success;
193
+ }
@@ -1,9 +1,12 @@
1
- import { describe, test, expect, afterEach, beforeAll } from "bun:test";
1
+ import { describe, test, expect, afterEach, beforeAll, afterAll } from "bun:test";
2
2
  import {
3
3
  loadConfig,
4
4
  saveConfig,
5
5
  getConfigValue,
6
6
  setConfigValue,
7
+ getApiKey,
8
+ listConfig,
9
+ getConfigPath,
7
10
  type Config,
8
11
  } from "./config";
9
12
 
@@ -41,4 +44,73 @@ describe("config core", () => {
41
44
  expect(loaded.default_team).toBe(testConfig.default_team);
42
45
  expect(loaded.output_format).toBe(testConfig.output_format);
43
46
  });
47
+
48
+ test("listConfig returns full config", () => {
49
+ const testConfig: Config = {
50
+ api_key: "list_test_key",
51
+ default_team: "LIST_TEST",
52
+ };
53
+ saveConfig(testConfig);
54
+ const listed = listConfig();
55
+ expect(listed.api_key).toBe(testConfig.api_key);
56
+ expect(listed.default_team).toBe(testConfig.default_team);
57
+ });
58
+
59
+ test("getConfigPath returns path string", () => {
60
+ const path = getConfigPath();
61
+ expect(typeof path).toBe("string");
62
+ expect(path).toContain(".lnr");
63
+ expect(path).toContain("config.json");
64
+ });
65
+ });
66
+
67
+ describe("getApiKey precedence", () => {
68
+ let originalConfig: Config;
69
+ let originalEnv: string | undefined;
70
+
71
+ beforeAll(() => {
72
+ originalConfig = loadConfig();
73
+ originalEnv = process.env.LINEAR_API_KEY;
74
+ });
75
+
76
+ afterEach(() => {
77
+ saveConfig(originalConfig);
78
+ if (originalEnv !== undefined) {
79
+ process.env.LINEAR_API_KEY = originalEnv;
80
+ } else {
81
+ delete process.env.LINEAR_API_KEY;
82
+ }
83
+ });
84
+
85
+ afterAll(() => {
86
+ if (originalEnv !== undefined) {
87
+ process.env.LINEAR_API_KEY = originalEnv;
88
+ } else {
89
+ delete process.env.LINEAR_API_KEY;
90
+ }
91
+ });
92
+
93
+ test("getApiKey returns config value when env not set", () => {
94
+ delete process.env.LINEAR_API_KEY;
95
+ saveConfig({ api_key: "config_key_123" });
96
+ expect(getApiKey()).toBe("config_key_123");
97
+ });
98
+
99
+ test("getApiKey returns env var when set", () => {
100
+ saveConfig({ api_key: "config_key_123" });
101
+ process.env.LINEAR_API_KEY = "env_key_456";
102
+ expect(getApiKey()).toBe("env_key_456");
103
+ });
104
+
105
+ test("getApiKey prefers env over config", () => {
106
+ saveConfig({ api_key: "should_not_return" });
107
+ process.env.LINEAR_API_KEY = "should_return";
108
+ expect(getApiKey()).toBe("should_return");
109
+ });
110
+
111
+ test("getApiKey returns undefined when neither set", () => {
112
+ delete process.env.LINEAR_API_KEY;
113
+ saveConfig({});
114
+ expect(getApiKey()).toBeUndefined();
115
+ });
44
116
  });
package/src/config.ts CHANGED
@@ -36,7 +36,15 @@ export function saveConfig(config: Config): void {
36
36
  }
37
37
 
38
38
  export function getApiKey(): string | undefined {
39
- return loadConfig().api_key;
39
+ return process.env.LINEAR_API_KEY ?? loadConfig().api_key;
40
+ }
41
+
42
+ export function listConfig(): Config {
43
+ return loadConfig();
44
+ }
45
+
46
+ export function getConfigPath(): string {
47
+ return CONFIG_PATH;
40
48
  }
41
49
 
42
50
  export function setApiKey(key: string): void {
@@ -0,0 +1,110 @@
1
+ import type { LinearClient } from "@linear/sdk";
2
+
3
+ export interface Document {
4
+ id: string;
5
+ title: string;
6
+ content: string | null;
7
+ createdAt: Date;
8
+ updatedAt: Date;
9
+ url: string;
10
+ project: string | null;
11
+ }
12
+
13
+ export async function listDocuments(
14
+ client: LinearClient,
15
+ projectId?: string
16
+ ): Promise<Document[]> {
17
+ try {
18
+ const filter = projectId
19
+ ? { project: { id: { eq: projectId } } }
20
+ : undefined;
21
+
22
+ const documentsConnection = await client.documents({ filter });
23
+ const nodes = documentsConnection.nodes;
24
+
25
+ return Promise.all(
26
+ nodes.map(async (d) => ({
27
+ id: d.id,
28
+ title: d.title,
29
+ content: d.content ?? null,
30
+ createdAt: d.createdAt,
31
+ updatedAt: d.updatedAt,
32
+ url: d.url,
33
+ project: (await d.project)?.name ?? null,
34
+ }))
35
+ );
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ export async function getDocument(
42
+ client: LinearClient,
43
+ id: string
44
+ ): Promise<Document | null> {
45
+ try {
46
+ const doc = await client.document(id);
47
+ if (!doc) {
48
+ return null;
49
+ }
50
+
51
+ return {
52
+ id: doc.id,
53
+ title: doc.title,
54
+ content: doc.content ?? null,
55
+ createdAt: doc.createdAt,
56
+ updatedAt: doc.updatedAt,
57
+ url: doc.url,
58
+ project: (await doc.project)?.name ?? null,
59
+ };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ export async function createDocument(
66
+ client: LinearClient,
67
+ input: { title: string; content?: string; projectId?: string }
68
+ ): Promise<Document | null> {
69
+ const result = await client.createDocument({
70
+ title: input.title,
71
+ content: input.content,
72
+ projectId: input.projectId,
73
+ });
74
+
75
+ if (!result.success) {
76
+ return null;
77
+ }
78
+
79
+ const doc = await result.document;
80
+ if (!doc) {
81
+ return null;
82
+ }
83
+
84
+ return {
85
+ id: doc.id,
86
+ title: doc.title,
87
+ content: doc.content ?? null,
88
+ createdAt: doc.createdAt,
89
+ updatedAt: doc.updatedAt,
90
+ url: doc.url,
91
+ project: (await doc.project)?.name ?? null,
92
+ };
93
+ }
94
+
95
+ export async function updateDocument(
96
+ client: LinearClient,
97
+ id: string,
98
+ input: { title?: string; content?: string }
99
+ ): Promise<boolean> {
100
+ const result = await client.updateDocument(id, input);
101
+ return result.success;
102
+ }
103
+
104
+ export async function deleteDocument(
105
+ client: LinearClient,
106
+ id: string
107
+ ): Promise<boolean> {
108
+ const result = await client.deleteDocument(id);
109
+ return result.success;
110
+ }
package/src/index.ts CHANGED
@@ -30,6 +30,8 @@ export {
30
30
  getConfigValue,
31
31
  setConfigValue,
32
32
  ensureConfigDir,
33
+ listConfig,
34
+ getConfigPath,
33
35
  type Config,
34
36
  } from "./config";
35
37
 
@@ -43,6 +45,8 @@ export {
43
45
  priorityFromString,
44
46
  getTeamStates,
45
47
  getTeamLabels,
48
+ archiveIssue,
49
+ getSubIssues,
46
50
  } from "./issues";
47
51
 
48
52
  // projects
@@ -67,7 +71,53 @@ export {
67
71
  export { listCycles, getCurrentCycle, getCycleIssues } from "./cycles";
68
72
 
69
73
  // me
70
- export { getViewer, getMyIssues, getMyCreatedIssues } from "./me";
74
+ export { getViewer, getMyIssues, getMyCreatedIssues, getMyActivity } from "./me";
75
+ export type { Activity } from "./types";
71
76
 
72
77
  // search
73
78
  export { searchIssues } from "./search";
79
+
80
+ // relations
81
+ export { createIssueRelation } from "./relations";
82
+
83
+ // documents
84
+ export type { Document } from "./documents";
85
+ export {
86
+ listDocuments,
87
+ getDocument,
88
+ createDocument,
89
+ updateDocument,
90
+ deleteDocument,
91
+ } from "./documents";
92
+
93
+ // labels
94
+ export type { Label } from "./labels";
95
+ export {
96
+ listLabels,
97
+ getLabel,
98
+ createLabel,
99
+ updateLabel,
100
+ deleteLabel,
101
+ } from "./labels";
102
+
103
+ // comments
104
+ export type {
105
+ Comment,
106
+ CommentReaction,
107
+ CommentSyncInfo,
108
+ CommentsResult,
109
+ SyncMeta,
110
+ SlackSyncMeta,
111
+ GithubSyncMeta,
112
+ JiraSyncMeta,
113
+ GenericSyncMeta,
114
+ } from "./comments";
115
+ export {
116
+ getIssueComments,
117
+ updateComment,
118
+ replyToComment,
119
+ deleteComment,
120
+ } from "./comments";
121
+
122
+ // reactions
123
+ export { createReaction, deleteReaction } from "./reactions";
package/src/issues.ts CHANGED
@@ -64,6 +64,7 @@ export async function listIssues(
64
64
  createdAt: n.createdAt,
65
65
  updatedAt: n.updatedAt,
66
66
  url: n.url,
67
+ parentId: (await n.parent)?.id ?? null,
67
68
  }))
68
69
  );
69
70
  }
@@ -81,6 +82,7 @@ export async function getIssue(
81
82
 
82
83
  const state = await issue.state;
83
84
  const assignee = await issue.assignee;
85
+ const parent = await issue.parent;
84
86
 
85
87
  return {
86
88
  id: issue.id,
@@ -93,6 +95,7 @@ export async function getIssue(
93
95
  createdAt: issue.createdAt,
94
96
  updatedAt: issue.updatedAt,
95
97
  url: issue.url,
98
+ parentId: parent?.id ?? null,
96
99
  };
97
100
  } catch {
98
101
  return null;
@@ -180,3 +183,36 @@ export async function getTeamLabels(
180
183
  const labels = await team.labels();
181
184
  return labels.nodes.map((l) => ({ id: l.id, name: l.name }));
182
185
  }
186
+
187
+ export async function archiveIssue(
188
+ client: LinearClient,
189
+ issueId: string
190
+ ): Promise<boolean> {
191
+ const result = await client.archiveIssue(issueId);
192
+ return result.success;
193
+ }
194
+
195
+ export async function getSubIssues(
196
+ client: LinearClient,
197
+ parentIdentifier: string
198
+ ): Promise<Issue[]> {
199
+ const parent = await client.issue(parentIdentifier);
200
+ if (!parent) return [];
201
+
202
+ const children = await parent.children();
203
+ return Promise.all(
204
+ children.nodes.map(async (n) => ({
205
+ id: n.id,
206
+ identifier: n.identifier,
207
+ title: n.title,
208
+ description: n.description,
209
+ state: (await n.state)?.name ?? null,
210
+ assignee: (await n.assignee)?.name ?? null,
211
+ priority: n.priority,
212
+ createdAt: n.createdAt,
213
+ updatedAt: n.updatedAt,
214
+ url: n.url,
215
+ parentId: parent.id,
216
+ }))
217
+ );
218
+ }
package/src/labels.ts ADDED
@@ -0,0 +1,97 @@
1
+ import type { LinearClient } from "@linear/sdk";
2
+
3
+ export interface Label {
4
+ id: string;
5
+ name: string;
6
+ color: string;
7
+ description: string | null;
8
+ }
9
+
10
+ export async function listLabels(
11
+ client: LinearClient,
12
+ teamId?: string
13
+ ): Promise<Label[]> {
14
+ if (teamId) {
15
+ const team = await client.team(teamId);
16
+ if (!team) {
17
+ return [];
18
+ }
19
+ const labelsConnection = await team.labels();
20
+ return labelsConnection.nodes.map((l) => ({
21
+ id: l.id,
22
+ name: l.name,
23
+ color: l.color,
24
+ description: l.description ?? null,
25
+ }));
26
+ }
27
+
28
+ const labelsConnection = await client.issueLabels();
29
+ return labelsConnection.nodes.map((l) => ({
30
+ id: l.id,
31
+ name: l.name,
32
+ color: l.color,
33
+ description: l.description ?? null,
34
+ }));
35
+ }
36
+
37
+ export async function getLabel(
38
+ client: LinearClient,
39
+ id: string
40
+ ): Promise<Label | null> {
41
+ try {
42
+ const label = await client.issueLabel(id);
43
+ return {
44
+ id: label.id,
45
+ name: label.name,
46
+ color: label.color,
47
+ description: label.description ?? null,
48
+ };
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ export async function createLabel(
55
+ client: LinearClient,
56
+ input: { name: string; teamId?: string; color?: string; description?: string }
57
+ ): Promise<Label | null> {
58
+ const payload = await client.createIssueLabel({
59
+ name: input.name,
60
+ teamId: input.teamId,
61
+ color: input.color,
62
+ description: input.description,
63
+ });
64
+
65
+ if (!payload.success) {
66
+ return null;
67
+ }
68
+
69
+ const label = await payload.issueLabel;
70
+ if (!label) {
71
+ return null;
72
+ }
73
+
74
+ return {
75
+ id: label.id,
76
+ name: label.name,
77
+ color: label.color,
78
+ description: label.description ?? null,
79
+ };
80
+ }
81
+
82
+ export async function updateLabel(
83
+ client: LinearClient,
84
+ id: string,
85
+ input: { name?: string; color?: string; description?: string }
86
+ ): Promise<boolean> {
87
+ const payload = await client.updateIssueLabel(id, input);
88
+ return payload.success;
89
+ }
90
+
91
+ export async function deleteLabel(
92
+ client: LinearClient,
93
+ id: string
94
+ ): Promise<boolean> {
95
+ const payload = await client.deleteIssueLabel(id);
96
+ return payload.success;
97
+ }
package/src/me.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { LinearClient } from "@linear/sdk";
2
- import type { User, Issue } from "./types";
1
+ import { LinearClient, PaginationOrderBy } from "@linear/sdk";
2
+ import type { User, Issue, Activity } from "./types";
3
3
 
4
4
  export async function getViewer(client: LinearClient): Promise<User> {
5
5
  const viewer = await client.viewer;
@@ -59,3 +59,37 @@ export async function getMyCreatedIssues(
59
59
  }))
60
60
  );
61
61
  }
62
+
63
+ export async function getMyActivity(
64
+ client: LinearClient,
65
+ limit = 20
66
+ ): Promise<Activity[]> {
67
+ const viewer = await client.viewer;
68
+ const assignedConnection = await viewer.assignedIssues({
69
+ first: limit * 2,
70
+ orderBy: PaginationOrderBy.UpdatedAt,
71
+ });
72
+ const createdConnection = await viewer.createdIssues({
73
+ first: limit * 2,
74
+ orderBy: PaginationOrderBy.UpdatedAt,
75
+ });
76
+
77
+ const issueMap = new Map<string, Activity>();
78
+
79
+ for (const i of [...assignedConnection.nodes, ...createdConnection.nodes]) {
80
+ if (!issueMap.has(i.id)) {
81
+ issueMap.set(i.id, {
82
+ id: i.id,
83
+ identifier: i.identifier,
84
+ title: i.title,
85
+ state: (await i.state)?.name ?? null,
86
+ updatedAt: i.updatedAt,
87
+ url: i.url,
88
+ });
89
+ }
90
+ }
91
+
92
+ return [...issueMap.values()]
93
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
94
+ .slice(0, limit);
95
+ }
@@ -0,0 +1,18 @@
1
+ import type { LinearClient } from "@linear/sdk";
2
+
3
+ export async function createReaction(
4
+ client: LinearClient,
5
+ commentId: string,
6
+ emoji: string
7
+ ): Promise<boolean> {
8
+ const result = await client.createReaction({ commentId, emoji });
9
+ return result.success;
10
+ }
11
+
12
+ export async function deleteReaction(
13
+ client: LinearClient,
14
+ reactionId: string
15
+ ): Promise<boolean> {
16
+ const result = await client.deleteReaction(reactionId);
17
+ return result.success;
18
+ }
@@ -0,0 +1,17 @@
1
+ import { IssueRelationType, type LinearClient } from "@linear/sdk";
2
+
3
+ export async function createIssueRelation(
4
+ client: LinearClient,
5
+ issueId: string,
6
+ relatedIssueId: string,
7
+ type: "blocks" | "related"
8
+ ): Promise<boolean> {
9
+ const relationType =
10
+ type === "blocks" ? IssueRelationType.Blocks : IssueRelationType.Related;
11
+ const result = await client.createIssueRelation({
12
+ issueId,
13
+ relatedIssueId,
14
+ type: relationType,
15
+ });
16
+ return result.success;
17
+ }
package/src/types.ts CHANGED
@@ -9,6 +9,7 @@ export interface Issue {
9
9
  createdAt: Date;
10
10
  updatedAt: Date;
11
11
  url: string;
12
+ parentId?: string | null;
12
13
  }
13
14
 
14
15
  export interface Project {
@@ -71,6 +72,7 @@ export interface CreateIssueInput {
71
72
  assigneeId?: string;
72
73
  priority?: number;
73
74
  labelIds?: string[];
75
+ parentId?: string;
74
76
  }
75
77
 
76
78
  export interface UpdateIssueInput {
@@ -78,6 +80,7 @@ export interface UpdateIssueInput {
78
80
  assigneeId?: string;
79
81
  priority?: number;
80
82
  labelIds?: string[];
83
+ parentId?: string;
81
84
  }
82
85
 
83
86
  export interface CreateProjectInput {
@@ -85,3 +88,12 @@ export interface CreateProjectInput {
85
88
  description?: string;
86
89
  teamIds?: string[];
87
90
  }
91
+
92
+ export interface Activity {
93
+ id: string;
94
+ identifier: string;
95
+ title: string;
96
+ state?: string | null;
97
+ updatedAt: Date;
98
+ url: string;
99
+ }