glab-agent 0.2.6 → 0.2.7

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.
@@ -17,6 +17,12 @@ export interface GitlabTodoItem {
17
17
  targetIid?: number;
18
18
  body?: string;
19
19
  sourceBranch?: string;
20
+ /** Note ID of the @mention comment that created this todo (for emoji reactions) */
21
+ noteId?: number;
22
+ /** GitLab user ID of the todo's author (the person who @mentioned the bot) */
23
+ authorId?: number;
24
+ /** GitLab username of the todo's author */
25
+ authorUsername?: string;
20
26
  }
21
27
 
22
28
  export interface GitlabIssue {
@@ -99,6 +105,11 @@ export interface GitlabClient {
99
105
  createProject(name: string, options?: { visibility?: string; initializeWithReadme?: boolean }): Promise<{ id: number }>;
100
106
  getRepositoryFile(projectId: number | string, filePath: string, ref?: string): Promise<{ content: string } | undefined>;
101
107
  createOrUpdateRepositoryFile(projectId: number | string, filePath: string, content: string, commitMessage: string, options?: { create?: boolean }): Promise<void>;
108
+ addAwardEmoji(projectId: number | string, issueIid: number, noteId: number, emojiName: string): Promise<{ id: number }>;
109
+ removeAwardEmoji(projectId: number | string, issueIid: number, noteId: number, awardId: number): Promise<void>;
110
+ listAwardEmoji(projectId: number | string, issueIid: number, noteId: number): Promise<Array<{ id: number; name: string; user: { id: number } }>>;
111
+ findMergeRequestByBranch(projectId: number | string, sourceBranch: string): Promise<{ iid: number; webUrl: string } | undefined>;
112
+ getUserById(userId: number): Promise<{ id: number; email?: string; username: string }>;
102
113
  }
103
114
 
104
115
  interface GitlabGlabClientOptions {
@@ -224,6 +235,13 @@ export function parseTodoUniversal(payload: Payload): GitlabTodoItem | undefined
224
235
  ? target.source_branch as string
225
236
  : undefined;
226
237
 
238
+ const note = payload.note as Payload | undefined;
239
+ const noteId = parseInteger(note?.id);
240
+
241
+ const author = payload.author as Payload | undefined;
242
+ const authorId = parseInteger(author?.id);
243
+ const authorUsername = typeof author?.username === "string" ? author.username as string : undefined;
244
+
227
245
  return {
228
246
  id,
229
247
  projectId,
@@ -237,7 +255,10 @@ export function parseTodoUniversal(payload: Payload): GitlabTodoItem | undefined
237
255
  createdAt: typeof payload.created_at === "string" ? payload.created_at : undefined,
238
256
  labels: parseLabels(target),
239
257
  body: typeof payload.body === "string" ? payload.body : undefined,
240
- sourceBranch
258
+ sourceBranch,
259
+ noteId,
260
+ authorId,
261
+ authorUsername
241
262
  };
242
263
  }
243
264
 
@@ -749,6 +770,65 @@ export class GitlabGlabClient implements GitlabClient {
749
770
  });
750
771
  }
751
772
 
773
+ async addAwardEmoji(projectId: number | string, issueIid: number, noteId: number, emojiName: string): Promise<{ id: number }> {
774
+ const stdout = await this.request(`projects/${projectId}/issues/${issueIid}/notes/${noteId}/award_emoji`, {
775
+ method: "POST",
776
+ fields: { name: emojiName }
777
+ });
778
+ const payload = JSON.parse(stdout) as Payload;
779
+ return { id: Number(payload.id) };
780
+ }
781
+
782
+ async removeAwardEmoji(projectId: number | string, issueIid: number, noteId: number, awardId: number): Promise<void> {
783
+ await this.request(`projects/${projectId}/issues/${issueIid}/notes/${noteId}/award_emoji/${awardId}`, {
784
+ method: "DELETE",
785
+ fields: {}
786
+ });
787
+ }
788
+
789
+ async listAwardEmoji(projectId: number | string, issueIid: number, noteId: number): Promise<Array<{ id: number; name: string; user: { id: number } }>> {
790
+ const payload = await this.readJson(`projects/${projectId}/issues/${issueIid}/notes/${noteId}/award_emoji`);
791
+ if (!Array.isArray(payload)) return [];
792
+ return payload.map((item: unknown) => {
793
+ const p = (item ?? {}) as Record<string, unknown>;
794
+ return {
795
+ id: Number(p.id),
796
+ name: String(p.name ?? ""),
797
+ user: { id: Number((p.user as Record<string, unknown> | undefined)?.id ?? 0) }
798
+ };
799
+ });
800
+ }
801
+
802
+ async findMergeRequestByBranch(projectId: number | string, sourceBranch: string): Promise<{ iid: number; webUrl: string } | undefined> {
803
+ try {
804
+ const endpoint = this.withQuery(`projects/${projectId}/merge_requests`, {
805
+ source_branch: sourceBranch,
806
+ state: "opened",
807
+ per_page: "1"
808
+ });
809
+ const payload = await this.readJson(endpoint);
810
+ if (!Array.isArray(payload) || payload.length === 0) return undefined;
811
+ const mr = (payload[0] ?? {}) as Record<string, unknown>;
812
+ const iid = Number(mr.iid);
813
+ const webUrl = String(mr.web_url ?? "");
814
+ return (iid && webUrl) ? { iid, webUrl } : undefined;
815
+ } catch {
816
+ return undefined;
817
+ }
818
+ }
819
+
820
+ async getUserById(userId: number): Promise<{ id: number; email?: string; username: string }> {
821
+ const payload = (await this.readJson(`users/${userId}`)) as Payload;
822
+ const p = payload ?? {};
823
+ return {
824
+ id: Number(p.id),
825
+ email: typeof p.email === "string" && p.email
826
+ ? p.email
827
+ : (typeof p.public_email === "string" && p.public_email ? p.public_email : undefined),
828
+ username: typeof p.username === "string" ? p.username : ""
829
+ };
830
+ }
831
+
752
832
  /**
753
833
  * Direct HTTP call using Node's built-in fetch.
754
834
  * Used for endpoints where glab CLI's response parsing has issues (e.g. PUT /user/status).
@@ -798,7 +878,7 @@ export class GitlabGlabClient implements GitlabClient {
798
878
  private async request(
799
879
  endpoint: string,
800
880
  options: {
801
- method?: "GET" | "POST" | "PUT";
881
+ method?: "GET" | "POST" | "PUT" | "DELETE";
802
882
  fields?: Record<string, string>;
803
883
  } = {}
804
884
  ): Promise<string> {
@@ -6,6 +6,7 @@ export interface WebhookPayload {
6
6
  title: string;
7
7
  message: string;
8
8
  url?: string;
9
+ status?: "accepted" | "completed" | "failed" | "busy";
9
10
  }
10
11
 
11
12
  function isFeishuUrl(url: string): boolean {
@@ -16,18 +17,58 @@ function isSlackUrl(url: string): boolean {
16
17
  return url.includes("hooks.slack.com");
17
18
  }
18
19
 
20
+ function buildFeishuCard(payload: WebhookPayload): unknown {
21
+ const statusConfig = {
22
+ accepted: { color: "blue", title: "已接单" },
23
+ completed: { color: "green", title: "已完成" },
24
+ failed: { color: "red", title: "执行失败" },
25
+ busy: { color: "orange", title: "排队中" },
26
+ };
27
+ const cfg = statusConfig[payload.status ?? "accepted"];
28
+
29
+ const elements: unknown[] = [
30
+ {
31
+ tag: "div",
32
+ text: { tag: "lark_md", content: payload.message }
33
+ }
34
+ ];
35
+
36
+ // Add URL button if available
37
+ if (payload.url) {
38
+ elements.push({
39
+ tag: "action",
40
+ actions: [
41
+ {
42
+ tag: "button",
43
+ text: { tag: "plain_text", content: payload.status === "completed" ? "查看 MR" : "查看 Issue" },
44
+ type: "primary",
45
+ url: payload.url
46
+ }
47
+ ]
48
+ });
49
+ }
50
+
51
+ return {
52
+ msg_type: "interactive",
53
+ card: {
54
+ header: {
55
+ title: { tag: "plain_text", content: payload.title },
56
+ template: cfg.color
57
+ },
58
+ elements
59
+ }
60
+ };
61
+ }
62
+
19
63
  function buildWebhookBody(url: string, payload: WebhookPayload): unknown {
64
+ if (isFeishuUrl(url)) {
65
+ return buildFeishuCard(payload);
66
+ }
67
+
20
68
  const text = payload.url
21
69
  ? `${payload.title}\n${payload.message}\n${payload.url}`
22
70
  : `${payload.title}\n${payload.message}`;
23
71
 
24
- if (isFeishuUrl(url)) {
25
- return {
26
- msg_type: "text",
27
- content: { text }
28
- };
29
- }
30
-
31
72
  if (isSlackUrl(url)) {
32
73
  return { text };
33
74
  }
@@ -69,12 +110,15 @@ export async function notifyAccepted(
69
110
  agentName: string,
70
111
  issueIid: number,
71
112
  issueTitle: string,
113
+ issueUrl?: string,
72
114
  webhookUrl?: string
73
115
  ): Promise<void> {
74
116
  if (!webhookUrl) return;
75
117
  await sendWebhook(webhookUrl, {
76
118
  title: `\u{1F916} ${agentName} 已接单`,
77
119
  message: `#${issueIid} ${issueTitle}`,
120
+ url: issueUrl,
121
+ status: "accepted",
78
122
  });
79
123
  }
80
124
 
@@ -90,6 +134,7 @@ export async function notifyCompleted(
90
134
  title: `\u2705 ${agentName} 已完成`,
91
135
  message: `#${issueIid} ${issueTitle}`,
92
136
  url: mrUrl,
137
+ status: "completed",
93
138
  });
94
139
  }
95
140
 
@@ -103,6 +148,24 @@ export async function notifyFailed(
103
148
  if (!webhookUrl) return;
104
149
  await sendWebhook(webhookUrl, {
105
150
  title: `\u274C ${agentName} 执行失败`,
106
- message: `#${issueIid} ${issueTitle}\n${summary.slice(0, 100)}`,
151
+ message: `#${issueIid} ${issueTitle}\n${summary.slice(0, 200)}`,
152
+ status: "failed",
153
+ });
154
+ }
155
+
156
+ export async function notifyQueued(
157
+ agentName: string,
158
+ issueIid: number,
159
+ issueTitle: string,
160
+ queuePosition: number,
161
+ issueUrl?: string,
162
+ webhookUrl?: string
163
+ ): Promise<void> {
164
+ if (!webhookUrl) return;
165
+ await sendWebhook(webhookUrl, {
166
+ title: `\u23F3 ${agentName} 排队中`,
167
+ message: `#${issueIid} ${issueTitle}\n排在第 ${queuePosition} 位`,
168
+ url: issueUrl,
169
+ status: "busy",
107
170
  });
108
171
  }