standup-mr 0.6.0 → 0.7.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 +27 -0
- package/README.md +32 -27
- package/dist/{chunk-MB7KWLUR.js → chunk-J5LAGA3U.js} +158 -40
- package/dist/cli.js +19 -5
- package/dist/index.d.ts +63 -61
- package/dist/index.js +3 -1
- package/dist/mcp/server.js +209 -65
- package/llms-install.md +12 -12
- package/mcp/README.md +27 -27
- package/package.json +15 -3
package/dist/mcp/server.js
CHANGED
|
@@ -24,17 +24,39 @@ function findPackageRoot(startDir) {
|
|
|
24
24
|
}
|
|
25
25
|
function packageVersion(moduleUrl) {
|
|
26
26
|
const root = findPackageRoot(dirname(fileURLToPath(moduleUrl)));
|
|
27
|
-
const manifest = JSON.parse(
|
|
27
|
+
const manifest = JSON.parse(
|
|
28
|
+
readFileSync(join(root, "package.json"), "utf8")
|
|
29
|
+
);
|
|
28
30
|
if (!manifest.version) {
|
|
29
31
|
throw new Error(`package.json at ${root} has no version`);
|
|
30
32
|
}
|
|
31
33
|
return manifest.version;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
// src/render/chat.ts
|
|
37
|
+
var MARKDOWN_NATIVE = ["discord"];
|
|
38
|
+
var FENCE = /^```/;
|
|
39
|
+
function convertLine(line) {
|
|
40
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
41
|
+
if (heading) return `*${heading[1]}*`;
|
|
42
|
+
return line.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "*$1*");
|
|
43
|
+
}
|
|
44
|
+
function toChatText(markdown, kind) {
|
|
45
|
+
if (MARKDOWN_NATIVE.includes(kind)) return markdown;
|
|
46
|
+
let fenced = false;
|
|
47
|
+
return markdown.split("\n").map((line) => {
|
|
48
|
+
if (FENCE.test(line)) {
|
|
49
|
+
fenced = !fenced;
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
return fenced ? line : convertLine(line);
|
|
53
|
+
}).join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
// src/notify/notify.constants.ts
|
|
35
57
|
var PAYLOAD_FIELD = {
|
|
36
|
-
slack: "text",
|
|
37
|
-
discord: "content",
|
|
58
|
+
"slack": "text",
|
|
59
|
+
"discord": "content",
|
|
38
60
|
"google-chat": "text"
|
|
39
61
|
};
|
|
40
62
|
var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
|
|
@@ -50,10 +72,12 @@ async function postWebhook(url, text2, kind = "slack", fetchImpl = fetch) {
|
|
|
50
72
|
const response = await fetchImpl(url, {
|
|
51
73
|
method: "POST",
|
|
52
74
|
headers: { "Content-Type": "application/json" },
|
|
53
|
-
body: JSON.stringify({ [field]: text2 })
|
|
75
|
+
body: JSON.stringify({ [field]: toChatText(text2, kind) })
|
|
54
76
|
});
|
|
55
77
|
if (!response.ok) {
|
|
56
|
-
throw new Error(
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Webhook rejected the message: HTTP ${response.status}.`
|
|
80
|
+
);
|
|
57
81
|
}
|
|
58
82
|
}
|
|
59
83
|
var WEBHOOK_HOSTS = {
|
|
@@ -148,7 +172,9 @@ function cliStatus(cli) {
|
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
function parseLoggedInHosts(statusOutput) {
|
|
151
|
-
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
175
|
+
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
176
|
+
(match) => match[1]
|
|
177
|
+
);
|
|
152
178
|
return [...new Set(hosts)].sort();
|
|
153
179
|
}
|
|
154
180
|
function glabHosts() {
|
|
@@ -164,13 +190,26 @@ function ghToken(host) {
|
|
|
164
190
|
return cliCapture("gh", ["auth", "token", "--hostname", host]);
|
|
165
191
|
}
|
|
166
192
|
|
|
167
|
-
// src/buckets/buckets.constants.ts
|
|
168
|
-
var STALE_DAYS = 14;
|
|
169
|
-
|
|
170
193
|
// src/dates/dates.constants.ts
|
|
171
194
|
var DAYS = {
|
|
172
|
-
en: [
|
|
173
|
-
|
|
195
|
+
en: [
|
|
196
|
+
"Sunday",
|
|
197
|
+
"Monday",
|
|
198
|
+
"Tuesday",
|
|
199
|
+
"Wednesday",
|
|
200
|
+
"Thursday",
|
|
201
|
+
"Friday",
|
|
202
|
+
"Saturday"
|
|
203
|
+
],
|
|
204
|
+
tr: [
|
|
205
|
+
"Pazar",
|
|
206
|
+
"Pazartesi",
|
|
207
|
+
"Sal\u0131",
|
|
208
|
+
"\xC7ar\u015Famba",
|
|
209
|
+
"Per\u015Fembe",
|
|
210
|
+
"Cuma",
|
|
211
|
+
"Cumartesi"
|
|
212
|
+
]
|
|
174
213
|
};
|
|
175
214
|
var MONTHS = {
|
|
176
215
|
en: [
|
|
@@ -241,10 +280,14 @@ function previousActiveDays(eventDates, today) {
|
|
|
241
280
|
}));
|
|
242
281
|
}
|
|
243
282
|
|
|
283
|
+
// src/buckets/buckets.constants.ts
|
|
284
|
+
var STALE_DAYS = 14;
|
|
285
|
+
|
|
244
286
|
// src/buckets/buckets.ts
|
|
245
287
|
function classify(mr, today, staleDays = STALE_DAYS) {
|
|
246
288
|
if (mr.draft) return "draft";
|
|
247
|
-
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
289
|
+
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
290
|
+
return "blocked";
|
|
248
291
|
const age = Math.round(
|
|
249
292
|
(Date.parse(`${isoDay(today)}T00:00:00Z`) - Date.parse(`${mr.updated}T00:00:00Z`)) / MS_PER_DAY
|
|
250
293
|
);
|
|
@@ -330,17 +373,25 @@ function retryAfter(response) {
|
|
|
330
373
|
const value = response.headers.get("retry-after");
|
|
331
374
|
if (!value) return "";
|
|
332
375
|
const seconds = Number(value);
|
|
333
|
-
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
376
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
377
|
+
return `; retry after ${value}`;
|
|
334
378
|
return `; retry after ${seconds}s`;
|
|
335
379
|
}
|
|
336
380
|
function assertUsable(response, host) {
|
|
337
381
|
if (response.ok || response.status === 404) return;
|
|
338
382
|
if (response.status === 403 || response.status === 429) {
|
|
339
383
|
if (remaining(response) === "0") {
|
|
340
|
-
throw new ApiError(
|
|
384
|
+
throw new ApiError(
|
|
385
|
+
`${host} rate limit reached${resetAt(response)}.`,
|
|
386
|
+
response.status
|
|
387
|
+
);
|
|
341
388
|
}
|
|
342
389
|
const retry = retryAfter(response);
|
|
343
|
-
if (retry)
|
|
390
|
+
if (retry)
|
|
391
|
+
throw new ApiError(
|
|
392
|
+
`${host} rate limit reached${retry}.`,
|
|
393
|
+
response.status
|
|
394
|
+
);
|
|
344
395
|
}
|
|
345
396
|
if (response.status === 401) {
|
|
346
397
|
throw new ApiError(
|
|
@@ -500,8 +551,8 @@ var GitHubProvider = class {
|
|
|
500
551
|
}
|
|
501
552
|
headers() {
|
|
502
553
|
return {
|
|
503
|
-
authorization: `Bearer ${this.token}`,
|
|
504
|
-
accept: "application/vnd.github+json",
|
|
554
|
+
"authorization": `Bearer ${this.token}`,
|
|
555
|
+
"accept": "application/vnd.github+json",
|
|
505
556
|
"x-github-api-version": API_VERSION
|
|
506
557
|
};
|
|
507
558
|
}
|
|
@@ -553,11 +604,14 @@ var GitHubProvider = class {
|
|
|
553
604
|
return rows;
|
|
554
605
|
}
|
|
555
606
|
async getLogText(path) {
|
|
556
|
-
const first = await this.send(buildUrl(this.api, path), {
|
|
607
|
+
const first = await this.send(buildUrl(this.api, path), {
|
|
608
|
+
redirect: "manual"
|
|
609
|
+
});
|
|
557
610
|
if (first.status === 404) return "";
|
|
558
611
|
const location = first.headers.get("location");
|
|
559
612
|
if (!location) {
|
|
560
|
-
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
613
|
+
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
614
|
+
return "";
|
|
561
615
|
assertUsable(first, this.host);
|
|
562
616
|
return first.ok ? await first.text() : "";
|
|
563
617
|
}
|
|
@@ -600,7 +654,9 @@ var GitHubProvider = class {
|
|
|
600
654
|
async shapePr(item) {
|
|
601
655
|
const project = repoFromUrl(item.repository_url);
|
|
602
656
|
const iid = item.number;
|
|
603
|
-
const pull = await this.getJson(
|
|
657
|
+
const pull = await this.getJson(
|
|
658
|
+
`repos/${project}/pulls/${iid}`
|
|
659
|
+
);
|
|
604
660
|
if (!pull) return null;
|
|
605
661
|
const sha = pull.head?.sha ?? "";
|
|
606
662
|
const [checks, reviews] = await Promise.all([
|
|
@@ -608,9 +664,15 @@ var GitHubProvider = class {
|
|
|
608
664
|
`repos/${project}/commits/${sha}/check-runs`,
|
|
609
665
|
{ per_page: PAGE_SIZE }
|
|
610
666
|
) : Promise.resolve(null),
|
|
611
|
-
this.getPaged(
|
|
667
|
+
this.getPaged(
|
|
668
|
+
`repos/${project}/pulls/${iid}/reviews`,
|
|
669
|
+
{},
|
|
670
|
+
2
|
|
671
|
+
)
|
|
612
672
|
]);
|
|
613
|
-
const { pipeline, pipelineId } = normalizeChecks(
|
|
673
|
+
const { pipeline, pipelineId } = normalizeChecks(
|
|
674
|
+
checks?.check_runs ?? []
|
|
675
|
+
);
|
|
614
676
|
return {
|
|
615
677
|
provider: "github",
|
|
616
678
|
project,
|
|
@@ -649,7 +711,9 @@ var GitHubProvider = class {
|
|
|
649
711
|
`is:pr is:open review-requested:${identity.username}`,
|
|
650
712
|
SEARCH_CAP
|
|
651
713
|
);
|
|
652
|
-
const cutoff = isoDay(
|
|
714
|
+
const cutoff = isoDay(
|
|
715
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY)
|
|
716
|
+
);
|
|
653
717
|
const rows = await Promise.all(
|
|
654
718
|
items.map(async (item) => {
|
|
655
719
|
const project = repoFromUrl(item.repository_url);
|
|
@@ -715,7 +779,10 @@ var GitHubProvider = class {
|
|
|
715
779
|
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
716
780
|
);
|
|
717
781
|
if (!failed) return null;
|
|
718
|
-
const summary = [
|
|
782
|
+
const summary = [
|
|
783
|
+
failed.output?.summary ?? "",
|
|
784
|
+
failed.output?.text ?? ""
|
|
785
|
+
].filter(Boolean).join("\n");
|
|
719
786
|
const errors = extractErrors(summary);
|
|
720
787
|
if (errors.length === 0) return null;
|
|
721
788
|
return {
|
|
@@ -828,7 +895,9 @@ var GitLabProvider = class {
|
|
|
828
895
|
const raw = await this.getPaged("events", {
|
|
829
896
|
after: isoDay(since)
|
|
830
897
|
});
|
|
831
|
-
const ids = [
|
|
898
|
+
const ids = [
|
|
899
|
+
...new Set(raw.map((e) => e.project_id).filter(Boolean))
|
|
900
|
+
];
|
|
832
901
|
const paths = new Map(
|
|
833
902
|
await Promise.all(
|
|
834
903
|
ids.map(async (id) => [id, await this.projectPath(id)])
|
|
@@ -839,7 +908,7 @@ var GitLabProvider = class {
|
|
|
839
908
|
return {
|
|
840
909
|
at: String(event.created_at).slice(0, 16),
|
|
841
910
|
action: event.action_name,
|
|
842
|
-
project: paths.get(event.project_id) ?? "",
|
|
911
|
+
project: paths.get(event.project_id ?? -1) ?? "",
|
|
843
912
|
targetType: event.target_type ?? "",
|
|
844
913
|
title: event.target_title ?? "",
|
|
845
914
|
branch: push.ref ?? "",
|
|
@@ -851,7 +920,9 @@ var GitLabProvider = class {
|
|
|
851
920
|
async countUnresolved(projectId, iid) {
|
|
852
921
|
const discussions = await this.getJson(
|
|
853
922
|
`projects/${projectId}/merge_requests/${iid}/discussions`,
|
|
854
|
-
{
|
|
923
|
+
{
|
|
924
|
+
per_page: PAGE_SIZE2
|
|
925
|
+
}
|
|
855
926
|
);
|
|
856
927
|
if (!discussions) return 0;
|
|
857
928
|
return discussions.filter((discussion) => {
|
|
@@ -904,7 +975,9 @@ var GitLabProvider = class {
|
|
|
904
975
|
async approvedByMe(projectId, iid, uid) {
|
|
905
976
|
const approvals = await this.getJson(`projects/${projectId}/merge_requests/${iid}/approvals`);
|
|
906
977
|
if (!approvals) return false;
|
|
907
|
-
return (approvals.approved_by ?? []).some(
|
|
978
|
+
return (approvals.approved_by ?? []).some(
|
|
979
|
+
(entry2) => entry2.user?.id === uid
|
|
980
|
+
);
|
|
908
981
|
}
|
|
909
982
|
async getReviews(identity, today) {
|
|
910
983
|
const raw = await this.getPaged("merge_requests", {
|
|
@@ -912,7 +985,9 @@ var GitLabProvider = class {
|
|
|
912
985
|
state: "opened",
|
|
913
986
|
reviewer_id: identity.id
|
|
914
987
|
});
|
|
915
|
-
const cutoff = isoDay(
|
|
988
|
+
const cutoff = isoDay(
|
|
989
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY)
|
|
990
|
+
);
|
|
916
991
|
const rows = await Promise.all(
|
|
917
992
|
raw.map(async (mr) => ({
|
|
918
993
|
provider: "gitlab",
|
|
@@ -924,7 +999,11 @@ var GitLabProvider = class {
|
|
|
924
999
|
draft: mr.draft,
|
|
925
1000
|
url: mr.web_url,
|
|
926
1001
|
fresh: String(mr.updated_at).slice(0, 10) >= cutoff,
|
|
927
|
-
approvedByMe: await this.approvedByMe(
|
|
1002
|
+
approvedByMe: await this.approvedByMe(
|
|
1003
|
+
mr.project_id,
|
|
1004
|
+
mr.iid,
|
|
1005
|
+
identity.id
|
|
1006
|
+
)
|
|
928
1007
|
}))
|
|
929
1008
|
);
|
|
930
1009
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
@@ -936,7 +1015,9 @@ var GitLabProvider = class {
|
|
|
936
1015
|
) ?? [];
|
|
937
1016
|
const job = jobs.find((j) => j.status === "failed");
|
|
938
1017
|
if (!job) return null;
|
|
939
|
-
const trace = await this.getText(
|
|
1018
|
+
const trace = await this.getText(
|
|
1019
|
+
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
1020
|
+
);
|
|
940
1021
|
return {
|
|
941
1022
|
provider: "gitlab",
|
|
942
1023
|
project: mr.project,
|
|
@@ -969,7 +1050,12 @@ function hostFrom(cliHost, envHost, probe, fallback, labels) {
|
|
|
969
1050
|
if (cliHost) return cliHost;
|
|
970
1051
|
if (envHost) return envHost;
|
|
971
1052
|
const found = probe();
|
|
972
|
-
return resolveHost(
|
|
1053
|
+
return resolveHost(
|
|
1054
|
+
void 0,
|
|
1055
|
+
void 0,
|
|
1056
|
+
found.length > 0 ? found : fallback,
|
|
1057
|
+
labels
|
|
1058
|
+
);
|
|
973
1059
|
}
|
|
974
1060
|
function chooseKind(options = {}) {
|
|
975
1061
|
const env = options.env ?? process.env;
|
|
@@ -1022,7 +1108,13 @@ function connect(options = {}) {
|
|
|
1022
1108
|
const env = options.env ?? process.env;
|
|
1023
1109
|
const probe = options.probe ?? DEFAULT_PROBE;
|
|
1024
1110
|
if (chooseKind(options) === "github") {
|
|
1025
|
-
const host2 = hostFrom(
|
|
1111
|
+
const host2 = hostFrom(
|
|
1112
|
+
options.host,
|
|
1113
|
+
env.GITHUB_HOST,
|
|
1114
|
+
probe.github,
|
|
1115
|
+
[DOT_COM],
|
|
1116
|
+
GITHUB_LABELS
|
|
1117
|
+
);
|
|
1026
1118
|
const token2 = resolveToken(
|
|
1027
1119
|
host2,
|
|
1028
1120
|
options.token,
|
|
@@ -1032,7 +1124,13 @@ function connect(options = {}) {
|
|
|
1032
1124
|
);
|
|
1033
1125
|
return new GitHubProvider(host2, token2);
|
|
1034
1126
|
}
|
|
1035
|
-
const host = hostFrom(
|
|
1127
|
+
const host = hostFrom(
|
|
1128
|
+
options.host,
|
|
1129
|
+
env.GITLAB_HOST,
|
|
1130
|
+
probe.gitlab,
|
|
1131
|
+
[],
|
|
1132
|
+
GITLAB_LABELS
|
|
1133
|
+
);
|
|
1036
1134
|
const token = resolveToken(
|
|
1037
1135
|
host,
|
|
1038
1136
|
options.token,
|
|
@@ -1043,21 +1141,6 @@ function connect(options = {}) {
|
|
|
1043
1141
|
return new GitLabProvider(host, token);
|
|
1044
1142
|
}
|
|
1045
1143
|
|
|
1046
|
-
// src/skill/skill.ts
|
|
1047
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1048
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
1049
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1050
|
-
function readStandupSkillBody() {
|
|
1051
|
-
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1052
|
-
const skillPath = join2(findPackageRoot(moduleDir), "skills", "standup", "SKILL.md");
|
|
1053
|
-
if (!existsSync2(skillPath)) {
|
|
1054
|
-
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1055
|
-
}
|
|
1056
|
-
const content = readFileSync2(skillPath, "utf8");
|
|
1057
|
-
const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
1058
|
-
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
1144
|
// src/report/report.constants.ts
|
|
1062
1145
|
var LOOKBACK_DAYS = 21;
|
|
1063
1146
|
|
|
@@ -1095,9 +1178,36 @@ async function buildReport(provider, today, lang = "en", lookbackDays = LOOKBACK
|
|
|
1095
1178
|
};
|
|
1096
1179
|
}
|
|
1097
1180
|
|
|
1181
|
+
// src/skill/skill.ts
|
|
1182
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1183
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1184
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1185
|
+
function readStandupSkillBody() {
|
|
1186
|
+
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1187
|
+
const skillPath = join2(
|
|
1188
|
+
findPackageRoot(moduleDir),
|
|
1189
|
+
"skills",
|
|
1190
|
+
"standup",
|
|
1191
|
+
"SKILL.md"
|
|
1192
|
+
);
|
|
1193
|
+
if (!existsSync2(skillPath)) {
|
|
1194
|
+
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1195
|
+
}
|
|
1196
|
+
const content = readFileSync2(skillPath, "utf8");
|
|
1197
|
+
const withoutFrontmatter = content.replace(
|
|
1198
|
+
/^---\r?\n[\s\S]*?\r?\n---\r?\n/,
|
|
1199
|
+
""
|
|
1200
|
+
);
|
|
1201
|
+
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1098
1204
|
// mcp/server.ts
|
|
1099
1205
|
async function collect(options = {}) {
|
|
1100
|
-
const provider = options.providerImpl ?? connect({
|
|
1206
|
+
const provider = options.providerImpl ?? connect({
|
|
1207
|
+
provider: options.provider,
|
|
1208
|
+
host: options.host,
|
|
1209
|
+
token: options.token
|
|
1210
|
+
});
|
|
1101
1211
|
return buildReport(provider, /* @__PURE__ */ new Date(), options.lang ?? "en");
|
|
1102
1212
|
}
|
|
1103
1213
|
var STANDUP_TOOL_DESCRIPTION = 'Collect merge-request-based standup data from GitLab or GitHub. Returns one JSON object: `today`, `previousDays[]` with the events of each, `todayEvents[]`, `myMrs[]` bucketed ready / blocked / draft / stale, `reviews[]` waiting on you, and `blockers[]` carrying the error lines read out of each failed pipeline job log.\n\nCall it once at the start of a working day, to write a standup note. It is a snapshot, not a search API: it cannot fetch one named merge request, reach further back than the previous working day, or filter by project.\n\nRead-only, and credentials never come from an argument \u2014 they come from the environment or a logged-in gh / glab session. A rejected token, a refused resource or a rate limit fails the call with the host\'s own message, after two retries on transient server errors. A blocker whose diagnosis could not be fetched is still returned, with `job: "unknown"`, so a red pipeline is never silently dropped.';
|
|
@@ -1118,7 +1228,9 @@ async function runStandupTool(args, collector = collect) {
|
|
|
1118
1228
|
host: args.host,
|
|
1119
1229
|
lang: args.lang
|
|
1120
1230
|
});
|
|
1121
|
-
return {
|
|
1231
|
+
return {
|
|
1232
|
+
content: [{ type: "text", text: JSON.stringify(report) }]
|
|
1233
|
+
};
|
|
1122
1234
|
}
|
|
1123
1235
|
var WEBHOOK_URL_ENV = "STANDUP_WEBHOOK_URL";
|
|
1124
1236
|
var POST_TOOL_DESCRIPTION = `Posts a finished standup note to a chat webhook. This one has a side effect: it sends a message other people will see, so only call it on a note the user has agreed to send.
|
|
@@ -1165,27 +1277,56 @@ async function runPostTool(args, deps = {}) {
|
|
|
1165
1277
|
async function runInstructionsTool() {
|
|
1166
1278
|
return text(readStandupSkillBody());
|
|
1167
1279
|
}
|
|
1168
|
-
|
|
1169
|
-
const server = new McpServer({
|
|
1170
|
-
|
|
1280
|
+
function createServer() {
|
|
1281
|
+
const server = new McpServer({
|
|
1282
|
+
name: "standup-mr",
|
|
1283
|
+
version: packageVersion(import.meta.url)
|
|
1284
|
+
});
|
|
1285
|
+
server.registerTool(
|
|
1171
1286
|
"get_standup_data",
|
|
1172
|
-
|
|
1173
|
-
|
|
1287
|
+
{
|
|
1288
|
+
description: STANDUP_TOOL_DESCRIPTION,
|
|
1289
|
+
inputSchema: STANDUP_TOOL_SCHEMA,
|
|
1290
|
+
annotations: {
|
|
1291
|
+
readOnlyHint: true,
|
|
1292
|
+
destructiveHint: false,
|
|
1293
|
+
idempotentHint: true,
|
|
1294
|
+
openWorldHint: true
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1174
1297
|
async (args) => await runStandupTool(args)
|
|
1175
1298
|
);
|
|
1176
|
-
server.
|
|
1299
|
+
server.registerTool(
|
|
1177
1300
|
"post_standup_note",
|
|
1178
|
-
|
|
1179
|
-
|
|
1301
|
+
{
|
|
1302
|
+
description: POST_TOOL_DESCRIPTION,
|
|
1303
|
+
inputSchema: POST_TOOL_SCHEMA,
|
|
1304
|
+
annotations: {
|
|
1305
|
+
readOnlyHint: false,
|
|
1306
|
+
destructiveHint: false,
|
|
1307
|
+
idempotentHint: false,
|
|
1308
|
+
openWorldHint: true
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1180
1311
|
async (args) => await runPostTool(args)
|
|
1181
1312
|
);
|
|
1182
|
-
server.
|
|
1313
|
+
server.registerTool(
|
|
1183
1314
|
"get_note_instructions",
|
|
1184
|
-
|
|
1185
|
-
|
|
1315
|
+
{
|
|
1316
|
+
description: INSTRUCTIONS_TOOL_DESCRIPTION,
|
|
1317
|
+
annotations: {
|
|
1318
|
+
readOnlyHint: true,
|
|
1319
|
+
destructiveHint: false,
|
|
1320
|
+
idempotentHint: true,
|
|
1321
|
+
openWorldHint: false
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1186
1324
|
async () => await runInstructionsTool()
|
|
1187
1325
|
);
|
|
1188
|
-
|
|
1326
|
+
return server;
|
|
1327
|
+
}
|
|
1328
|
+
async function main() {
|
|
1329
|
+
await createServer().connect(new StdioServerTransport());
|
|
1189
1330
|
}
|
|
1190
1331
|
var entry = process.argv[1];
|
|
1191
1332
|
var entryUrl;
|
|
@@ -1196,8 +1337,10 @@ try {
|
|
|
1196
1337
|
}
|
|
1197
1338
|
if (entryUrl && import.meta.url === entryUrl) {
|
|
1198
1339
|
main().catch((error) => {
|
|
1199
|
-
process.stderr.write(
|
|
1200
|
-
|
|
1340
|
+
process.stderr.write(
|
|
1341
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
1342
|
+
`
|
|
1343
|
+
);
|
|
1201
1344
|
process.exitCode = 1;
|
|
1202
1345
|
});
|
|
1203
1346
|
}
|
|
@@ -1209,6 +1352,7 @@ export {
|
|
|
1209
1352
|
STANDUP_TOOL_SCHEMA,
|
|
1210
1353
|
WEBHOOK_URL_ENV,
|
|
1211
1354
|
collect,
|
|
1355
|
+
createServer,
|
|
1212
1356
|
main,
|
|
1213
1357
|
runInstructionsTool,
|
|
1214
1358
|
runPostTool,
|
package/llms-install.md
CHANGED
|
@@ -18,13 +18,13 @@ Add this to the MCP settings file (for Cline,
|
|
|
18
18
|
|
|
19
19
|
```json
|
|
20
20
|
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"standup": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
25
|
+
"env": {}
|
|
26
|
+
}
|
|
26
27
|
}
|
|
27
|
-
}
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -35,12 +35,12 @@ That is the whole install when the user already has `gh` or `glab` logged in
|
|
|
35
35
|
|
|
36
36
|
Add only the pair the user actually needs.
|
|
37
37
|
|
|
38
|
-
| Variable
|
|
39
|
-
|
|
40
|
-
| `GITHUB_TOKEN`
|
|
41
|
-
| `GITHUB_HOST`
|
|
42
|
-
| `GITLAB_TOKEN`
|
|
43
|
-
| `GITLAB_HOST`
|
|
38
|
+
| Variable | When |
|
|
39
|
+
| ------------------ | -------------------------------------------------------------------------- |
|
|
40
|
+
| `GITHUB_TOKEN` | GitHub, no `gh` session |
|
|
41
|
+
| `GITHUB_HOST` | GitHub Enterprise only; defaults to `github.com` |
|
|
42
|
+
| `GITLAB_TOKEN` | GitLab, no `glab` session |
|
|
43
|
+
| `GITLAB_HOST` | **Required for GitLab** — there is no default host |
|
|
44
44
|
| `STANDUP_PROVIDER` | `github` or `gitlab`, when both are configured and the choice is ambiguous |
|
|
45
45
|
|
|
46
46
|
Ask the user for a token; do not invent one, and do not put a token anywhere
|
package/mcp/README.md
CHANGED
|
@@ -9,16 +9,16 @@ GitLab:
|
|
|
9
9
|
|
|
10
10
|
```json
|
|
11
11
|
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"standup": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
16
|
+
"env": {
|
|
17
|
+
"GITLAB_HOST": "gitlab.example.com",
|
|
18
|
+
"GITLAB_TOKEN": "glpat-..."
|
|
19
|
+
}
|
|
20
|
+
}
|
|
20
21
|
}
|
|
21
|
-
}
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -26,16 +26,16 @@ GitHub:
|
|
|
26
26
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"standup": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
33
|
+
"env": {
|
|
34
|
+
"GITHUB_HOST": "github.com",
|
|
35
|
+
"GITHUB_TOKEN": "ghp_..."
|
|
36
|
+
}
|
|
37
|
+
}
|
|
37
38
|
}
|
|
38
|
-
}
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
@@ -46,16 +46,16 @@ If you're working from a local clone instead of the published package, point
|
|
|
46
46
|
|
|
47
47
|
```json
|
|
48
48
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"standup": {
|
|
51
|
+
"command": "node",
|
|
52
|
+
"args": ["/absolute/path/to/standup-mr/dist/mcp/server.js"],
|
|
53
|
+
"env": {
|
|
54
|
+
"GITHUB_HOST": "github.com",
|
|
55
|
+
"GITHUB_TOKEN": "ghp_..."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
57
58
|
}
|
|
58
|
-
}
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "standup-mr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"mcpName": "io.github.Jubstaaa/standup-mr",
|
|
5
5
|
"description": "Standup notes from merge request state, not commit logs.",
|
|
6
6
|
"keywords": [
|
|
@@ -55,19 +55,31 @@
|
|
|
55
55
|
],
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup",
|
|
58
|
+
"lint": "eslint .",
|
|
59
|
+
"format": "prettier . --write",
|
|
60
|
+
"format:check": "prettier . --check",
|
|
58
61
|
"test": "bun test",
|
|
59
62
|
"test:tz": "TZ=Europe/Istanbul bun test ./src/dates/dates.tz.istanbul.cases.ts && TZ=America/Los_Angeles bun test ./src/dates/dates.tz.los-angeles.cases.ts",
|
|
60
63
|
"typecheck": "tsc --noEmit",
|
|
61
64
|
"prepublishOnly": "tsup"
|
|
62
65
|
},
|
|
63
66
|
"dependencies": {
|
|
64
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
65
68
|
"zod": "^4.4.3"
|
|
66
69
|
},
|
|
67
70
|
"devDependencies": {
|
|
71
|
+
"@eslint/js": "^9.39.1",
|
|
68
72
|
"@types/bun": "^1.0.0",
|
|
69
73
|
"@types/node": "^22.0.0",
|
|
74
|
+
"eslint": "^9.39.1",
|
|
75
|
+
"eslint-config-prettier": "^10.1.8",
|
|
76
|
+
"eslint-plugin-import": "^2.32.0",
|
|
77
|
+
"eslint-plugin-perfectionist": "^5.5.0",
|
|
78
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
79
|
+
"globals": "^17.12.0",
|
|
80
|
+
"prettier": "^3.9.6",
|
|
70
81
|
"tsup": "^8.0.0",
|
|
71
|
-
"typescript": "^5.5.0"
|
|
82
|
+
"typescript": "^5.5.0",
|
|
83
|
+
"typescript-eslint": "^8.44.0"
|
|
72
84
|
}
|
|
73
85
|
}
|