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.
@@ -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(readFileSync(join(root, "package.json"), "utf8"));
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(`Webhook rejected the message: HTTP ${response.status}.`);
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((match) => match[1]);
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: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
173
- tr: ["Pazar", "Pazartesi", "Sal\u0131", "\xC7ar\u015Famba", "Per\u015Fembe", "Cuma", "Cumartesi"]
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) return "blocked";
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) return `; retry after ${value}`;
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(`${host} rate limit reached${resetAt(response)}.`, response.status);
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) throw new ApiError(`${host} rate limit reached${retry}.`, response.status);
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), { redirect: "manual" });
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) return "";
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(`repos/${project}/pulls/${iid}`);
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(`repos/${project}/pulls/${iid}/reviews`, {}, 2)
667
+ this.getPaged(
668
+ `repos/${project}/pulls/${iid}/reviews`,
669
+ {},
670
+ 2
671
+ )
612
672
  ]);
613
- const { pipeline, pipelineId } = normalizeChecks(checks?.check_runs ?? []);
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(new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY));
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 = [failed.output?.summary ?? "", failed.output?.text ?? ""].filter(Boolean).join("\n");
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 = [...new Set(raw.map((e) => e.project_id).filter(Boolean))];
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
- { per_page: PAGE_SIZE2 }
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((entry2) => entry2.user?.id === uid);
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(new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY));
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(mr.project_id, mr.iid, identity.id)
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(`projects/${mr.projectId}/jobs/${job.id}/trace`);
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(void 0, void 0, found.length > 0 ? found : fallback, labels);
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(options.host, env.GITHUB_HOST, probe.github, [DOT_COM], GITHUB_LABELS);
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(options.host, env.GITLAB_HOST, probe.gitlab, [], GITLAB_LABELS);
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({ provider: options.provider, host: options.host, token: options.token });
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 { content: [{ type: "text", text: JSON.stringify(report) }] };
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
- async function main() {
1169
- const server = new McpServer({ name: "standup-mr", version: packageVersion(import.meta.url) });
1170
- server.tool(
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
- STANDUP_TOOL_DESCRIPTION,
1173
- STANDUP_TOOL_SCHEMA,
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.tool(
1299
+ server.registerTool(
1177
1300
  "post_standup_note",
1178
- POST_TOOL_DESCRIPTION,
1179
- POST_TOOL_SCHEMA,
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.tool(
1313
+ server.registerTool(
1183
1314
  "get_note_instructions",
1184
- INSTRUCTIONS_TOOL_DESCRIPTION,
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
- await server.connect(new StdioServerTransport());
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(`${error instanceof Error ? error.message : String(error)}
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
- "mcpServers": {
22
- "standup": {
23
- "command": "npx",
24
- "args": ["-y", "standup-mr", "mcp"],
25
- "env": {}
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 | 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 |
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
- "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
- }
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
- "mcpServers": {
30
- "standup": {
31
- "command": "npx",
32
- "args": ["-y", "standup-mr", "mcp"],
33
- "env": {
34
- "GITHUB_HOST": "github.com",
35
- "GITHUB_TOKEN": "ghp_..."
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
- "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
- }
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.6.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.0.0",
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
  }