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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to standup-mr are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.7.0] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Tool annotations.** Every MCP tool now declares `readOnlyHint`,
|
|
12
|
+
`destructiveHint`, `idempotentHint` and `openWorldHint`, so a host can tell
|
|
13
|
+
the read-only `get_standup_data` and `get_note_instructions` apart from
|
|
14
|
+
`post_standup_note`, which sends a message other people will see, and warn
|
|
15
|
+
before invoking it.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The MCP server registers its tools with `registerTool`, which needs
|
|
20
|
+
`@modelcontextprotocol/sdk` 1.30.0 or later.
|
|
21
|
+
|
|
22
|
+
## [0.6.1] - 2026-09-02
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- A note posted to Google Chat or Slack arrived with its formatting showing:
|
|
27
|
+
`**bold**` as literal asterisks, `## Dün` as a literal heading marker.
|
|
28
|
+
Neither renders standard Markdown — both use a single asterisk for bold and
|
|
29
|
+
have no headings — while the note is Markdown throughout. Bold is now
|
|
30
|
+
converted and headings become bold lines for those two. Discord speaks
|
|
31
|
+
Markdown natively and is sent untouched, and fenced blocks pass through
|
|
32
|
+
verbatim so a code sample keeps its asterisks.
|
|
33
|
+
|
|
7
34
|
## [0.6.0] - 2026-09-02
|
|
8
35
|
|
|
9
36
|
### Added
|
package/README.md
CHANGED
|
@@ -12,13 +12,13 @@ pipeline is red, it opens the job log and tells you **why**.
|
|
|
12
12
|
|
|
13
13
|
## What makes it different
|
|
14
14
|
|
|
15
|
-
|
|
|
16
|
-
|
|
17
|
-
| Source
|
|
18
|
-
| Merge request state
|
|
19
|
-
| Review queue
|
|
20
|
-
| Failed pipeline
|
|
21
|
-
| Self-hosted (GitLab CE/EE, GitHub Enterprise) | varies
|
|
15
|
+
| | commit-log tools | standup-mr |
|
|
16
|
+
| --------------------------------------------- | ---------------- | ----------------------------------------------------- |
|
|
17
|
+
| Source | local `git log` | GitLab or GitHub API |
|
|
18
|
+
| Merge request state | ✗ | ready / blocked / draft / stale |
|
|
19
|
+
| Review queue | ✗ | pending only, approvals filtered out |
|
|
20
|
+
| Failed pipeline | ✗ | error lines from the job trace or the Actions job log |
|
|
21
|
+
| Self-hosted (GitLab CE/EE, GitHub Enterprise) | varies | first class |
|
|
22
22
|
|
|
23
23
|
## Use
|
|
24
24
|
|
|
@@ -36,10 +36,10 @@ npx standup-mr fetch --markdown | npx standup-mr post --google-chat "$URL"
|
|
|
36
36
|
|
|
37
37
|
### Identity
|
|
38
38
|
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
| Flags
|
|
42
|
-
| Env
|
|
39
|
+
| | GitHub | GitLab |
|
|
40
|
+
| ----------- | ------------------------------------------- | ------------------------------------------------------- |
|
|
41
|
+
| Flags | `--host` / `--token` | `--host` / `--token` |
|
|
42
|
+
| Env | `GITHUB_HOST` / `GITHUB_TOKEN` | `GITLAB_HOST` / `GITLAB_TOKEN` |
|
|
43
43
|
| CLI session | [`gh`](https://cli.github.com/) auth config | [`glab`](https://gitlab.com/gitlab-org/cli) auth config |
|
|
44
44
|
|
|
45
45
|
GitHub defaults to `github.com` when no host is given. GitLab has no default —
|
|
@@ -93,15 +93,15 @@ paste in the note-writing rules separately.
|
|
|
93
93
|
|
|
94
94
|
```json
|
|
95
95
|
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"standup": {
|
|
98
|
+
"command": "npx",
|
|
99
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
100
|
+
"env": {
|
|
101
|
+
"GITHUB_TOKEN": "ghp_..."
|
|
102
|
+
}
|
|
103
|
+
}
|
|
103
104
|
}
|
|
104
|
-
}
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
@@ -123,11 +123,11 @@ documentation for the current format rather than trusting this file blindly.
|
|
|
123
123
|
|
|
124
124
|
The server exposes three tools:
|
|
125
125
|
|
|
126
|
-
| Tool
|
|
127
|
-
|
|
128
|
-
| `get_standup_data`
|
|
126
|
+
| Tool | What it does |
|
|
127
|
+
| ----------------------- | -------------------------------------------------------------------------------------------- |
|
|
128
|
+
| `get_standup_data` | Reads the provider and returns the report as JSON. Optional `provider`, `host`, `lang`. |
|
|
129
129
|
| `get_note_instructions` | Returns the note-writing rules, so the assistant can write the note the way the skill would. |
|
|
130
|
-
| `post_standup_note`
|
|
130
|
+
| `post_standup_note` | Posts a finished note to a Slack, Discord or Google Chat webhook. |
|
|
131
131
|
|
|
132
132
|
`post_standup_note` reads the webhook URL from `STANDUP_WEBHOOK_URL` and never
|
|
133
133
|
takes it as an argument — anyone holding that URL can post to the channel, so
|
|
@@ -135,7 +135,12 @@ it belongs with the tokens, not in a transcript. The payload shape is inferred
|
|
|
135
135
|
from the URL host; `kind` is only needed when a proxy hides it.
|
|
136
136
|
|
|
137
137
|
Three shapes are implemented: `slack` and `google-chat` both post `{"text"}`,
|
|
138
|
-
`discord` posts `{"content"}`.
|
|
138
|
+
`discord` posts `{"content"}`.
|
|
139
|
+
|
|
140
|
+
Slack and Google Chat do not render standard Markdown, so the note is rewritten
|
|
141
|
+
on the way out: `**bold**` becomes `*bold*` and a `##` heading becomes a bold
|
|
142
|
+
line. Inline code, fenced blocks and their contents are left alone. Discord
|
|
143
|
+
speaks Markdown natively and is sent untouched. Anything else that accepts a Slack-shaped body —
|
|
139
144
|
Mattermost, Rocket.Chat, an n8n or Zapier endpoint — works today by passing
|
|
140
145
|
`kind: "slack"`, or `--slack URL` on the CLI.
|
|
141
146
|
|
|
@@ -191,8 +196,8 @@ _Structured digest — not a written note._
|
|
|
191
196
|
## Blockers
|
|
192
197
|
|
|
193
198
|
- `acme/mobile` !6 — job `quality`
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
- `npm ERR! code E404`
|
|
200
|
+
- `npm ERR! 404 Not Found - GET https://registry.example.com/@acme%2fui`
|
|
196
201
|
```
|
|
197
202
|
|
|
198
203
|
The last section is the point of the tool. Every other standup tool can tell you
|
|
@@ -227,7 +232,7 @@ pipe `standup fetch` into anything, or import the package, read
|
|
|
227
232
|
returns `null` with no error.
|
|
228
233
|
- `MergeRequest`, `Review` and `Blocker` carry a required `provider` field, and
|
|
229
234
|
`Provider.getReviews` takes an `Identity` rather than a numeric id.
|
|
230
|
-
- The MCP `CollectOptions.provider` is now a provider
|
|
235
|
+
- The MCP `CollectOptions.provider` is now a provider _name_; inject a
|
|
231
236
|
`Provider` instance through `providerImpl`.
|
|
232
237
|
|
|
233
238
|
Claude Code plugin users should run `/plugin marketplace update standup-mr` —
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/buckets/buckets.constants.ts
|
|
4
|
-
var STALE_DAYS = 14;
|
|
5
|
-
|
|
6
3
|
// src/dates/dates.constants.ts
|
|
7
4
|
var DAYS = {
|
|
8
|
-
en: [
|
|
9
|
-
|
|
5
|
+
en: [
|
|
6
|
+
"Sunday",
|
|
7
|
+
"Monday",
|
|
8
|
+
"Tuesday",
|
|
9
|
+
"Wednesday",
|
|
10
|
+
"Thursday",
|
|
11
|
+
"Friday",
|
|
12
|
+
"Saturday"
|
|
13
|
+
],
|
|
14
|
+
tr: [
|
|
15
|
+
"Pazar",
|
|
16
|
+
"Pazartesi",
|
|
17
|
+
"Sal\u0131",
|
|
18
|
+
"\xC7ar\u015Famba",
|
|
19
|
+
"Per\u015Fembe",
|
|
20
|
+
"Cuma",
|
|
21
|
+
"Cumartesi"
|
|
22
|
+
]
|
|
10
23
|
};
|
|
11
24
|
var MONTHS = {
|
|
12
25
|
en: [
|
|
@@ -77,10 +90,14 @@ function previousActiveDays(eventDates, today) {
|
|
|
77
90
|
}));
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
// src/buckets/buckets.constants.ts
|
|
94
|
+
var STALE_DAYS = 14;
|
|
95
|
+
|
|
80
96
|
// src/buckets/buckets.ts
|
|
81
97
|
function classify(mr, today, staleDays = STALE_DAYS) {
|
|
82
98
|
if (mr.draft) return "draft";
|
|
83
|
-
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
99
|
+
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
100
|
+
return "blocked";
|
|
84
101
|
const age = Math.round(
|
|
85
102
|
(Date.parse(`${isoDay(today)}T00:00:00Z`) - Date.parse(`${mr.updated}T00:00:00Z`)) / MS_PER_DAY
|
|
86
103
|
);
|
|
@@ -169,7 +186,9 @@ function cliStatus(cli) {
|
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
function parseLoggedInHosts(statusOutput) {
|
|
172
|
-
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
189
|
+
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
190
|
+
(match) => match[1]
|
|
191
|
+
);
|
|
173
192
|
return [...new Set(hosts)].sort();
|
|
174
193
|
}
|
|
175
194
|
var parseGlabHosts = parseLoggedInHosts;
|
|
@@ -186,10 +205,30 @@ function ghToken(host) {
|
|
|
186
205
|
return cliCapture("gh", ["auth", "token", "--hostname", host]);
|
|
187
206
|
}
|
|
188
207
|
|
|
208
|
+
// src/render/chat.ts
|
|
209
|
+
var MARKDOWN_NATIVE = ["discord"];
|
|
210
|
+
var FENCE = /^```/;
|
|
211
|
+
function convertLine(line) {
|
|
212
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
213
|
+
if (heading) return `*${heading[1]}*`;
|
|
214
|
+
return line.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "*$1*");
|
|
215
|
+
}
|
|
216
|
+
function toChatText(markdown, kind) {
|
|
217
|
+
if (MARKDOWN_NATIVE.includes(kind)) return markdown;
|
|
218
|
+
let fenced = false;
|
|
219
|
+
return markdown.split("\n").map((line) => {
|
|
220
|
+
if (FENCE.test(line)) {
|
|
221
|
+
fenced = !fenced;
|
|
222
|
+
return line;
|
|
223
|
+
}
|
|
224
|
+
return fenced ? line : convertLine(line);
|
|
225
|
+
}).join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
189
228
|
// src/notify/notify.constants.ts
|
|
190
229
|
var PAYLOAD_FIELD = {
|
|
191
|
-
slack: "text",
|
|
192
|
-
discord: "content",
|
|
230
|
+
"slack": "text",
|
|
231
|
+
"discord": "content",
|
|
193
232
|
"google-chat": "text"
|
|
194
233
|
};
|
|
195
234
|
var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
|
|
@@ -205,10 +244,12 @@ async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
|
|
|
205
244
|
const response = await fetchImpl(url, {
|
|
206
245
|
method: "POST",
|
|
207
246
|
headers: { "Content-Type": "application/json" },
|
|
208
|
-
body: JSON.stringify({ [field]: text })
|
|
247
|
+
body: JSON.stringify({ [field]: toChatText(text, kind) })
|
|
209
248
|
});
|
|
210
249
|
if (!response.ok) {
|
|
211
|
-
throw new Error(
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Webhook rejected the message: HTTP ${response.status}.`
|
|
252
|
+
);
|
|
212
253
|
}
|
|
213
254
|
}
|
|
214
255
|
var WEBHOOK_HOSTS = {
|
|
@@ -259,17 +300,25 @@ function retryAfter(response) {
|
|
|
259
300
|
const value = response.headers.get("retry-after");
|
|
260
301
|
if (!value) return "";
|
|
261
302
|
const seconds = Number(value);
|
|
262
|
-
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
303
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
304
|
+
return `; retry after ${value}`;
|
|
263
305
|
return `; retry after ${seconds}s`;
|
|
264
306
|
}
|
|
265
307
|
function assertUsable(response, host) {
|
|
266
308
|
if (response.ok || response.status === 404) return;
|
|
267
309
|
if (response.status === 403 || response.status === 429) {
|
|
268
310
|
if (remaining(response) === "0") {
|
|
269
|
-
throw new ApiError(
|
|
311
|
+
throw new ApiError(
|
|
312
|
+
`${host} rate limit reached${resetAt(response)}.`,
|
|
313
|
+
response.status
|
|
314
|
+
);
|
|
270
315
|
}
|
|
271
316
|
const retry = retryAfter(response);
|
|
272
|
-
if (retry)
|
|
317
|
+
if (retry)
|
|
318
|
+
throw new ApiError(
|
|
319
|
+
`${host} rate limit reached${retry}.`,
|
|
320
|
+
response.status
|
|
321
|
+
);
|
|
273
322
|
}
|
|
274
323
|
if (response.status === 401) {
|
|
275
324
|
throw new ApiError(
|
|
@@ -470,8 +519,8 @@ var GitHubProvider = class {
|
|
|
470
519
|
}
|
|
471
520
|
headers() {
|
|
472
521
|
return {
|
|
473
|
-
authorization: `Bearer ${this.token}`,
|
|
474
|
-
accept: "application/vnd.github+json",
|
|
522
|
+
"authorization": `Bearer ${this.token}`,
|
|
523
|
+
"accept": "application/vnd.github+json",
|
|
475
524
|
"x-github-api-version": API_VERSION
|
|
476
525
|
};
|
|
477
526
|
}
|
|
@@ -523,11 +572,14 @@ var GitHubProvider = class {
|
|
|
523
572
|
return rows;
|
|
524
573
|
}
|
|
525
574
|
async getLogText(path) {
|
|
526
|
-
const first = await this.send(buildUrl(this.api, path), {
|
|
575
|
+
const first = await this.send(buildUrl(this.api, path), {
|
|
576
|
+
redirect: "manual"
|
|
577
|
+
});
|
|
527
578
|
if (first.status === 404) return "";
|
|
528
579
|
const location = first.headers.get("location");
|
|
529
580
|
if (!location) {
|
|
530
|
-
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
581
|
+
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
582
|
+
return "";
|
|
531
583
|
assertUsable(first, this.host);
|
|
532
584
|
return first.ok ? await first.text() : "";
|
|
533
585
|
}
|
|
@@ -570,7 +622,9 @@ var GitHubProvider = class {
|
|
|
570
622
|
async shapePr(item) {
|
|
571
623
|
const project = repoFromUrl(item.repository_url);
|
|
572
624
|
const iid = item.number;
|
|
573
|
-
const pull = await this.getJson(
|
|
625
|
+
const pull = await this.getJson(
|
|
626
|
+
`repos/${project}/pulls/${iid}`
|
|
627
|
+
);
|
|
574
628
|
if (!pull) return null;
|
|
575
629
|
const sha = pull.head?.sha ?? "";
|
|
576
630
|
const [checks, reviews] = await Promise.all([
|
|
@@ -578,9 +632,15 @@ var GitHubProvider = class {
|
|
|
578
632
|
`repos/${project}/commits/${sha}/check-runs`,
|
|
579
633
|
{ per_page: PAGE_SIZE }
|
|
580
634
|
) : Promise.resolve(null),
|
|
581
|
-
this.getPaged(
|
|
635
|
+
this.getPaged(
|
|
636
|
+
`repos/${project}/pulls/${iid}/reviews`,
|
|
637
|
+
{},
|
|
638
|
+
2
|
|
639
|
+
)
|
|
582
640
|
]);
|
|
583
|
-
const { pipeline, pipelineId } = normalizeChecks(
|
|
641
|
+
const { pipeline, pipelineId } = normalizeChecks(
|
|
642
|
+
checks?.check_runs ?? []
|
|
643
|
+
);
|
|
584
644
|
return {
|
|
585
645
|
provider: "github",
|
|
586
646
|
project,
|
|
@@ -619,7 +679,9 @@ var GitHubProvider = class {
|
|
|
619
679
|
`is:pr is:open review-requested:${identity.username}`,
|
|
620
680
|
SEARCH_CAP
|
|
621
681
|
);
|
|
622
|
-
const cutoff = isoDay(
|
|
682
|
+
const cutoff = isoDay(
|
|
683
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY)
|
|
684
|
+
);
|
|
623
685
|
const rows = await Promise.all(
|
|
624
686
|
items.map(async (item) => {
|
|
625
687
|
const project = repoFromUrl(item.repository_url);
|
|
@@ -685,7 +747,10 @@ var GitHubProvider = class {
|
|
|
685
747
|
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
686
748
|
);
|
|
687
749
|
if (!failed) return null;
|
|
688
|
-
const summary = [
|
|
750
|
+
const summary = [
|
|
751
|
+
failed.output?.summary ?? "",
|
|
752
|
+
failed.output?.text ?? ""
|
|
753
|
+
].filter(Boolean).join("\n");
|
|
689
754
|
const errors = extractErrors(summary);
|
|
690
755
|
if (errors.length === 0) return null;
|
|
691
756
|
return {
|
|
@@ -798,7 +863,9 @@ var GitLabProvider = class {
|
|
|
798
863
|
const raw = await this.getPaged("events", {
|
|
799
864
|
after: isoDay(since)
|
|
800
865
|
});
|
|
801
|
-
const ids = [
|
|
866
|
+
const ids = [
|
|
867
|
+
...new Set(raw.map((e) => e.project_id).filter(Boolean))
|
|
868
|
+
];
|
|
802
869
|
const paths = new Map(
|
|
803
870
|
await Promise.all(
|
|
804
871
|
ids.map(async (id) => [id, await this.projectPath(id)])
|
|
@@ -809,7 +876,7 @@ var GitLabProvider = class {
|
|
|
809
876
|
return {
|
|
810
877
|
at: String(event.created_at).slice(0, 16),
|
|
811
878
|
action: event.action_name,
|
|
812
|
-
project: paths.get(event.project_id) ?? "",
|
|
879
|
+
project: paths.get(event.project_id ?? -1) ?? "",
|
|
813
880
|
targetType: event.target_type ?? "",
|
|
814
881
|
title: event.target_title ?? "",
|
|
815
882
|
branch: push.ref ?? "",
|
|
@@ -821,7 +888,9 @@ var GitLabProvider = class {
|
|
|
821
888
|
async countUnresolved(projectId, iid) {
|
|
822
889
|
const discussions = await this.getJson(
|
|
823
890
|
`projects/${projectId}/merge_requests/${iid}/discussions`,
|
|
824
|
-
{
|
|
891
|
+
{
|
|
892
|
+
per_page: PAGE_SIZE2
|
|
893
|
+
}
|
|
825
894
|
);
|
|
826
895
|
if (!discussions) return 0;
|
|
827
896
|
return discussions.filter((discussion) => {
|
|
@@ -874,7 +943,9 @@ var GitLabProvider = class {
|
|
|
874
943
|
async approvedByMe(projectId, iid, uid) {
|
|
875
944
|
const approvals = await this.getJson(`projects/${projectId}/merge_requests/${iid}/approvals`);
|
|
876
945
|
if (!approvals) return false;
|
|
877
|
-
return (approvals.approved_by ?? []).some(
|
|
946
|
+
return (approvals.approved_by ?? []).some(
|
|
947
|
+
(entry) => entry.user?.id === uid
|
|
948
|
+
);
|
|
878
949
|
}
|
|
879
950
|
async getReviews(identity, today) {
|
|
880
951
|
const raw = await this.getPaged("merge_requests", {
|
|
@@ -882,7 +953,9 @@ var GitLabProvider = class {
|
|
|
882
953
|
state: "opened",
|
|
883
954
|
reviewer_id: identity.id
|
|
884
955
|
});
|
|
885
|
-
const cutoff = isoDay(
|
|
956
|
+
const cutoff = isoDay(
|
|
957
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY)
|
|
958
|
+
);
|
|
886
959
|
const rows = await Promise.all(
|
|
887
960
|
raw.map(async (mr) => ({
|
|
888
961
|
provider: "gitlab",
|
|
@@ -894,7 +967,11 @@ var GitLabProvider = class {
|
|
|
894
967
|
draft: mr.draft,
|
|
895
968
|
url: mr.web_url,
|
|
896
969
|
fresh: String(mr.updated_at).slice(0, 10) >= cutoff,
|
|
897
|
-
approvedByMe: await this.approvedByMe(
|
|
970
|
+
approvedByMe: await this.approvedByMe(
|
|
971
|
+
mr.project_id,
|
|
972
|
+
mr.iid,
|
|
973
|
+
identity.id
|
|
974
|
+
)
|
|
898
975
|
}))
|
|
899
976
|
);
|
|
900
977
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
@@ -906,7 +983,9 @@ var GitLabProvider = class {
|
|
|
906
983
|
) ?? [];
|
|
907
984
|
const job = jobs.find((j) => j.status === "failed");
|
|
908
985
|
if (!job) return null;
|
|
909
|
-
const trace = await this.getText(
|
|
986
|
+
const trace = await this.getText(
|
|
987
|
+
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
988
|
+
);
|
|
910
989
|
return {
|
|
911
990
|
provider: "gitlab",
|
|
912
991
|
project: mr.project,
|
|
@@ -939,7 +1018,12 @@ function hostFrom(cliHost, envHost, probe, fallback, labels) {
|
|
|
939
1018
|
if (cliHost) return cliHost;
|
|
940
1019
|
if (envHost) return envHost;
|
|
941
1020
|
const found = probe();
|
|
942
|
-
return resolveHost(
|
|
1021
|
+
return resolveHost(
|
|
1022
|
+
void 0,
|
|
1023
|
+
void 0,
|
|
1024
|
+
found.length > 0 ? found : fallback,
|
|
1025
|
+
labels
|
|
1026
|
+
);
|
|
943
1027
|
}
|
|
944
1028
|
function chooseKind(options = {}) {
|
|
945
1029
|
const env = options.env ?? process.env;
|
|
@@ -992,7 +1076,13 @@ function connect(options = {}) {
|
|
|
992
1076
|
const env = options.env ?? process.env;
|
|
993
1077
|
const probe = options.probe ?? DEFAULT_PROBE;
|
|
994
1078
|
if (chooseKind(options) === "github") {
|
|
995
|
-
const host2 = hostFrom(
|
|
1079
|
+
const host2 = hostFrom(
|
|
1080
|
+
options.host,
|
|
1081
|
+
env.GITHUB_HOST,
|
|
1082
|
+
probe.github,
|
|
1083
|
+
[DOT_COM],
|
|
1084
|
+
GITHUB_LABELS
|
|
1085
|
+
);
|
|
996
1086
|
const token2 = resolveToken(
|
|
997
1087
|
host2,
|
|
998
1088
|
options.token,
|
|
@@ -1002,7 +1092,13 @@ function connect(options = {}) {
|
|
|
1002
1092
|
);
|
|
1003
1093
|
return new GitHubProvider(host2, token2);
|
|
1004
1094
|
}
|
|
1005
|
-
const host = hostFrom(
|
|
1095
|
+
const host = hostFrom(
|
|
1096
|
+
options.host,
|
|
1097
|
+
env.GITLAB_HOST,
|
|
1098
|
+
probe.gitlab,
|
|
1099
|
+
[],
|
|
1100
|
+
GITLAB_LABELS
|
|
1101
|
+
);
|
|
1006
1102
|
const token = resolveToken(
|
|
1007
1103
|
host,
|
|
1008
1104
|
options.token,
|
|
@@ -1066,10 +1162,18 @@ function eventLine(event) {
|
|
|
1066
1162
|
}
|
|
1067
1163
|
function toMarkdown(report, lang = "en") {
|
|
1068
1164
|
const base = STRINGS[lang] ?? STRINGS.en;
|
|
1069
|
-
const t = {
|
|
1165
|
+
const t = {
|
|
1166
|
+
...base,
|
|
1167
|
+
...PROVIDER_STRINGS[report.provider][lang] ?? {}
|
|
1168
|
+
};
|
|
1070
1169
|
const ref = REF_PREFIX[report.provider];
|
|
1071
1170
|
const out = [];
|
|
1072
|
-
out.push(
|
|
1171
|
+
out.push(
|
|
1172
|
+
`# ${report.today.label} \u2014 ${report.user}`,
|
|
1173
|
+
"",
|
|
1174
|
+
`_${t.digest}_`,
|
|
1175
|
+
""
|
|
1176
|
+
);
|
|
1073
1177
|
if (report.previousDays.length === 0) {
|
|
1074
1178
|
out.push(`## ${t.previous}`, "", `_${t.nothing}_`, "");
|
|
1075
1179
|
}
|
|
@@ -1096,7 +1200,10 @@ function toMarkdown(report, lang = "en") {
|
|
|
1096
1200
|
}
|
|
1097
1201
|
const pending = report.reviews.filter((r) => !r.approvedByMe);
|
|
1098
1202
|
if (pending.length > 0) {
|
|
1099
|
-
out.push(
|
|
1203
|
+
out.push(
|
|
1204
|
+
`## ${t.reviews} (${report.reviewPendingCount} ${t.pending})`,
|
|
1205
|
+
""
|
|
1206
|
+
);
|
|
1100
1207
|
for (const review of pending) {
|
|
1101
1208
|
out.push(
|
|
1102
1209
|
`- \`${review.project}\` ${ref}${review.iid} ${review.title} \u2014 ${review.author}`
|
|
@@ -1107,7 +1214,9 @@ function toMarkdown(report, lang = "en") {
|
|
|
1107
1214
|
if (report.blockers.length > 0) {
|
|
1108
1215
|
out.push(`## ${t.blockers}`, "");
|
|
1109
1216
|
for (const blocker of report.blockers) {
|
|
1110
|
-
out.push(
|
|
1217
|
+
out.push(
|
|
1218
|
+
`- \`${blocker.project}\` ${ref}${blocker.mr} \u2014 job \`${blocker.job}\``
|
|
1219
|
+
);
|
|
1111
1220
|
out.push(...blocker.errors.map((line) => ` - \`${line}\``));
|
|
1112
1221
|
}
|
|
1113
1222
|
out.push("");
|
|
@@ -1175,21 +1284,29 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
1175
1284
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1176
1285
|
function readStandupSkillBody() {
|
|
1177
1286
|
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1178
|
-
const skillPath = join2(
|
|
1287
|
+
const skillPath = join2(
|
|
1288
|
+
findPackageRoot(moduleDir),
|
|
1289
|
+
"skills",
|
|
1290
|
+
"standup",
|
|
1291
|
+
"SKILL.md"
|
|
1292
|
+
);
|
|
1179
1293
|
if (!existsSync2(skillPath)) {
|
|
1180
1294
|
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1181
1295
|
}
|
|
1182
1296
|
const content = readFileSync2(skillPath, "utf8");
|
|
1183
|
-
const withoutFrontmatter = content.replace(
|
|
1297
|
+
const withoutFrontmatter = content.replace(
|
|
1298
|
+
/^---\r?\n[\s\S]*?\r?\n---\r?\n/,
|
|
1299
|
+
""
|
|
1300
|
+
);
|
|
1184
1301
|
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1185
1302
|
}
|
|
1186
1303
|
|
|
1187
1304
|
export {
|
|
1188
|
-
STALE_DAYS,
|
|
1189
1305
|
isoDay,
|
|
1190
1306
|
localAt,
|
|
1191
1307
|
label,
|
|
1192
1308
|
previousActiveDays,
|
|
1309
|
+
STALE_DAYS,
|
|
1193
1310
|
classify,
|
|
1194
1311
|
markMissingPipelines,
|
|
1195
1312
|
ConfigError,
|
|
@@ -1203,6 +1320,7 @@ export {
|
|
|
1203
1320
|
glabToken,
|
|
1204
1321
|
ghHosts,
|
|
1205
1322
|
ghToken,
|
|
1323
|
+
toChatText,
|
|
1206
1324
|
WEBHOOK_KINDS,
|
|
1207
1325
|
postWebhook,
|
|
1208
1326
|
inferWebhookKind,
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
postWebhook,
|
|
9
9
|
readStandupSkillBody,
|
|
10
10
|
toMarkdown
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-J5LAGA3U.js";
|
|
12
12
|
|
|
13
13
|
// src/cli/cli.ts
|
|
14
14
|
import { realpathSync } from "fs";
|
|
@@ -64,7 +64,11 @@ async function main(argv) {
|
|
|
64
64
|
markdown: { type: "boolean", default: false }
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
|
-
const report = await buildReport(
|
|
67
|
+
const report = await buildReport(
|
|
68
|
+
connect(values),
|
|
69
|
+
/* @__PURE__ */ new Date(),
|
|
70
|
+
values.lang
|
|
71
|
+
);
|
|
68
72
|
const output = values.markdown ? toMarkdown(report, values.lang) : JSON.stringify(report, null, 1);
|
|
69
73
|
process.stdout.write(`${output}
|
|
70
74
|
`);
|
|
@@ -75,7 +79,10 @@ async function main(argv) {
|
|
|
75
79
|
args: rest,
|
|
76
80
|
options: {
|
|
77
81
|
...Object.fromEntries(
|
|
78
|
-
WEBHOOK_KINDS.map((kind2) => [
|
|
82
|
+
WEBHOOK_KINDS.map((kind2) => [
|
|
83
|
+
kind2,
|
|
84
|
+
{ type: "string" }
|
|
85
|
+
])
|
|
79
86
|
),
|
|
80
87
|
text: { type: "string", default: "-" }
|
|
81
88
|
}
|
|
@@ -83,7 +90,9 @@ async function main(argv) {
|
|
|
83
90
|
const urls = values;
|
|
84
91
|
const kind = WEBHOOK_KINDS.find((candidate) => urls[candidate]);
|
|
85
92
|
if (!kind) {
|
|
86
|
-
const flags = WEBHOOK_KINDS.map(
|
|
93
|
+
const flags = WEBHOOK_KINDS.map(
|
|
94
|
+
(candidate) => `--${candidate} URL`
|
|
95
|
+
).join(" | ");
|
|
87
96
|
process.stderr.write(`Pass one of: ${flags}.
|
|
88
97
|
`);
|
|
89
98
|
return 1;
|
|
@@ -99,7 +108,12 @@ async function main(argv) {
|
|
|
99
108
|
}
|
|
100
109
|
if (command === "mcp") {
|
|
101
110
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
102
|
-
const serverPath = join(
|
|
111
|
+
const serverPath = join(
|
|
112
|
+
findPackageRoot(moduleDir),
|
|
113
|
+
"dist",
|
|
114
|
+
"mcp",
|
|
115
|
+
"server.js"
|
|
116
|
+
);
|
|
103
117
|
const serverUrl = pathToFileURL(serverPath).href;
|
|
104
118
|
const { main: startMcpServer } = await import(serverUrl);
|
|
105
119
|
await startMcpServer();
|