standup-mr 0.1.2 → 0.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/CHANGELOG.md +77 -0
- package/README.md +112 -23
- package/dist/chunk-MIQ7FQS6.js +1108 -0
- package/dist/cli.js +10 -13
- package/dist/index.d.ts +138 -17
- package/dist/index.js +45 -5
- package/dist/mcp/server.js +598 -81
- package/mcp/README.md +28 -6
- package/package.json +4 -3
- package/skills/standup/SKILL.md +73 -26
- package/dist/chunk-SKCJT2AY.js +0 -551
package/dist/mcp/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { pathToFileURL } from "url";
|
|
|
8
8
|
import { execFileSync } from "child_process";
|
|
9
9
|
|
|
10
10
|
// src/config/config.constants.ts
|
|
11
|
-
var
|
|
11
|
+
var CLI_TIMEOUT_MS = 15e3;
|
|
12
12
|
|
|
13
13
|
// src/config/config.ts
|
|
14
14
|
var ConfigError = class extends Error {
|
|
@@ -17,51 +17,61 @@ var ConfigError = class extends Error {
|
|
|
17
17
|
this.name = "ConfigError";
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
var GITLAB_LABELS = {
|
|
21
|
+
name: "GitLab",
|
|
22
|
+
cli: "glab",
|
|
23
|
+
envHost: "GITLAB_HOST",
|
|
24
|
+
envToken: "GITLAB_TOKEN",
|
|
25
|
+
login: (host) => `glab auth login --hostname ${host}`
|
|
26
|
+
};
|
|
27
|
+
var GITHUB_LABELS = {
|
|
28
|
+
name: "GitHub",
|
|
29
|
+
cli: "gh",
|
|
30
|
+
envHost: "GITHUB_HOST",
|
|
31
|
+
envToken: "GITHUB_TOKEN",
|
|
32
|
+
login: (host) => `gh auth login --hostname ${host}`
|
|
33
|
+
};
|
|
34
|
+
function resolveHost(cliHost, envHost, hostList, labels = GITLAB_LABELS) {
|
|
21
35
|
if (cliHost) return cliHost;
|
|
22
36
|
if (envHost) return envHost;
|
|
23
|
-
const hosts =
|
|
37
|
+
const hosts = hostList ?? [];
|
|
24
38
|
if (hosts.length === 1) return hosts[0];
|
|
25
39
|
if (hosts.length === 0) {
|
|
26
40
|
throw new ConfigError(
|
|
27
|
-
|
|
41
|
+
`No ${labels.name} host configured. Pass --host, set ${labels.envHost}, or log in with \`${labels.cli} auth login\`.`
|
|
28
42
|
);
|
|
29
43
|
}
|
|
30
44
|
throw new ConfigError(
|
|
31
|
-
`Multiple
|
|
45
|
+
`Multiple ${labels.name} hosts found (${[...hosts].sort().join(", ")}). Pick one with --host or ${labels.envHost}.`
|
|
32
46
|
);
|
|
33
47
|
}
|
|
34
|
-
function resolveToken(host, cliToken, envToken,
|
|
48
|
+
function resolveToken(host, cliToken, envToken, lookup, labels = GITLAB_LABELS) {
|
|
35
49
|
if (cliToken) return cliToken;
|
|
36
50
|
if (envToken) return envToken;
|
|
37
|
-
if (
|
|
38
|
-
const token =
|
|
51
|
+
if (lookup) {
|
|
52
|
+
const token = lookup(host);
|
|
39
53
|
if (token) return token;
|
|
40
54
|
}
|
|
41
55
|
throw new ConfigError(
|
|
42
|
-
`No token for ${host}. Pass --token, set
|
|
56
|
+
`No token for ${host}. Pass --token, set ${labels.envToken}, or run \`${labels.login(host)}\`.`
|
|
43
57
|
);
|
|
44
58
|
}
|
|
45
|
-
function
|
|
59
|
+
function cliCapture(cli, args) {
|
|
46
60
|
try {
|
|
47
|
-
return execFileSync(
|
|
61
|
+
return execFileSync(cli, args, {
|
|
48
62
|
encoding: "utf8",
|
|
49
|
-
timeout:
|
|
63
|
+
timeout: CLI_TIMEOUT_MS,
|
|
50
64
|
stdio: ["ignore", "pipe", "ignore"]
|
|
51
65
|
}).trim();
|
|
52
66
|
} catch {
|
|
53
67
|
return "";
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
|
-
function
|
|
57
|
-
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map((match) => match[1]);
|
|
58
|
-
return [...new Set(hosts)].sort();
|
|
59
|
-
}
|
|
60
|
-
function glabStatus() {
|
|
70
|
+
function cliStatus(cli) {
|
|
61
71
|
try {
|
|
62
|
-
return execFileSync(
|
|
72
|
+
return execFileSync(cli, ["auth", "status"], {
|
|
63
73
|
encoding: "utf8",
|
|
64
|
-
timeout:
|
|
74
|
+
timeout: CLI_TIMEOUT_MS,
|
|
65
75
|
stdio: ["ignore", "pipe", "pipe"]
|
|
66
76
|
});
|
|
67
77
|
} catch (error) {
|
|
@@ -69,11 +79,21 @@ function glabStatus() {
|
|
|
69
79
|
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
70
80
|
}
|
|
71
81
|
}
|
|
82
|
+
function parseLoggedInHosts(statusOutput) {
|
|
83
|
+
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map((match) => match[1]);
|
|
84
|
+
return [...new Set(hosts)].sort();
|
|
85
|
+
}
|
|
72
86
|
function glabHosts() {
|
|
73
|
-
return
|
|
87
|
+
return parseLoggedInHosts(cliStatus("glab"));
|
|
74
88
|
}
|
|
75
89
|
function glabToken(host) {
|
|
76
|
-
return glab
|
|
90
|
+
return cliCapture("glab", ["config", "get", "token", "--host", host]);
|
|
91
|
+
}
|
|
92
|
+
function ghHosts() {
|
|
93
|
+
return parseLoggedInHosts(cliStatus("gh"));
|
|
94
|
+
}
|
|
95
|
+
function ghToken(host) {
|
|
96
|
+
return cliCapture("gh", ["auth", "token", "--hostname", host]);
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
// src/buckets/buckets.constants.ts
|
|
@@ -123,6 +143,12 @@ function isoDay(day) {
|
|
|
123
143
|
const date = String(day.getDate()).padStart(2, "0");
|
|
124
144
|
return `${year}-${month}-${date}`;
|
|
125
145
|
}
|
|
146
|
+
function localAt(iso) {
|
|
147
|
+
const at = new Date(iso);
|
|
148
|
+
const hours = String(at.getHours()).padStart(2, "0");
|
|
149
|
+
const minutes = String(at.getMinutes()).padStart(2, "0");
|
|
150
|
+
return `${isoDay(at)}T${hours}:${minutes}`;
|
|
151
|
+
}
|
|
126
152
|
function label(day, lang = "en") {
|
|
127
153
|
const key = DAYS[lang] ? lang : "en";
|
|
128
154
|
const weekday = DAYS[key][day.getDay()];
|
|
@@ -130,15 +156,21 @@ function label(day, lang = "en") {
|
|
|
130
156
|
const date = day.getDate();
|
|
131
157
|
return key === "tr" ? `${date} ${month} ${weekday}` : `${weekday}, ${date} ${month}`;
|
|
132
158
|
}
|
|
133
|
-
function
|
|
159
|
+
function isWeekday(day) {
|
|
160
|
+
const weekday = (/* @__PURE__ */ new Date(`${day}T00:00:00`)).getDay();
|
|
161
|
+
return weekday >= 1 && weekday <= 5;
|
|
162
|
+
}
|
|
163
|
+
function previousActiveDays(eventDates, today) {
|
|
134
164
|
const cutoff = isoDay(today);
|
|
135
|
-
const past = [...eventDates].filter((
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
const past = [...eventDates].filter((day) => day < cutoff).sort();
|
|
166
|
+
if (past.length === 0) return [];
|
|
167
|
+
const anchor = past.filter(isWeekday).pop() ?? past[0];
|
|
168
|
+
return past.filter((day) => day >= anchor).map((day) => ({
|
|
169
|
+
date: day,
|
|
170
|
+
gapDays: Math.round(
|
|
171
|
+
(Date.parse(`${cutoff}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) / MS_PER_DAY
|
|
172
|
+
)
|
|
173
|
+
}));
|
|
142
174
|
}
|
|
143
175
|
|
|
144
176
|
// src/buckets/buckets.ts
|
|
@@ -163,6 +195,9 @@ var SECTION = /section_(start|end):\d+:\S*/g;
|
|
|
163
195
|
var SIGNAL = /(\berror\b|Error:|fatal:|npm ERR!|\bfailed\b)/i;
|
|
164
196
|
var NOISE = /^(Cleaning up|Job failed: exit status|ERROR: Job failed|Uploading artifacts|Job succeeded)/i;
|
|
165
197
|
var MAX_LINE = 200;
|
|
198
|
+
var TIMESTAMP = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+/;
|
|
199
|
+
var ERROR_MARKER = /^##\[error\]\s*/;
|
|
200
|
+
var GROUP = /^##\[(group|endgroup)\]/;
|
|
166
201
|
|
|
167
202
|
// src/trace/trace.ts
|
|
168
203
|
function extractErrors(rawTrace, limit = 8) {
|
|
@@ -171,8 +206,11 @@ function extractErrors(rawTrace, limit = 8) {
|
|
|
171
206
|
const seen = /* @__PURE__ */ new Set();
|
|
172
207
|
const hits = [];
|
|
173
208
|
for (const rawLine of cleaned.split("\n")) {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
209
|
+
const stamped = rawLine.trim().replace(TIMESTAMP, "");
|
|
210
|
+
if (GROUP.test(stamped)) continue;
|
|
211
|
+
if (!stamped || NOISE.test(stamped) || !SIGNAL.test(stamped)) continue;
|
|
212
|
+
const line = stamped.replace(ERROR_MARKER, "").trim();
|
|
213
|
+
if (!line) continue;
|
|
176
214
|
const clipped = line.slice(0, MAX_LINE);
|
|
177
215
|
if (seen.has(clipped)) continue;
|
|
178
216
|
seen.add(clipped);
|
|
@@ -181,64 +219,465 @@ function extractErrors(rawTrace, limit = 8) {
|
|
|
181
219
|
return hits.slice(-limit);
|
|
182
220
|
}
|
|
183
221
|
|
|
184
|
-
// src/providers/
|
|
222
|
+
// src/providers/base/http.ts
|
|
223
|
+
function buildUrl(api, path, params) {
|
|
224
|
+
const base = `${api}/${path}`;
|
|
225
|
+
if (!params || Object.keys(params).length === 0) return base;
|
|
226
|
+
const query = new URLSearchParams();
|
|
227
|
+
for (const [key, value] of Object.entries(params)) {
|
|
228
|
+
query.set(key, String(value));
|
|
229
|
+
}
|
|
230
|
+
return `${base}?${query.toString()}`;
|
|
231
|
+
}
|
|
232
|
+
var ApiError = class extends Error {
|
|
233
|
+
constructor(message) {
|
|
234
|
+
super(message);
|
|
235
|
+
this.name = "ApiError";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function remaining(response) {
|
|
239
|
+
return response.headers.get("x-ratelimit-remaining") ?? response.headers.get("ratelimit-remaining");
|
|
240
|
+
}
|
|
241
|
+
function resetAt(response) {
|
|
242
|
+
const reset = response.headers.get("x-ratelimit-reset") ?? response.headers.get("ratelimit-reset");
|
|
243
|
+
if (!reset) return "";
|
|
244
|
+
const seconds = Number(reset);
|
|
245
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return "";
|
|
246
|
+
return `; resets at ${new Date(seconds * 1e3).toISOString()}`;
|
|
247
|
+
}
|
|
248
|
+
function retryAfter(response) {
|
|
249
|
+
const value = response.headers.get("retry-after");
|
|
250
|
+
if (!value) return "";
|
|
251
|
+
const seconds = Number(value);
|
|
252
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return `; retry after ${value}`;
|
|
253
|
+
return `; retry after ${seconds}s`;
|
|
254
|
+
}
|
|
255
|
+
function assertUsable(response, host) {
|
|
256
|
+
if (response.ok || response.status === 404) return;
|
|
257
|
+
if (response.status === 403 || response.status === 429) {
|
|
258
|
+
if (remaining(response) === "0") {
|
|
259
|
+
throw new ApiError(`${host} rate limit reached${resetAt(response)}.`);
|
|
260
|
+
}
|
|
261
|
+
const retry = retryAfter(response);
|
|
262
|
+
if (retry) throw new ApiError(`${host} rate limit reached${retry}.`);
|
|
263
|
+
}
|
|
264
|
+
if (response.status === 401) {
|
|
265
|
+
throw new ApiError(
|
|
266
|
+
`${host} rejected the token (401). Check the token and its scopes.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (response.status === 403) {
|
|
270
|
+
throw new ApiError(
|
|
271
|
+
`${host} refused the request (403). The token has no access to that resource.`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
throw new ApiError(`${host} returned ${response.status}.`);
|
|
275
|
+
}
|
|
276
|
+
function unreachable(host, cause) {
|
|
277
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
278
|
+
return new ApiError(`Could not reach ${host}: ${detail}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/providers/github/github.constants.ts
|
|
185
282
|
var PAGE_SIZE = 100;
|
|
186
283
|
var FRESH_REVIEW_DAYS = 7;
|
|
284
|
+
var API_VERSION = "2022-11-28";
|
|
285
|
+
var DOT_COM = "github.com";
|
|
286
|
+
var SEARCH_CAP = 3;
|
|
287
|
+
var EVENT_FEED_CAP = 300;
|
|
288
|
+
var EVENTS_PAGE_CAP = EVENT_FEED_CAP / PAGE_SIZE;
|
|
289
|
+
var FAILED_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
290
|
+
"failure",
|
|
291
|
+
"timed_out",
|
|
292
|
+
"startup_failure",
|
|
293
|
+
"action_required"
|
|
294
|
+
]);
|
|
295
|
+
var EVENT_ACTIONS = {
|
|
296
|
+
PushEvent: "pushed to",
|
|
297
|
+
PullRequestReviewEvent: "reviewed",
|
|
298
|
+
PullRequestReviewCommentEvent: "commented on",
|
|
299
|
+
IssueCommentEvent: "commented on",
|
|
300
|
+
CreateEvent: "created",
|
|
301
|
+
DeleteEvent: "deleted"
|
|
302
|
+
};
|
|
303
|
+
var EVENT_TARGET_TYPES = {
|
|
304
|
+
PullRequestEvent: "MergeRequest",
|
|
305
|
+
PullRequestReviewEvent: "MergeRequest",
|
|
306
|
+
PullRequestReviewCommentEvent: "MergeRequest",
|
|
307
|
+
IssueCommentEvent: "Note"
|
|
308
|
+
};
|
|
187
309
|
|
|
188
|
-
// src/providers/
|
|
189
|
-
|
|
310
|
+
// src/providers/github/github.map.ts
|
|
311
|
+
function repoFromUrl(repositoryUrl) {
|
|
312
|
+
const match = /\/repos\/([^/]+\/[^/]+)/.exec(repositoryUrl);
|
|
313
|
+
return match ? match[1] : "";
|
|
314
|
+
}
|
|
315
|
+
function mapEvent(raw) {
|
|
316
|
+
const payload = raw.payload ?? {};
|
|
317
|
+
const type = raw.type ?? "";
|
|
318
|
+
const commits = payload.commits ?? [];
|
|
319
|
+
const last = commits[commits.length - 1];
|
|
320
|
+
return {
|
|
321
|
+
at: localAt(raw.created_at),
|
|
322
|
+
action: type === "PullRequestEvent" ? String(payload.action ?? "updated") : EVENT_ACTIONS[type] ?? type.replace(/Event$/, "").toLowerCase(),
|
|
323
|
+
project: raw.repo?.name ?? "",
|
|
324
|
+
targetType: EVENT_TARGET_TYPES[type] ?? "",
|
|
325
|
+
title: payload.pull_request?.title ?? payload.issue?.title ?? "",
|
|
326
|
+
branch: String(payload.ref ?? "").replace(/^refs\/heads\//, ""),
|
|
327
|
+
commits: Number(payload.size ?? 0),
|
|
328
|
+
commitTitle: last ? String(last.message ?? "").split("\n")[0] : ""
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/providers/github/github.state.ts
|
|
333
|
+
function normalizeChecks(runs) {
|
|
334
|
+
if (runs.length === 0) return { pipeline: null, pipelineId: null };
|
|
335
|
+
const failed = runs.find(
|
|
336
|
+
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
337
|
+
);
|
|
338
|
+
if (failed) return { pipeline: "failed", pipelineId: failed.id };
|
|
339
|
+
if (runs.some((run) => run.status !== "completed")) {
|
|
340
|
+
return { pipeline: "running", pipelineId: null };
|
|
341
|
+
}
|
|
342
|
+
if (runs.some((run) => run.conclusion === "cancelled")) {
|
|
343
|
+
return { pipeline: "canceled", pipelineId: null };
|
|
344
|
+
}
|
|
345
|
+
return { pipeline: "success", pipelineId: null };
|
|
346
|
+
}
|
|
347
|
+
function latestStateByReviewer(reviews) {
|
|
348
|
+
const latest = /* @__PURE__ */ new Map();
|
|
349
|
+
for (const review of reviews) {
|
|
350
|
+
const login = review.user?.login;
|
|
351
|
+
if (!login) continue;
|
|
352
|
+
if (review.state === "COMMENTED" || review.state === "PENDING") continue;
|
|
353
|
+
latest.set(login, review.state);
|
|
354
|
+
}
|
|
355
|
+
return latest;
|
|
356
|
+
}
|
|
357
|
+
function countChangesRequested(reviews) {
|
|
358
|
+
return [...latestStateByReviewer(reviews).values()].filter(
|
|
359
|
+
(state) => state === "CHANGES_REQUESTED"
|
|
360
|
+
).length;
|
|
361
|
+
}
|
|
362
|
+
function approvedBy(reviews, login) {
|
|
363
|
+
return latestStateByReviewer(reviews).get(login) === "APPROVED";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/providers/github/github.ts
|
|
367
|
+
var GitHubProvider = class {
|
|
368
|
+
kind = "github";
|
|
190
369
|
host;
|
|
191
370
|
api;
|
|
192
371
|
token;
|
|
193
372
|
fetchImpl;
|
|
373
|
+
identity = null;
|
|
194
374
|
constructor(host, token, fetchImpl = fetch) {
|
|
195
375
|
this.host = host;
|
|
376
|
+
this.api = host === DOT_COM ? "https://api.github.com" : `https://${host}/api/v3`;
|
|
196
377
|
this.token = token;
|
|
197
|
-
this.api = `https://${host}/api/v4`;
|
|
198
378
|
this.fetchImpl = fetchImpl;
|
|
199
379
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
380
|
+
headers() {
|
|
381
|
+
return {
|
|
382
|
+
authorization: `Bearer ${this.token}`,
|
|
383
|
+
accept: "application/vnd.github+json",
|
|
384
|
+
"x-github-api-version": API_VERSION
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
async send(url, init) {
|
|
388
|
+
try {
|
|
389
|
+
return await this.fetchImpl(url, { headers: this.headers(), ...init });
|
|
390
|
+
} catch (cause) {
|
|
391
|
+
throw unreachable(this.host, cause);
|
|
206
392
|
}
|
|
207
|
-
return `${base}?${query.toString()}`;
|
|
208
393
|
}
|
|
209
394
|
async getJson(path, params) {
|
|
395
|
+
const response = await this.send(buildUrl(this.api, path, params));
|
|
396
|
+
assertUsable(response, this.host);
|
|
397
|
+
if (!response.ok) return null;
|
|
210
398
|
try {
|
|
211
|
-
const response = await this.fetchImpl(this.url(path, params), {
|
|
212
|
-
headers: { "PRIVATE-TOKEN": this.token }
|
|
213
|
-
});
|
|
214
|
-
if (!response.ok) return null;
|
|
215
399
|
return await response.json();
|
|
216
|
-
} catch {
|
|
217
|
-
|
|
400
|
+
} catch (cause) {
|
|
401
|
+
throw unreachable(this.host, cause);
|
|
218
402
|
}
|
|
219
403
|
}
|
|
220
|
-
async
|
|
404
|
+
async getPaged(path, params = {}, cap = 5) {
|
|
405
|
+
const rows = [];
|
|
406
|
+
for (let page = 1; page <= cap; page += 1) {
|
|
407
|
+
const chunk = await this.getJson(path, {
|
|
408
|
+
...params,
|
|
409
|
+
per_page: PAGE_SIZE,
|
|
410
|
+
page
|
|
411
|
+
});
|
|
412
|
+
if (!chunk || chunk.length === 0) break;
|
|
413
|
+
rows.push(...chunk);
|
|
414
|
+
if (chunk.length < PAGE_SIZE) break;
|
|
415
|
+
}
|
|
416
|
+
return rows;
|
|
417
|
+
}
|
|
418
|
+
async getSearch(query, cap = 3) {
|
|
419
|
+
const rows = [];
|
|
420
|
+
for (let page = 1; page <= cap; page += 1) {
|
|
421
|
+
const chunk = await this.getJson("search/issues", {
|
|
422
|
+
q: query,
|
|
423
|
+
per_page: PAGE_SIZE,
|
|
424
|
+
page
|
|
425
|
+
});
|
|
426
|
+
const items = chunk?.items ?? [];
|
|
427
|
+
if (items.length === 0) break;
|
|
428
|
+
rows.push(...items);
|
|
429
|
+
if (items.length < PAGE_SIZE) break;
|
|
430
|
+
}
|
|
431
|
+
return rows;
|
|
432
|
+
}
|
|
433
|
+
async getLogText(path) {
|
|
434
|
+
const first = await this.send(buildUrl(this.api, path), { redirect: "manual" });
|
|
435
|
+
if (first.status === 404) return "";
|
|
436
|
+
const location = first.headers.get("location");
|
|
437
|
+
if (!location) {
|
|
438
|
+
if (first.status === 0 || first.status >= 300 && first.status < 400) return "";
|
|
439
|
+
assertUsable(first, this.host);
|
|
440
|
+
return first.ok ? await first.text() : "";
|
|
441
|
+
}
|
|
442
|
+
let blob;
|
|
443
|
+
try {
|
|
444
|
+
blob = await this.fetchImpl(location);
|
|
445
|
+
} catch (cause) {
|
|
446
|
+
throw unreachable(this.host, cause);
|
|
447
|
+
}
|
|
448
|
+
if (!blob.ok) return "";
|
|
449
|
+
return await blob.text();
|
|
450
|
+
}
|
|
451
|
+
async getIdentity() {
|
|
452
|
+
if (this.identity) return this.identity;
|
|
453
|
+
const me = await this.getJson("user");
|
|
454
|
+
if (!me) {
|
|
455
|
+
throw new ApiError(
|
|
456
|
+
`Could not read the ${this.host} user. Check the host, the token, and network access.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
this.identity = { id: me.id, username: me.login };
|
|
460
|
+
return this.identity;
|
|
461
|
+
}
|
|
462
|
+
async getEvents(since) {
|
|
463
|
+
const login = (await this.getIdentity()).username;
|
|
464
|
+
const raw = await this.getPaged(
|
|
465
|
+
`users/${encodeURIComponent(login)}/events`,
|
|
466
|
+
{},
|
|
467
|
+
EVENTS_PAGE_CAP
|
|
468
|
+
);
|
|
469
|
+
if (raw.length === 0) {
|
|
470
|
+
process.stderr.write(
|
|
471
|
+
`Warning: the ${this.host} events feed returned nothing. Private activity is only visible to a token that belongs to the same account.
|
|
472
|
+
`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const floor = isoDay(since);
|
|
476
|
+
return raw.map(mapEvent).filter((event) => event.at.slice(0, 10) >= floor).sort((a, b) => a.at.localeCompare(b.at));
|
|
477
|
+
}
|
|
478
|
+
async shapePr(item) {
|
|
479
|
+
const project = repoFromUrl(item.repository_url);
|
|
480
|
+
const iid = item.number;
|
|
481
|
+
const pull = await this.getJson(`repos/${project}/pulls/${iid}`);
|
|
482
|
+
if (!pull) return null;
|
|
483
|
+
const sha = pull.head?.sha ?? "";
|
|
484
|
+
const [checks, reviews] = await Promise.all([
|
|
485
|
+
sha ? this.getJson(
|
|
486
|
+
`repos/${project}/commits/${sha}/check-runs`,
|
|
487
|
+
{ per_page: PAGE_SIZE }
|
|
488
|
+
) : Promise.resolve(null),
|
|
489
|
+
this.getPaged(`repos/${project}/pulls/${iid}/reviews`, {}, 2)
|
|
490
|
+
]);
|
|
491
|
+
const { pipeline, pipelineId } = normalizeChecks(checks?.check_runs ?? []);
|
|
492
|
+
return {
|
|
493
|
+
provider: "github",
|
|
494
|
+
project,
|
|
495
|
+
projectId: pull.base?.repo?.id ?? 0,
|
|
496
|
+
iid,
|
|
497
|
+
title: pull.title,
|
|
498
|
+
draft: pull.draft,
|
|
499
|
+
branch: pull.head?.ref ?? "",
|
|
500
|
+
target: pull.base?.ref ?? "",
|
|
501
|
+
updated: localAt(pull.updated_at).slice(0, 10),
|
|
502
|
+
url: pull.html_url,
|
|
503
|
+
mergeStatus: pull.mergeable_state ?? null,
|
|
504
|
+
pipeline,
|
|
505
|
+
pipelineId,
|
|
506
|
+
unresolved: countChangesRequested(reviews),
|
|
507
|
+
pipelineMissing: false,
|
|
508
|
+
bucket: "ready"
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async getMyMrs(today) {
|
|
512
|
+
const login = (await this.getIdentity()).username;
|
|
513
|
+
const items = await this.getSearch(
|
|
514
|
+
`is:pr is:open author:${login} archived:false`,
|
|
515
|
+
SEARCH_CAP
|
|
516
|
+
);
|
|
517
|
+
const shaped = await Promise.all(items.map((item) => this.shapePr(item)));
|
|
518
|
+
const rows = shaped.filter((row) => row !== null);
|
|
519
|
+
markMissingPipelines(rows);
|
|
520
|
+
for (const row of rows) {
|
|
521
|
+
row.bucket = classify(row, today);
|
|
522
|
+
}
|
|
523
|
+
return rows;
|
|
524
|
+
}
|
|
525
|
+
async getReviews(identity, today) {
|
|
526
|
+
const items = await this.getSearch(
|
|
527
|
+
`is:pr is:open review-requested:${identity.username}`,
|
|
528
|
+
SEARCH_CAP
|
|
529
|
+
);
|
|
530
|
+
const cutoff = isoDay(new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY));
|
|
531
|
+
const rows = await Promise.all(
|
|
532
|
+
items.map(async (item) => {
|
|
533
|
+
const project = repoFromUrl(item.repository_url);
|
|
534
|
+
const reviews = await this.getPaged(
|
|
535
|
+
`repos/${project}/pulls/${item.number}/reviews`,
|
|
536
|
+
{},
|
|
537
|
+
2
|
|
538
|
+
);
|
|
539
|
+
const updated = localAt(item.updated_at).slice(0, 10);
|
|
540
|
+
return {
|
|
541
|
+
provider: "github",
|
|
542
|
+
project,
|
|
543
|
+
iid: item.number,
|
|
544
|
+
title: item.title,
|
|
545
|
+
author: item.user?.login ?? "",
|
|
546
|
+
updated,
|
|
547
|
+
draft: item.draft ?? false,
|
|
548
|
+
url: item.html_url,
|
|
549
|
+
fresh: updated >= cutoff,
|
|
550
|
+
approvedByMe: approvedBy(reviews, identity.username)
|
|
551
|
+
};
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
555
|
+
}
|
|
556
|
+
async actionsBlocker(mr, sha) {
|
|
557
|
+
const runs = await this.getJson(
|
|
558
|
+
`repos/${mr.project}/actions/runs`,
|
|
559
|
+
{ head_sha: sha, per_page: 10 }
|
|
560
|
+
);
|
|
561
|
+
const run = (runs?.workflow_runs ?? []).find(
|
|
562
|
+
(row) => row.conclusion !== null && FAILED_CONCLUSIONS.has(row.conclusion)
|
|
563
|
+
);
|
|
564
|
+
if (!run) return null;
|
|
565
|
+
const jobs = await this.getJson(
|
|
566
|
+
`repos/${mr.project}/actions/runs/${run.id}/jobs`,
|
|
567
|
+
{ per_page: PAGE_SIZE }
|
|
568
|
+
);
|
|
569
|
+
const job = (jobs?.jobs ?? []).find(
|
|
570
|
+
(row) => row.conclusion !== null && FAILED_CONCLUSIONS.has(row.conclusion)
|
|
571
|
+
);
|
|
572
|
+
if (!job) return null;
|
|
573
|
+
const log = await this.getLogText(
|
|
574
|
+
`repos/${mr.project}/actions/jobs/${job.id}/logs`
|
|
575
|
+
);
|
|
576
|
+
return {
|
|
577
|
+
provider: "github",
|
|
578
|
+
project: mr.project,
|
|
579
|
+
mr: mr.iid,
|
|
580
|
+
title: mr.title,
|
|
581
|
+
job: job.name,
|
|
582
|
+
stage: run.name ?? "",
|
|
583
|
+
url: mr.url,
|
|
584
|
+
errors: extractErrors(log)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async checkRunBlocker(mr, sha) {
|
|
588
|
+
const checks = await this.getJson(
|
|
589
|
+
`repos/${mr.project}/commits/${sha}/check-runs`,
|
|
590
|
+
{ per_page: PAGE_SIZE }
|
|
591
|
+
);
|
|
592
|
+
const failed = (checks?.check_runs ?? []).find(
|
|
593
|
+
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
594
|
+
);
|
|
595
|
+
if (!failed) return null;
|
|
596
|
+
const summary = [failed.output?.summary ?? "", failed.output?.text ?? ""].filter(Boolean).join("\n");
|
|
597
|
+
const errors = extractErrors(summary);
|
|
598
|
+
if (errors.length === 0) return null;
|
|
599
|
+
return {
|
|
600
|
+
provider: "github",
|
|
601
|
+
project: mr.project,
|
|
602
|
+
mr: mr.iid,
|
|
603
|
+
title: mr.title,
|
|
604
|
+
job: failed.name,
|
|
605
|
+
stage: failed.app?.slug ?? "check",
|
|
606
|
+
url: mr.url,
|
|
607
|
+
errors
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async diagnose(mr) {
|
|
611
|
+
const pull = await this.getJson(
|
|
612
|
+
`repos/${mr.project}/pulls/${mr.iid}`
|
|
613
|
+
);
|
|
614
|
+
const sha = pull?.head?.sha;
|
|
615
|
+
if (!sha) return null;
|
|
616
|
+
const actions = await this.actionsBlocker(mr, sha);
|
|
617
|
+
if (actions?.errors.length) return actions;
|
|
618
|
+
return await this.checkRunBlocker(mr, sha) ?? actions;
|
|
619
|
+
}
|
|
620
|
+
async getBlockers(mrs) {
|
|
621
|
+
const red = mrs.filter((mr) => mr.pipeline === "failed");
|
|
622
|
+
const diagnosed = await Promise.all(red.map((mr) => this.diagnose(mr)));
|
|
623
|
+
return diagnosed.filter((row) => row !== null);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// src/providers/gitlab/gitlab.constants.ts
|
|
628
|
+
var PAGE_SIZE2 = 100;
|
|
629
|
+
var FRESH_REVIEW_DAYS2 = 7;
|
|
630
|
+
|
|
631
|
+
// src/providers/gitlab/gitlab.ts
|
|
632
|
+
var GitLabProvider = class {
|
|
633
|
+
kind = "gitlab";
|
|
634
|
+
host;
|
|
635
|
+
api;
|
|
636
|
+
token;
|
|
637
|
+
fetchImpl;
|
|
638
|
+
constructor(host, token, fetchImpl = fetch) {
|
|
639
|
+
this.host = host;
|
|
640
|
+
this.token = token;
|
|
641
|
+
this.api = `https://${host}/api/v4`;
|
|
642
|
+
this.fetchImpl = fetchImpl;
|
|
643
|
+
}
|
|
644
|
+
async send(url) {
|
|
645
|
+
let response;
|
|
221
646
|
try {
|
|
222
|
-
|
|
647
|
+
response = await this.fetchImpl(url, {
|
|
223
648
|
headers: { "PRIVATE-TOKEN": this.token }
|
|
224
649
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} catch {
|
|
228
|
-
return "";
|
|
650
|
+
} catch (cause) {
|
|
651
|
+
throw unreachable(this.host, cause);
|
|
229
652
|
}
|
|
653
|
+
assertUsable(response, this.host);
|
|
654
|
+
return response;
|
|
655
|
+
}
|
|
656
|
+
async getJson(path, params) {
|
|
657
|
+
const response = await this.send(buildUrl(this.api, path, params));
|
|
658
|
+
if (!response.ok) return null;
|
|
659
|
+
try {
|
|
660
|
+
return await response.json();
|
|
661
|
+
} catch (cause) {
|
|
662
|
+
throw unreachable(this.host, cause);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async getText(path) {
|
|
666
|
+
const response = await this.send(buildUrl(this.api, path));
|
|
667
|
+
if (!response.ok) return "";
|
|
668
|
+
return await response.text();
|
|
230
669
|
}
|
|
231
670
|
async getPaged(path, params = {}, cap = 5) {
|
|
232
671
|
const rows = [];
|
|
233
672
|
for (let page = 1; page <= cap; page += 1) {
|
|
234
673
|
const chunk = await this.getJson(path, {
|
|
235
674
|
...params,
|
|
236
|
-
per_page:
|
|
675
|
+
per_page: PAGE_SIZE2,
|
|
237
676
|
page
|
|
238
677
|
});
|
|
239
678
|
if (!chunk || chunk.length === 0) break;
|
|
240
679
|
rows.push(...chunk);
|
|
241
|
-
if (chunk.length <
|
|
680
|
+
if (chunk.length < PAGE_SIZE2) break;
|
|
242
681
|
}
|
|
243
682
|
return rows;
|
|
244
683
|
}
|
|
@@ -284,7 +723,7 @@ var GitLabProvider = class {
|
|
|
284
723
|
async countUnresolved(projectId, iid) {
|
|
285
724
|
const discussions = await this.getJson(
|
|
286
725
|
`projects/${projectId}/merge_requests/${iid}/discussions`,
|
|
287
|
-
{ per_page:
|
|
726
|
+
{ per_page: PAGE_SIZE2 }
|
|
288
727
|
);
|
|
289
728
|
if (!discussions) return 0;
|
|
290
729
|
return discussions.filter((discussion) => {
|
|
@@ -304,6 +743,7 @@ var GitLabProvider = class {
|
|
|
304
743
|
]);
|
|
305
744
|
const latest = pipelines?.[0];
|
|
306
745
|
return {
|
|
746
|
+
provider: "gitlab",
|
|
307
747
|
project: String(mr.references.full).split("!")[0],
|
|
308
748
|
projectId,
|
|
309
749
|
iid,
|
|
@@ -338,15 +778,16 @@ var GitLabProvider = class {
|
|
|
338
778
|
if (!approvals) return false;
|
|
339
779
|
return (approvals.approved_by ?? []).some((entry2) => entry2.user?.id === uid);
|
|
340
780
|
}
|
|
341
|
-
async getReviews(
|
|
781
|
+
async getReviews(identity, today) {
|
|
342
782
|
const raw = await this.getPaged("merge_requests", {
|
|
343
783
|
scope: "all",
|
|
344
784
|
state: "opened",
|
|
345
|
-
reviewer_id:
|
|
785
|
+
reviewer_id: identity.id
|
|
346
786
|
});
|
|
347
|
-
const cutoff = isoDay(new Date(today.getTime() -
|
|
787
|
+
const cutoff = isoDay(new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY));
|
|
348
788
|
const rows = await Promise.all(
|
|
349
789
|
raw.map(async (mr) => ({
|
|
790
|
+
provider: "gitlab",
|
|
350
791
|
project: String(mr.references.full).split("!")[0],
|
|
351
792
|
iid: mr.iid,
|
|
352
793
|
title: mr.title,
|
|
@@ -355,7 +796,7 @@ var GitLabProvider = class {
|
|
|
355
796
|
draft: mr.draft,
|
|
356
797
|
url: mr.web_url,
|
|
357
798
|
fresh: String(mr.updated_at).slice(0, 10) >= cutoff,
|
|
358
|
-
approvedByMe: await this.approvedByMe(mr.project_id, mr.iid,
|
|
799
|
+
approvedByMe: await this.approvedByMe(mr.project_id, mr.iid, identity.id)
|
|
359
800
|
}))
|
|
360
801
|
);
|
|
361
802
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
@@ -366,7 +807,7 @@ var GitLabProvider = class {
|
|
|
366
807
|
red.map(async (mr) => {
|
|
367
808
|
const jobs = await this.getJson(
|
|
368
809
|
`projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
|
|
369
|
-
{ per_page:
|
|
810
|
+
{ per_page: PAGE_SIZE2 }
|
|
370
811
|
) ?? [];
|
|
371
812
|
const job = jobs.find((j) => j.status === "failed");
|
|
372
813
|
if (!job) return null;
|
|
@@ -374,6 +815,7 @@ var GitLabProvider = class {
|
|
|
374
815
|
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
375
816
|
);
|
|
376
817
|
return {
|
|
818
|
+
provider: "gitlab",
|
|
377
819
|
project: mr.project,
|
|
378
820
|
mr: mr.iid,
|
|
379
821
|
title: mr.title,
|
|
@@ -388,6 +830,86 @@ var GitLabProvider = class {
|
|
|
388
830
|
}
|
|
389
831
|
};
|
|
390
832
|
|
|
833
|
+
// src/providers/select.ts
|
|
834
|
+
var DEFAULT_PROBE = { gitlab: glabHosts, github: ghHosts };
|
|
835
|
+
function hostFrom(cliHost, envHost, probe, fallback, labels) {
|
|
836
|
+
if (cliHost) return cliHost;
|
|
837
|
+
if (envHost) return envHost;
|
|
838
|
+
const found = probe();
|
|
839
|
+
return resolveHost(void 0, void 0, found.length > 0 ? found : fallback, labels);
|
|
840
|
+
}
|
|
841
|
+
function chooseKind(options = {}) {
|
|
842
|
+
const env = options.env ?? process.env;
|
|
843
|
+
const probe = options.probe ?? DEFAULT_PROBE;
|
|
844
|
+
if (options.provider) {
|
|
845
|
+
if (options.provider === "github" || options.provider === "gitlab") {
|
|
846
|
+
return options.provider;
|
|
847
|
+
}
|
|
848
|
+
throw new ConfigError(
|
|
849
|
+
`Unknown provider "${options.provider}" for --provider. Use github or gitlab.`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if (options.host) {
|
|
853
|
+
const host = options.host.toLowerCase();
|
|
854
|
+
if (host === DOT_COM || host.includes("github")) return "github";
|
|
855
|
+
if (host === "gitlab.com" || host.includes("gitlab")) return "gitlab";
|
|
856
|
+
}
|
|
857
|
+
if (env.STANDUP_PROVIDER) {
|
|
858
|
+
if (env.STANDUP_PROVIDER === "github" || env.STANDUP_PROVIDER === "gitlab") {
|
|
859
|
+
return env.STANDUP_PROVIDER;
|
|
860
|
+
}
|
|
861
|
+
throw new ConfigError(
|
|
862
|
+
`Unknown provider "${env.STANDUP_PROVIDER}" for STANDUP_PROVIDER. Use github or gitlab.`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const hasGitHubEnv = Boolean(env.GITHUB_HOST || env.GITHUB_TOKEN);
|
|
866
|
+
const hasGitLabEnv = Boolean(env.GITLAB_HOST || env.GITLAB_TOKEN);
|
|
867
|
+
if (hasGitHubEnv !== hasGitLabEnv) return hasGitHubEnv ? "github" : "gitlab";
|
|
868
|
+
if (hasGitHubEnv && hasGitLabEnv) {
|
|
869
|
+
const ghHost = Boolean(env.GITHUB_HOST);
|
|
870
|
+
const glabHost = Boolean(env.GITLAB_HOST);
|
|
871
|
+
if (ghHost !== glabHost) return ghHost ? "github" : "gitlab";
|
|
872
|
+
throw new ConfigError(
|
|
873
|
+
"Both GITHUB_* and GITLAB_* are configured. Pass --provider github or --provider gitlab."
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
const ghLoggedIn = probe.github().length > 0;
|
|
877
|
+
const glabLoggedIn = probe.gitlab().length > 0;
|
|
878
|
+
if (ghLoggedIn !== glabLoggedIn) return ghLoggedIn ? "github" : "gitlab";
|
|
879
|
+
if (ghLoggedIn && glabLoggedIn) {
|
|
880
|
+
throw new ConfigError(
|
|
881
|
+
"Both gh and glab are authenticated. Pass --provider github or --provider gitlab."
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
throw new ConfigError(
|
|
885
|
+
"No provider configured. Pass --provider with --host and --token, set GITHUB_* or GITLAB_* environment variables, or log in with gh or glab."
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
function connect(options = {}) {
|
|
889
|
+
const env = options.env ?? process.env;
|
|
890
|
+
const probe = options.probe ?? DEFAULT_PROBE;
|
|
891
|
+
if (chooseKind(options) === "github") {
|
|
892
|
+
const host2 = hostFrom(options.host, env.GITHUB_HOST, probe.github, [DOT_COM], GITHUB_LABELS);
|
|
893
|
+
const token2 = resolveToken(
|
|
894
|
+
host2,
|
|
895
|
+
options.token,
|
|
896
|
+
env.GITHUB_TOKEN,
|
|
897
|
+
ghToken,
|
|
898
|
+
GITHUB_LABELS
|
|
899
|
+
);
|
|
900
|
+
return new GitHubProvider(host2, token2);
|
|
901
|
+
}
|
|
902
|
+
const host = hostFrom(options.host, env.GITLAB_HOST, probe.gitlab, [], GITLAB_LABELS);
|
|
903
|
+
const token = resolveToken(
|
|
904
|
+
host,
|
|
905
|
+
options.token,
|
|
906
|
+
env.GITLAB_TOKEN,
|
|
907
|
+
glabToken,
|
|
908
|
+
GITLAB_LABELS
|
|
909
|
+
);
|
|
910
|
+
return new GitLabProvider(host, token);
|
|
911
|
+
}
|
|
912
|
+
|
|
391
913
|
// src/report/report.constants.ts
|
|
392
914
|
var LOOKBACK_DAYS = 21;
|
|
393
915
|
|
|
@@ -396,27 +918,27 @@ async function buildReport(provider, today, lang = "en", lookbackDays = LOOKBACK
|
|
|
396
918
|
const identity = await provider.getIdentity();
|
|
397
919
|
const since = new Date(today.getTime() - lookbackDays * MS_PER_DAY);
|
|
398
920
|
const events = await provider.getEvents(since);
|
|
399
|
-
const
|
|
921
|
+
const days = previousActiveDays(
|
|
400
922
|
new Set(events.map((e) => e.at.slice(0, 10))),
|
|
401
923
|
today
|
|
402
924
|
);
|
|
403
|
-
const
|
|
925
|
+
const previousDays = days.map(({ date, gapDays }) => ({
|
|
926
|
+
date,
|
|
927
|
+
label: label(/* @__PURE__ */ new Date(`${date}T00:00:00`), lang),
|
|
928
|
+
gapDays,
|
|
929
|
+
events: events.filter((e) => e.at.slice(0, 10) === date)
|
|
930
|
+
}));
|
|
404
931
|
const todayEvents = events.filter((e) => e.at.slice(0, 10) === isoDay(today));
|
|
405
932
|
const myMrs = await provider.getMyMrs(today);
|
|
406
933
|
const [reviews, blockers] = await Promise.all([
|
|
407
|
-
provider.getReviews(identity
|
|
934
|
+
provider.getReviews(identity, today),
|
|
408
935
|
provider.getBlockers(myMrs)
|
|
409
936
|
]);
|
|
410
937
|
return {
|
|
938
|
+
provider: provider.kind,
|
|
411
939
|
user: identity.username,
|
|
412
940
|
today: { date: isoDay(today), label: label(today, lang) },
|
|
413
|
-
|
|
414
|
-
date: previousDate,
|
|
415
|
-
label: previousDate ? label(/* @__PURE__ */ new Date(`${previousDate}T00:00:00`), lang) : null,
|
|
416
|
-
gapDays,
|
|
417
|
-
eventCount: previousEvents.length
|
|
418
|
-
},
|
|
419
|
-
previousEvents,
|
|
941
|
+
previousDays,
|
|
420
942
|
todayEvents,
|
|
421
943
|
myMrs,
|
|
422
944
|
reviews,
|
|
@@ -426,22 +948,17 @@ async function buildReport(provider, today, lang = "en", lookbackDays = LOOKBACK
|
|
|
426
948
|
}
|
|
427
949
|
|
|
428
950
|
// mcp/server.ts
|
|
429
|
-
function resolveProvider(options) {
|
|
430
|
-
if (options.provider) return options.provider;
|
|
431
|
-
const host = resolveHost(options.host, process.env.GITLAB_HOST, glabHosts());
|
|
432
|
-
const token = resolveToken(host, options.token, process.env.GITLAB_TOKEN, glabToken);
|
|
433
|
-
return new GitLabProvider(host, token);
|
|
434
|
-
}
|
|
435
951
|
async function collect(options = {}) {
|
|
436
|
-
|
|
952
|
+
const provider = options.providerImpl ?? connect({ provider: options.provider, host: options.host, token: options.token });
|
|
953
|
+
return buildReport(provider, /* @__PURE__ */ new Date(), options.lang ?? "en");
|
|
437
954
|
}
|
|
438
955
|
async function main() {
|
|
439
956
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
440
957
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
441
|
-
const server = new McpServer({ name: "standup-mr", version: "0.
|
|
958
|
+
const server = new McpServer({ name: "standup-mr", version: "0.2.0" });
|
|
442
959
|
server.tool(
|
|
443
960
|
"get_standup_data",
|
|
444
|
-
"Collect merge-request-based standup data from GitLab. Returns the previous working day activity, open merge requests bucketed by state (ready / blocked / draft / stale), pending reviews, and the error lines from any failed
|
|
961
|
+
"Collect merge-request-based standup data from GitLab or GitHub. Returns the previous working day activity, open merge requests or pull requests bucketed by state (ready / blocked / draft / stale), pending reviews, and the error lines from any failed pipeline or check.",
|
|
445
962
|
{},
|
|
446
963
|
async () => {
|
|
447
964
|
const report = await collect();
|