standup-mr 0.2.1 → 0.3.1
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 +56 -0
- package/README.md +53 -1
- package/dist/{chunk-DL2P3XR6.js → chunk-2X2XEEUY.js} +103 -43
- package/dist/cli.js +45 -4
- package/dist/index.d.ts +14 -2
- package/dist/index.js +13 -1
- package/dist/mcp/server.js +103 -54
- package/mcp/README.md +22 -8
- package/package.json +5 -10
- package/skills/standup/SKILL.md +37 -12
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@ 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.3.1] - 2026-09-01
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- One flaky sub-request no longer costs the whole note. A run fans out to
|
|
12
|
+
several requests per merge request — pull detail, check runs, reviews, and
|
|
13
|
+
for a red pipeline the run, the job and its log — and any one of them
|
|
14
|
+
returning a 502 aborted the entire standup. Server errors (5xx) and dropped
|
|
15
|
+
connections are now retried twice with a short backoff before giving up. When
|
|
16
|
+
the retries run out the error is raised exactly as before: nothing is
|
|
17
|
+
swallowed. A rejected token (401), a refused resource (403), a rate limit
|
|
18
|
+
(429) and a missing resource (404) are not retried, because retrying them is
|
|
19
|
+
never the right answer.
|
|
20
|
+
- A blocker whose diagnosis could not be fetched is now reported as a blocker
|
|
21
|
+
with `job: "unknown"` and an error line reading
|
|
22
|
+
`diagnosis unavailable: <reason>`, instead of failing the run. The merge
|
|
23
|
+
request is genuinely blocked either way, and the log fetch already degraded
|
|
24
|
+
this way; the metadata calls around it now match. **A 401 is never degraded**
|
|
25
|
+
— a revoked or invalid token still fails loudly, which is the whole reason
|
|
26
|
+
these calls throw in the first place.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `mcpName` in `package.json`, for publication to the official MCP Registry.
|
|
31
|
+
|
|
32
|
+
## [0.3.0] - 2026-09-01
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `standup mcp` — starts the stdio MCP server directly from the CLI, so
|
|
37
|
+
`npx -y standup-mr mcp` works without a local clone.
|
|
38
|
+
- `standup instructions` — prints the note-writing playbook (the standup
|
|
39
|
+
skill body, minus its YAML frontmatter) to stdout, for teaching a
|
|
40
|
+
non-Claude assistant the same rules, e.g.
|
|
41
|
+
`npx standup-mr instructions >> AGENTS.md`.
|
|
42
|
+
- README section for using the MCP server from Cursor, Codex, or another
|
|
43
|
+
assistant.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- The skill wrote the note in English even when the user was speaking Turkish.
|
|
48
|
+
The language rule was buried as a clause inside a sentence about section
|
|
49
|
+
structure, and `fetch` was called with no `--lang`, so every date label in
|
|
50
|
+
the JSON came back English and outweighed it. The rule is now a standalone,
|
|
51
|
+
unmissable line, and the skill passes `--lang` to match the user's language
|
|
52
|
+
so the payload reinforces the note instead of fighting it.
|
|
53
|
+
- On a machine where both `gh` and `glab` are authenticated, every run failed
|
|
54
|
+
once before succeeding: the skill called `fetch` with no `--provider`, hit
|
|
55
|
+
the "both authenticated" error, then retried with the flag. The skill now
|
|
56
|
+
passes `--provider` on the first call whenever it's known, and only asks the
|
|
57
|
+
user (instead of guessing) when it genuinely isn't.
|
|
58
|
+
|
|
59
|
+
Nothing breaking. `@modelcontextprotocol/sdk` moves from a dev dependency to
|
|
60
|
+
a regular dependency, since the CLI can now start the MCP server itself; the
|
|
61
|
+
CLI's `fetch`/`post`/`instructions` path still pulls nothing.
|
|
62
|
+
|
|
7
63
|
## [0.2.1] - 2026-08-31
|
|
8
64
|
|
|
9
65
|
### Fixed
|
package/README.md
CHANGED
|
@@ -76,6 +76,52 @@ skill. From inside Claude Code:
|
|
|
76
76
|
|
|
77
77
|
Then type `/standup`. Updates come with `/plugin marketplace update standup-mr`.
|
|
78
78
|
|
|
79
|
+
### Using it from Cursor, Codex, or another assistant
|
|
80
|
+
|
|
81
|
+
If you're not using Claude Code, wire up the MCP server for live data and
|
|
82
|
+
paste in the note-writing rules separately.
|
|
83
|
+
|
|
84
|
+
**Cursor** — add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json`
|
|
85
|
+
(project-local):
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"standup": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
93
|
+
"env": {
|
|
94
|
+
"GITHUB_TOKEN": "ghp_..."
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Codex** — add to `~/.codex/config.toml`:
|
|
102
|
+
|
|
103
|
+
```toml
|
|
104
|
+
[mcp_servers.standup]
|
|
105
|
+
command = "npx"
|
|
106
|
+
args = ["-y", "standup-mr", "mcp"]
|
|
107
|
+
|
|
108
|
+
[mcp_servers.standup.env]
|
|
109
|
+
GITHUB_TOKEN = "ghp_..."
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The exact config key and file path are version-dependent for both clients —
|
|
113
|
+
if a snippet above doesn't work, check [Cursor's MCP
|
|
114
|
+
docs](https://docs.cursor.com/context/mcp) or Codex's own config
|
|
115
|
+
documentation for the current format rather than trusting this file blindly.
|
|
116
|
+
|
|
117
|
+
The tool only returns data; the assistant still needs the note-writing rules
|
|
118
|
+
that the Claude Code skill carries. Paste them into whatever instructions
|
|
119
|
+
file your assistant reads (e.g. `AGENTS.md`):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx standup-mr instructions >> AGENTS.md
|
|
123
|
+
```
|
|
124
|
+
|
|
79
125
|
## `--markdown` is a digest, not a written note
|
|
80
126
|
|
|
81
127
|
`--markdown` organizes the raw material into readable sections. It does **not**
|
|
@@ -142,6 +188,10 @@ that the package is missing.
|
|
|
142
188
|
- **On GitHub, CI that reports only through the legacy commit-statuses API**
|
|
143
189
|
— still how some vendors integrate — shows up as `pipelineMissing`. Check
|
|
144
190
|
state is read from check-runs only.
|
|
191
|
+
- **A blocker whose diagnosis could not be fetched is still reported**, as
|
|
192
|
+
`job: "unknown"` with a `diagnosis unavailable: …` error line. The merge
|
|
193
|
+
request is blocked either way; only the explanation is missing. Server
|
|
194
|
+
errors are retried twice first, and a rejected token still fails the run.
|
|
145
195
|
|
|
146
196
|
## Upgrading from 0.1.x
|
|
147
197
|
|
|
@@ -162,7 +212,9 @@ the `standup` skill changed along with the report shape.
|
|
|
162
212
|
|
|
163
213
|
## Requirements
|
|
164
214
|
|
|
165
|
-
Node 20 or newer.
|
|
215
|
+
Node 20 or newer. The CLI core (`fetch`, `post`, `instructions`) pulls no
|
|
216
|
+
runtime dependencies. The MCP server (`mcp` command) brings one:
|
|
217
|
+
`@modelcontextprotocol/sdk`.
|
|
166
218
|
|
|
167
219
|
## License
|
|
168
220
|
|
|
@@ -218,9 +218,11 @@ function buildUrl(api, path, params) {
|
|
|
218
218
|
return `${base}?${query.toString()}`;
|
|
219
219
|
}
|
|
220
220
|
var ApiError = class extends Error {
|
|
221
|
-
|
|
221
|
+
status;
|
|
222
|
+
constructor(message, status) {
|
|
222
223
|
super(message);
|
|
223
224
|
this.name = "ApiError";
|
|
225
|
+
this.status = status;
|
|
224
226
|
}
|
|
225
227
|
};
|
|
226
228
|
function remaining(response) {
|
|
@@ -244,27 +246,67 @@ function assertUsable(response, host) {
|
|
|
244
246
|
if (response.ok || response.status === 404) return;
|
|
245
247
|
if (response.status === 403 || response.status === 429) {
|
|
246
248
|
if (remaining(response) === "0") {
|
|
247
|
-
throw new ApiError(`${host} rate limit reached${resetAt(response)}
|
|
249
|
+
throw new ApiError(`${host} rate limit reached${resetAt(response)}.`, response.status);
|
|
248
250
|
}
|
|
249
251
|
const retry = retryAfter(response);
|
|
250
|
-
if (retry) throw new ApiError(`${host} rate limit reached${retry}
|
|
252
|
+
if (retry) throw new ApiError(`${host} rate limit reached${retry}.`, response.status);
|
|
251
253
|
}
|
|
252
254
|
if (response.status === 401) {
|
|
253
255
|
throw new ApiError(
|
|
254
|
-
`${host} rejected the token (401). Check the token and its scopes
|
|
256
|
+
`${host} rejected the token (401). Check the token and its scopes.`,
|
|
257
|
+
401
|
|
255
258
|
);
|
|
256
259
|
}
|
|
257
260
|
if (response.status === 403) {
|
|
258
261
|
throw new ApiError(
|
|
259
|
-
`${host} refused the request (403). The token has no access to that resource
|
|
262
|
+
`${host} refused the request (403). The token has no access to that resource.`,
|
|
263
|
+
403
|
|
260
264
|
);
|
|
261
265
|
}
|
|
262
|
-
throw new ApiError(`${host} returned ${response.status}
|
|
266
|
+
throw new ApiError(`${host} returned ${response.status}.`, response.status);
|
|
263
267
|
}
|
|
264
268
|
function unreachable(host, cause) {
|
|
265
269
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
266
270
|
return new ApiError(`Could not reach ${host}: ${detail}`);
|
|
267
271
|
}
|
|
272
|
+
var RETRY_ATTEMPTS = 3;
|
|
273
|
+
var RETRY_BACKOFF_MS = [250, 750];
|
|
274
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
275
|
+
async function sendWithRetry(fetchImpl, url, init, host, sleep = wait) {
|
|
276
|
+
let last;
|
|
277
|
+
for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt += 1) {
|
|
278
|
+
if (attempt > 0) await sleep(RETRY_BACKOFF_MS[attempt - 1]);
|
|
279
|
+
const final = attempt === RETRY_ATTEMPTS - 1;
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetchImpl(url, init);
|
|
282
|
+
if (response.status >= 500 && !final) continue;
|
|
283
|
+
return response;
|
|
284
|
+
} catch (cause) {
|
|
285
|
+
last = cause;
|
|
286
|
+
if (final) throw unreachable(host, cause);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
throw unreachable(host, last);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/providers/base/diagnosis.ts
|
|
293
|
+
var DIAGNOSIS_UNAVAILABLE = "diagnosis unavailable";
|
|
294
|
+
var UNKNOWN = "unknown";
|
|
295
|
+
function degradable(cause) {
|
|
296
|
+
return cause instanceof ApiError && cause.status !== 401;
|
|
297
|
+
}
|
|
298
|
+
function undiagnosed(mr, cause) {
|
|
299
|
+
return {
|
|
300
|
+
provider: mr.provider,
|
|
301
|
+
project: mr.project,
|
|
302
|
+
mr: mr.iid,
|
|
303
|
+
title: mr.title,
|
|
304
|
+
job: UNKNOWN,
|
|
305
|
+
stage: UNKNOWN,
|
|
306
|
+
url: mr.url,
|
|
307
|
+
errors: [`${DIAGNOSIS_UNAVAILABLE}: ${cause.message}`]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
268
310
|
|
|
269
311
|
// src/trace/trace.constants.ts
|
|
270
312
|
var ANSI = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
@@ -414,11 +456,12 @@ var GitHubProvider = class {
|
|
|
414
456
|
};
|
|
415
457
|
}
|
|
416
458
|
async send(url, init) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
459
|
+
return await sendWithRetry(
|
|
460
|
+
this.fetchImpl,
|
|
461
|
+
url,
|
|
462
|
+
{ headers: this.headers(), ...init },
|
|
463
|
+
this.host
|
|
464
|
+
);
|
|
422
465
|
}
|
|
423
466
|
async getJson(path, params) {
|
|
424
467
|
const response = await this.send(buildUrl(this.api, path, params));
|
|
@@ -648,9 +691,17 @@ var GitHubProvider = class {
|
|
|
648
691
|
}
|
|
649
692
|
async getBlockers(mrs) {
|
|
650
693
|
const red = mrs.filter((mr) => mr.pipeline === "failed");
|
|
651
|
-
const diagnosed = await Promise.all(red.map((mr) => this.
|
|
694
|
+
const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
|
|
652
695
|
return diagnosed.filter((row) => row !== null);
|
|
653
696
|
}
|
|
697
|
+
async tryDiagnose(mr) {
|
|
698
|
+
try {
|
|
699
|
+
return await this.diagnose(mr);
|
|
700
|
+
} catch (cause) {
|
|
701
|
+
if (degradable(cause)) return undiagnosed(mr, cause);
|
|
702
|
+
throw cause;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
654
705
|
};
|
|
655
706
|
|
|
656
707
|
// src/providers/gitlab/gitlab.constants.ts
|
|
@@ -671,14 +722,12 @@ var GitLabProvider = class {
|
|
|
671
722
|
this.fetchImpl = fetchImpl;
|
|
672
723
|
}
|
|
673
724
|
async send(url) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
throw unreachable(this.host, cause);
|
|
681
|
-
}
|
|
725
|
+
const response = await sendWithRetry(
|
|
726
|
+
this.fetchImpl,
|
|
727
|
+
url,
|
|
728
|
+
{ headers: { "PRIVATE-TOKEN": this.token } },
|
|
729
|
+
this.host
|
|
730
|
+
);
|
|
682
731
|
assertUsable(response, this.host);
|
|
683
732
|
return response;
|
|
684
733
|
}
|
|
@@ -830,31 +879,36 @@ var GitLabProvider = class {
|
|
|
830
879
|
);
|
|
831
880
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
832
881
|
}
|
|
882
|
+
async diagnose(mr) {
|
|
883
|
+
const jobs = await this.getJson(
|
|
884
|
+
`projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
|
|
885
|
+
{ per_page: PAGE_SIZE2 }
|
|
886
|
+
) ?? [];
|
|
887
|
+
const job = jobs.find((j) => j.status === "failed");
|
|
888
|
+
if (!job) return null;
|
|
889
|
+
const trace = await this.getText(`projects/${mr.projectId}/jobs/${job.id}/trace`);
|
|
890
|
+
return {
|
|
891
|
+
provider: "gitlab",
|
|
892
|
+
project: mr.project,
|
|
893
|
+
mr: mr.iid,
|
|
894
|
+
title: mr.title,
|
|
895
|
+
job: job.name,
|
|
896
|
+
stage: job.stage,
|
|
897
|
+
url: mr.url,
|
|
898
|
+
errors: extractErrors(trace)
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
async tryDiagnose(mr) {
|
|
902
|
+
try {
|
|
903
|
+
return await this.diagnose(mr);
|
|
904
|
+
} catch (cause) {
|
|
905
|
+
if (degradable(cause)) return undiagnosed(mr, cause);
|
|
906
|
+
throw cause;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
833
909
|
async getBlockers(mrs) {
|
|
834
910
|
const red = mrs.filter((mr) => mr.pipeline === "failed");
|
|
835
|
-
const diagnosed = await Promise.all(
|
|
836
|
-
red.map(async (mr) => {
|
|
837
|
-
const jobs = await this.getJson(
|
|
838
|
-
`projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
|
|
839
|
-
{ per_page: PAGE_SIZE2 }
|
|
840
|
-
) ?? [];
|
|
841
|
-
const job = jobs.find((j) => j.status === "failed");
|
|
842
|
-
if (!job) return null;
|
|
843
|
-
const trace = await this.getText(
|
|
844
|
-
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
845
|
-
);
|
|
846
|
-
return {
|
|
847
|
-
provider: "gitlab",
|
|
848
|
-
project: mr.project,
|
|
849
|
-
mr: mr.iid,
|
|
850
|
-
title: mr.title,
|
|
851
|
-
job: job.name,
|
|
852
|
-
stage: job.stage,
|
|
853
|
-
url: mr.url,
|
|
854
|
-
errors: extractErrors(trace)
|
|
855
|
-
};
|
|
856
|
-
})
|
|
857
|
-
);
|
|
911
|
+
const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
|
|
858
912
|
return diagnosed.filter((row) => row !== null);
|
|
859
913
|
}
|
|
860
914
|
};
|
|
@@ -1103,6 +1157,12 @@ export {
|
|
|
1103
1157
|
ApiError,
|
|
1104
1158
|
assertUsable,
|
|
1105
1159
|
unreachable,
|
|
1160
|
+
RETRY_ATTEMPTS,
|
|
1161
|
+
RETRY_BACKOFF_MS,
|
|
1162
|
+
sendWithRetry,
|
|
1163
|
+
DIAGNOSIS_UNAVAILABLE,
|
|
1164
|
+
degradable,
|
|
1165
|
+
undiagnosed,
|
|
1106
1166
|
extractErrors,
|
|
1107
1167
|
repoFromUrl,
|
|
1108
1168
|
mapEvent,
|
package/dist/cli.js
CHANGED
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
connect,
|
|
6
6
|
postWebhook,
|
|
7
7
|
toMarkdown
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-2X2XEEUY.js";
|
|
9
9
|
|
|
10
10
|
// src/cli/cli.ts
|
|
11
|
-
import { realpathSync } from "fs";
|
|
12
|
-
import {
|
|
11
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
13
14
|
import { parseArgs } from "util";
|
|
14
15
|
|
|
15
16
|
// src/cli/cli.constants.ts
|
|
@@ -19,13 +20,20 @@ Usage:
|
|
|
19
20
|
standup fetch [--provider github|gitlab] [--host H] [--token T]
|
|
20
21
|
[--lang en|tr] [--markdown]
|
|
21
22
|
standup post (--slack URL | --discord URL) [--text TEXT]
|
|
23
|
+
standup mcp
|
|
24
|
+
standup instructions
|
|
22
25
|
|
|
23
26
|
The provider is taken from --provider, then STANDUP_PROVIDER, then whichever of
|
|
24
27
|
GITHUB_* / GITLAB_* is set, then whichever of gh / glab is logged in.
|
|
25
28
|
|
|
26
29
|
Credentials resolve as: flag, then GITHUB_HOST / GITHUB_TOKEN (or GITLAB_HOST /
|
|
27
30
|
GITLAB_TOKEN), then the gh or glab config.
|
|
28
|
-
--text defaults to '-', meaning read stdin
|
|
31
|
+
--text defaults to '-', meaning read stdin.
|
|
32
|
+
|
|
33
|
+
standup mcp starts the stdio MCP server, exposing get_standup_data.
|
|
34
|
+
standup instructions prints the note-writing playbook (the standup skill body)
|
|
35
|
+
to stdout, for teaching a non-Claude assistant the same rules, e.g.:
|
|
36
|
+
npx standup-mr instructions >> AGENTS.md`;
|
|
29
37
|
|
|
30
38
|
// src/cli/cli.ts
|
|
31
39
|
async function readStdin() {
|
|
@@ -33,6 +41,27 @@ async function readStdin() {
|
|
|
33
41
|
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
34
42
|
return Buffer.concat(chunks).toString("utf8");
|
|
35
43
|
}
|
|
44
|
+
function findPackageRoot(startDir) {
|
|
45
|
+
let dir = startDir;
|
|
46
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
47
|
+
const parent = dirname(dir);
|
|
48
|
+
if (parent === dir) {
|
|
49
|
+
throw new Error(`Could not locate package.json above ${startDir}`);
|
|
50
|
+
}
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
return dir;
|
|
54
|
+
}
|
|
55
|
+
function readStandupSkillBody() {
|
|
56
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
const skillPath = join(findPackageRoot(moduleDir), "skills", "standup", "SKILL.md");
|
|
58
|
+
if (!existsSync(skillPath)) {
|
|
59
|
+
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
60
|
+
}
|
|
61
|
+
const content = readFileSync(skillPath, "utf8");
|
|
62
|
+
const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
63
|
+
return withoutFrontmatter.replace(/^\n+/, "");
|
|
64
|
+
}
|
|
36
65
|
async function main(argv) {
|
|
37
66
|
const [command, ...rest] = argv;
|
|
38
67
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -80,6 +109,18 @@ async function main(argv) {
|
|
|
80
109
|
await postWebhook(url, text, values.slack ? "slack" : "discord");
|
|
81
110
|
return 0;
|
|
82
111
|
}
|
|
112
|
+
if (command === "mcp") {
|
|
113
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
114
|
+
const serverPath = join(findPackageRoot(moduleDir), "dist", "mcp", "server.js");
|
|
115
|
+
const serverUrl = pathToFileURL(serverPath).href;
|
|
116
|
+
const { main: startMcpServer } = await import(serverUrl);
|
|
117
|
+
await startMcpServer();
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
if (command === "instructions") {
|
|
121
|
+
process.stdout.write(readStandupSkillBody());
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
83
124
|
process.stderr.write(`Unknown command: ${command}
|
|
84
125
|
|
|
85
126
|
${USAGE}
|
package/dist/index.d.ts
CHANGED
|
@@ -142,10 +142,19 @@ interface Provider {
|
|
|
142
142
|
|
|
143
143
|
declare function buildUrl(api: string, path: string, params?: Record<string, string | number>): string;
|
|
144
144
|
declare class ApiError extends Error {
|
|
145
|
-
|
|
145
|
+
readonly status?: number;
|
|
146
|
+
constructor(message: string, status?: number);
|
|
146
147
|
}
|
|
147
148
|
declare function assertUsable(response: Response, host: string): void;
|
|
148
149
|
declare function unreachable(host: string, cause: unknown): ApiError;
|
|
150
|
+
declare const RETRY_ATTEMPTS = 3;
|
|
151
|
+
declare const RETRY_BACKOFF_MS: number[];
|
|
152
|
+
type Sleep = (ms: number) => Promise<void>;
|
|
153
|
+
declare function sendWithRetry(fetchImpl: FetchLike, url: string, init: RequestInit | undefined, host: string, sleep?: Sleep): Promise<Response>;
|
|
154
|
+
|
|
155
|
+
declare const DIAGNOSIS_UNAVAILABLE = "diagnosis unavailable";
|
|
156
|
+
declare function degradable(cause: unknown): cause is ApiError;
|
|
157
|
+
declare function undiagnosed(mr: MergeRequest, cause: ApiError): Blocker;
|
|
149
158
|
|
|
150
159
|
type Params$1 = Record<string, string | number>;
|
|
151
160
|
declare class GitHubProvider implements Provider {
|
|
@@ -171,6 +180,7 @@ declare class GitHubProvider implements Provider {
|
|
|
171
180
|
private checkRunBlocker;
|
|
172
181
|
private diagnose;
|
|
173
182
|
getBlockers(mrs: MergeRequest[]): Promise<Blocker[]>;
|
|
183
|
+
private tryDiagnose;
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
interface RawCommit {
|
|
@@ -247,6 +257,8 @@ declare class GitLabProvider implements Provider {
|
|
|
247
257
|
getMyMrs(today: Date): Promise<MergeRequest[]>;
|
|
248
258
|
private approvedByMe;
|
|
249
259
|
getReviews(identity: Identity, today: Date): Promise<Review[]>;
|
|
260
|
+
private diagnose;
|
|
261
|
+
private tryDiagnose;
|
|
250
262
|
getBlockers(mrs: MergeRequest[]): Promise<Blocker[]>;
|
|
251
263
|
}
|
|
252
264
|
|
|
@@ -269,4 +281,4 @@ declare function buildReport(provider: Provider, today: Date, lang?: string, loo
|
|
|
269
281
|
|
|
270
282
|
declare function extractErrors(rawTrace: string, limit?: number): string[];
|
|
271
283
|
|
|
272
|
-
export { type ActiveDay, type ActivityEvent, ApiError, type Blocker, type Bucket, ConfigError, type FetchLike, GITHUB_LABELS, GITLAB_LABELS, GitHubProvider, GitLabProvider, type Identity, type MergeRequest, type Provider, type ProviderKind, type ProviderLabels, type Review, STALE_DAYS, type SelectOptions, type StandupReport, approvedBy, assertUsable, buildReport, buildUrl, chooseKind, classify, connect, countChangesRequested, extractErrors, ghHosts, ghToken, glabHosts, glabToken, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, repoFromUrl, resolveHost, resolveToken, toMarkdown, unreachable };
|
|
284
|
+
export { type ActiveDay, type ActivityEvent, ApiError, type Blocker, type Bucket, ConfigError, DIAGNOSIS_UNAVAILABLE, type FetchLike, GITHUB_LABELS, GITLAB_LABELS, GitHubProvider, GitLabProvider, type Identity, type MergeRequest, type Provider, type ProviderKind, type ProviderLabels, RETRY_ATTEMPTS, RETRY_BACKOFF_MS, type Review, STALE_DAYS, type SelectOptions, type StandupReport, approvedBy, assertUsable, buildReport, buildUrl, chooseKind, classify, connect, countChangesRequested, degradable, extractErrors, ghHosts, ghToken, glabHosts, glabToken, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, repoFromUrl, resolveHost, resolveToken, sendWithRetry, toMarkdown, undiagnosed, unreachable };
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ApiError,
|
|
4
4
|
ConfigError,
|
|
5
|
+
DIAGNOSIS_UNAVAILABLE,
|
|
5
6
|
GITHUB_LABELS,
|
|
6
7
|
GITLAB_LABELS,
|
|
7
8
|
GitHubProvider,
|
|
8
9
|
GitLabProvider,
|
|
10
|
+
RETRY_ATTEMPTS,
|
|
11
|
+
RETRY_BACKOFF_MS,
|
|
9
12
|
STALE_DAYS,
|
|
10
13
|
approvedBy,
|
|
11
14
|
assertUsable,
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
classify,
|
|
16
19
|
connect,
|
|
17
20
|
countChangesRequested,
|
|
21
|
+
degradable,
|
|
18
22
|
extractErrors,
|
|
19
23
|
ghHosts,
|
|
20
24
|
ghToken,
|
|
@@ -34,16 +38,21 @@ import {
|
|
|
34
38
|
repoFromUrl,
|
|
35
39
|
resolveHost,
|
|
36
40
|
resolveToken,
|
|
41
|
+
sendWithRetry,
|
|
37
42
|
toMarkdown,
|
|
43
|
+
undiagnosed,
|
|
38
44
|
unreachable
|
|
39
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-2X2XEEUY.js";
|
|
40
46
|
export {
|
|
41
47
|
ApiError,
|
|
42
48
|
ConfigError,
|
|
49
|
+
DIAGNOSIS_UNAVAILABLE,
|
|
43
50
|
GITHUB_LABELS,
|
|
44
51
|
GITLAB_LABELS,
|
|
45
52
|
GitHubProvider,
|
|
46
53
|
GitLabProvider,
|
|
54
|
+
RETRY_ATTEMPTS,
|
|
55
|
+
RETRY_BACKOFF_MS,
|
|
47
56
|
STALE_DAYS,
|
|
48
57
|
approvedBy,
|
|
49
58
|
assertUsable,
|
|
@@ -53,6 +62,7 @@ export {
|
|
|
53
62
|
classify,
|
|
54
63
|
connect,
|
|
55
64
|
countChangesRequested,
|
|
65
|
+
degradable,
|
|
56
66
|
extractErrors,
|
|
57
67
|
ghHosts,
|
|
58
68
|
ghToken,
|
|
@@ -72,6 +82,8 @@ export {
|
|
|
72
82
|
repoFromUrl,
|
|
73
83
|
resolveHost,
|
|
74
84
|
resolveToken,
|
|
85
|
+
sendWithRetry,
|
|
75
86
|
toMarkdown,
|
|
87
|
+
undiagnosed,
|
|
76
88
|
unreachable
|
|
77
89
|
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// mcp/server.ts
|
|
4
4
|
import { realpathSync } from "fs";
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
8
|
|
|
7
9
|
// src/config/config.ts
|
|
8
10
|
import { execFileSync } from "child_process";
|
|
@@ -241,9 +243,11 @@ function buildUrl(api, path, params) {
|
|
|
241
243
|
return `${base}?${query.toString()}`;
|
|
242
244
|
}
|
|
243
245
|
var ApiError = class extends Error {
|
|
244
|
-
|
|
246
|
+
status;
|
|
247
|
+
constructor(message, status) {
|
|
245
248
|
super(message);
|
|
246
249
|
this.name = "ApiError";
|
|
250
|
+
this.status = status;
|
|
247
251
|
}
|
|
248
252
|
};
|
|
249
253
|
function remaining(response) {
|
|
@@ -267,27 +271,67 @@ function assertUsable(response, host) {
|
|
|
267
271
|
if (response.ok || response.status === 404) return;
|
|
268
272
|
if (response.status === 403 || response.status === 429) {
|
|
269
273
|
if (remaining(response) === "0") {
|
|
270
|
-
throw new ApiError(`${host} rate limit reached${resetAt(response)}
|
|
274
|
+
throw new ApiError(`${host} rate limit reached${resetAt(response)}.`, response.status);
|
|
271
275
|
}
|
|
272
276
|
const retry = retryAfter(response);
|
|
273
|
-
if (retry) throw new ApiError(`${host} rate limit reached${retry}
|
|
277
|
+
if (retry) throw new ApiError(`${host} rate limit reached${retry}.`, response.status);
|
|
274
278
|
}
|
|
275
279
|
if (response.status === 401) {
|
|
276
280
|
throw new ApiError(
|
|
277
|
-
`${host} rejected the token (401). Check the token and its scopes
|
|
281
|
+
`${host} rejected the token (401). Check the token and its scopes.`,
|
|
282
|
+
401
|
|
278
283
|
);
|
|
279
284
|
}
|
|
280
285
|
if (response.status === 403) {
|
|
281
286
|
throw new ApiError(
|
|
282
|
-
`${host} refused the request (403). The token has no access to that resource
|
|
287
|
+
`${host} refused the request (403). The token has no access to that resource.`,
|
|
288
|
+
403
|
|
283
289
|
);
|
|
284
290
|
}
|
|
285
|
-
throw new ApiError(`${host} returned ${response.status}
|
|
291
|
+
throw new ApiError(`${host} returned ${response.status}.`, response.status);
|
|
286
292
|
}
|
|
287
293
|
function unreachable(host, cause) {
|
|
288
294
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
289
295
|
return new ApiError(`Could not reach ${host}: ${detail}`);
|
|
290
296
|
}
|
|
297
|
+
var RETRY_ATTEMPTS = 3;
|
|
298
|
+
var RETRY_BACKOFF_MS = [250, 750];
|
|
299
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
300
|
+
async function sendWithRetry(fetchImpl, url, init, host, sleep = wait) {
|
|
301
|
+
let last;
|
|
302
|
+
for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt += 1) {
|
|
303
|
+
if (attempt > 0) await sleep(RETRY_BACKOFF_MS[attempt - 1]);
|
|
304
|
+
const final = attempt === RETRY_ATTEMPTS - 1;
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetchImpl(url, init);
|
|
307
|
+
if (response.status >= 500 && !final) continue;
|
|
308
|
+
return response;
|
|
309
|
+
} catch (cause) {
|
|
310
|
+
last = cause;
|
|
311
|
+
if (final) throw unreachable(host, cause);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
throw unreachable(host, last);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/providers/base/diagnosis.ts
|
|
318
|
+
var DIAGNOSIS_UNAVAILABLE = "diagnosis unavailable";
|
|
319
|
+
var UNKNOWN = "unknown";
|
|
320
|
+
function degradable(cause) {
|
|
321
|
+
return cause instanceof ApiError && cause.status !== 401;
|
|
322
|
+
}
|
|
323
|
+
function undiagnosed(mr, cause) {
|
|
324
|
+
return {
|
|
325
|
+
provider: mr.provider,
|
|
326
|
+
project: mr.project,
|
|
327
|
+
mr: mr.iid,
|
|
328
|
+
title: mr.title,
|
|
329
|
+
job: UNKNOWN,
|
|
330
|
+
stage: UNKNOWN,
|
|
331
|
+
url: mr.url,
|
|
332
|
+
errors: [`${DIAGNOSIS_UNAVAILABLE}: ${cause.message}`]
|
|
333
|
+
};
|
|
334
|
+
}
|
|
291
335
|
|
|
292
336
|
// src/providers/github/github.constants.ts
|
|
293
337
|
var PAGE_SIZE = 100;
|
|
@@ -396,11 +440,12 @@ var GitHubProvider = class {
|
|
|
396
440
|
};
|
|
397
441
|
}
|
|
398
442
|
async send(url, init) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
443
|
+
return await sendWithRetry(
|
|
444
|
+
this.fetchImpl,
|
|
445
|
+
url,
|
|
446
|
+
{ headers: this.headers(), ...init },
|
|
447
|
+
this.host
|
|
448
|
+
);
|
|
404
449
|
}
|
|
405
450
|
async getJson(path, params) {
|
|
406
451
|
const response = await this.send(buildUrl(this.api, path, params));
|
|
@@ -630,9 +675,17 @@ var GitHubProvider = class {
|
|
|
630
675
|
}
|
|
631
676
|
async getBlockers(mrs) {
|
|
632
677
|
const red = mrs.filter((mr) => mr.pipeline === "failed");
|
|
633
|
-
const diagnosed = await Promise.all(red.map((mr) => this.
|
|
678
|
+
const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
|
|
634
679
|
return diagnosed.filter((row) => row !== null);
|
|
635
680
|
}
|
|
681
|
+
async tryDiagnose(mr) {
|
|
682
|
+
try {
|
|
683
|
+
return await this.diagnose(mr);
|
|
684
|
+
} catch (cause) {
|
|
685
|
+
if (degradable(cause)) return undiagnosed(mr, cause);
|
|
686
|
+
throw cause;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
636
689
|
};
|
|
637
690
|
|
|
638
691
|
// src/providers/gitlab/gitlab.constants.ts
|
|
@@ -653,14 +706,12 @@ var GitLabProvider = class {
|
|
|
653
706
|
this.fetchImpl = fetchImpl;
|
|
654
707
|
}
|
|
655
708
|
async send(url) {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
throw unreachable(this.host, cause);
|
|
663
|
-
}
|
|
709
|
+
const response = await sendWithRetry(
|
|
710
|
+
this.fetchImpl,
|
|
711
|
+
url,
|
|
712
|
+
{ headers: { "PRIVATE-TOKEN": this.token } },
|
|
713
|
+
this.host
|
|
714
|
+
);
|
|
664
715
|
assertUsable(response, this.host);
|
|
665
716
|
return response;
|
|
666
717
|
}
|
|
@@ -812,31 +863,36 @@ var GitLabProvider = class {
|
|
|
812
863
|
);
|
|
813
864
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
814
865
|
}
|
|
866
|
+
async diagnose(mr) {
|
|
867
|
+
const jobs = await this.getJson(
|
|
868
|
+
`projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
|
|
869
|
+
{ per_page: PAGE_SIZE2 }
|
|
870
|
+
) ?? [];
|
|
871
|
+
const job = jobs.find((j) => j.status === "failed");
|
|
872
|
+
if (!job) return null;
|
|
873
|
+
const trace = await this.getText(`projects/${mr.projectId}/jobs/${job.id}/trace`);
|
|
874
|
+
return {
|
|
875
|
+
provider: "gitlab",
|
|
876
|
+
project: mr.project,
|
|
877
|
+
mr: mr.iid,
|
|
878
|
+
title: mr.title,
|
|
879
|
+
job: job.name,
|
|
880
|
+
stage: job.stage,
|
|
881
|
+
url: mr.url,
|
|
882
|
+
errors: extractErrors(trace)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
async tryDiagnose(mr) {
|
|
886
|
+
try {
|
|
887
|
+
return await this.diagnose(mr);
|
|
888
|
+
} catch (cause) {
|
|
889
|
+
if (degradable(cause)) return undiagnosed(mr, cause);
|
|
890
|
+
throw cause;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
815
893
|
async getBlockers(mrs) {
|
|
816
894
|
const red = mrs.filter((mr) => mr.pipeline === "failed");
|
|
817
|
-
const diagnosed = await Promise.all(
|
|
818
|
-
red.map(async (mr) => {
|
|
819
|
-
const jobs = await this.getJson(
|
|
820
|
-
`projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
|
|
821
|
-
{ per_page: PAGE_SIZE2 }
|
|
822
|
-
) ?? [];
|
|
823
|
-
const job = jobs.find((j) => j.status === "failed");
|
|
824
|
-
if (!job) return null;
|
|
825
|
-
const trace = await this.getText(
|
|
826
|
-
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
827
|
-
);
|
|
828
|
-
return {
|
|
829
|
-
provider: "gitlab",
|
|
830
|
-
project: mr.project,
|
|
831
|
-
mr: mr.iid,
|
|
832
|
-
title: mr.title,
|
|
833
|
-
job: job.name,
|
|
834
|
-
stage: job.stage,
|
|
835
|
-
url: mr.url,
|
|
836
|
-
errors: extractErrors(trace)
|
|
837
|
-
};
|
|
838
|
-
})
|
|
839
|
-
);
|
|
895
|
+
const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
|
|
840
896
|
return diagnosed.filter((row) => row !== null);
|
|
841
897
|
}
|
|
842
898
|
};
|
|
@@ -964,9 +1020,7 @@ async function collect(options = {}) {
|
|
|
964
1020
|
return buildReport(provider, /* @__PURE__ */ new Date(), options.lang ?? "en");
|
|
965
1021
|
}
|
|
966
1022
|
async function main() {
|
|
967
|
-
const
|
|
968
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
969
|
-
const server = new McpServer({ name: "standup-mr", version: "0.2.1" });
|
|
1023
|
+
const server = new McpServer({ name: "standup-mr", version: "0.3.0" });
|
|
970
1024
|
server.tool(
|
|
971
1025
|
"get_standup_data",
|
|
972
1026
|
"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.",
|
|
@@ -987,14 +1041,9 @@ try {
|
|
|
987
1041
|
}
|
|
988
1042
|
if (entryUrl && import.meta.url === entryUrl) {
|
|
989
1043
|
main().catch((error) => {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
);
|
|
994
|
-
process.exitCode = 1;
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
throw error;
|
|
1044
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
1045
|
+
`);
|
|
1046
|
+
process.exitCode = 1;
|
|
998
1047
|
});
|
|
999
1048
|
}
|
|
1000
1049
|
export {
|
package/mcp/README.md
CHANGED
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
Exposes one tool, `get_standup_data`, for either GitLab or GitHub, returning
|
|
4
4
|
the same JSON as `standup fetch`.
|
|
5
5
|
|
|
6
|
-
## Install
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
npm install standup-mr @modelcontextprotocol/sdk
|
|
10
|
-
```
|
|
11
|
-
|
|
12
6
|
## Configure
|
|
13
7
|
|
|
14
8
|
GitLab:
|
|
@@ -17,8 +11,8 @@ GitLab:
|
|
|
17
11
|
{
|
|
18
12
|
"mcpServers": {
|
|
19
13
|
"standup": {
|
|
20
|
-
"command": "
|
|
21
|
-
"args": ["
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
22
16
|
"env": {
|
|
23
17
|
"GITLAB_HOST": "gitlab.example.com",
|
|
24
18
|
"GITLAB_TOKEN": "glpat-..."
|
|
@@ -30,6 +24,26 @@ GitLab:
|
|
|
30
24
|
|
|
31
25
|
GitHub:
|
|
32
26
|
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
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
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Running from a clone
|
|
43
|
+
|
|
44
|
+
If you're working from a local clone instead of the published package, point
|
|
45
|
+
`command` at `node` and `args` at the absolute path to the built server:
|
|
46
|
+
|
|
33
47
|
```json
|
|
34
48
|
{
|
|
35
49
|
"mcpServers": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "standup-mr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"mcpName": "io.github.Jubstaaa/standup-mr",
|
|
4
5
|
"description": "Standup notes from merge request state, not commit logs.",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"standup",
|
|
@@ -58,19 +59,13 @@
|
|
|
58
59
|
"typecheck": "tsc --noEmit",
|
|
59
60
|
"prepublishOnly": "tsup"
|
|
60
61
|
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
64
|
+
},
|
|
61
65
|
"devDependencies": {
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
63
66
|
"@types/bun": "^1.0.0",
|
|
64
67
|
"@types/node": "^22.0.0",
|
|
65
68
|
"tsup": "^8.0.0",
|
|
66
69
|
"typescript": "^5.5.0"
|
|
67
|
-
},
|
|
68
|
-
"peerDependencies": {
|
|
69
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
70
|
-
},
|
|
71
|
-
"peerDependenciesMeta": {
|
|
72
|
-
"@modelcontextprotocol/sdk": {
|
|
73
|
-
"optional": true
|
|
74
|
-
}
|
|
75
70
|
}
|
|
76
71
|
}
|
package/skills/standup/SKILL.md
CHANGED
|
@@ -19,22 +19,43 @@ Not a commit log — what finished, what is waiting, what is stuck.
|
|
|
19
19
|
npx standup-mr fetch
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Prints one JSON document.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Prints one JSON document. Nothing else needs running: the command resolves
|
|
23
|
+
the host and token, works out which day to report on, and reads failed job
|
|
24
|
+
logs itself.
|
|
25
|
+
|
|
26
|
+
**Know the provider before the first call, don't discover it from a failure.**
|
|
27
|
+
If the user has named GitHub or GitLab, or it is otherwise obvious from
|
|
28
|
+
context, pass it on the very first invocation:
|
|
25
29
|
|
|
26
30
|
```bash
|
|
27
31
|
npx standup-mr fetch --provider github
|
|
28
32
|
npx standup-mr fetch --provider gitlab
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
which
|
|
35
|
+
If it is not known, do not guess. The command resolves it itself, in order:
|
|
36
|
+
`--provider` → a recognisable `--host` → `STANDUP_PROVIDER` → the
|
|
37
|
+
`GITHUB_*`/`GITLAB_*` env pair → whichever of `gh`/`glab` is logged in → an
|
|
38
|
+
error naming what it found. On a machine where both `gh` and `glab` are
|
|
39
|
+
authenticated, that resolution is ambiguous and the bare call fails with
|
|
40
|
+
`Both gh and glab are authenticated. Pass --provider github or --provider
|
|
41
|
+
gitlab.` — if you hit that, ask the user which one they mean rather than
|
|
42
|
+
picking one silently, unless the conversation already makes it obvious, in
|
|
43
|
+
which case pass that provider and say so in the note.
|
|
44
|
+
|
|
45
|
+
If the command exits non-zero for any other reason, relay its stderr verbatim
|
|
46
|
+
— do not invent a cause. The usual fix is `gh auth login` or `glab auth
|
|
47
|
+
login`, depending on which provider it was trying to reach.
|
|
48
|
+
|
|
49
|
+
**Pass `--lang` to match the language the user is speaking** — `--lang tr`
|
|
50
|
+
for Turkish. It only affects the date labels inside the JSON (`"label":
|
|
51
|
+
"Friday, 28 August"` → `"Cuma, 28 Ağustos"`); the prose of the note is always
|
|
52
|
+
your own work, in the user's language, regardless of this flag. Passing it
|
|
53
|
+
matters because the payload is otherwise ~1000 lines of English labels
|
|
54
|
+
fighting a note you're trying to write in another language. Today `--lang`
|
|
55
|
+
supports only `en` (default) and `tr` — any other value silently falls back
|
|
56
|
+
to English labels. If the user speaks a language `--lang` doesn't cover,
|
|
57
|
+
still write the note in their language; just leave the date labels in
|
|
58
|
+
English rather than passing a value that won't take effect.
|
|
38
59
|
|
|
39
60
|
## 2. Shape of the JSON
|
|
40
61
|
|
|
@@ -57,8 +78,12 @@ under its own `label`, never merged into a single "yesterday".
|
|
|
57
78
|
|
|
58
79
|
## 3. Write the note
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
**Write in the language the user is speaking, not the language of the JSON.**
|
|
82
|
+
The payload's labels and field names are English regardless of `--lang`
|
|
83
|
+
coverage — that is source data, not a cue for what language to write in.
|
|
84
|
+
|
|
85
|
+
Three sections: **Previous day · Today · Blockers**. Short, phrased the way
|
|
86
|
+
the user would say it out loud.
|
|
62
87
|
|
|
63
88
|
### Previous day
|
|
64
89
|
|