standup-mr 0.6.1 → 1.0.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 CHANGED
@@ -4,6 +4,52 @@ 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
+ ## [1.0.0] - 2026-10-12
8
+
9
+ First stable release. From here on the CLI commands and flags, the JSON that
10
+ `fetch` and `get_standup_data` return, the MCP tool names and arguments, and
11
+ the environment variables follow semantic versioning: anything that would
12
+ break an existing setup waits for 2.0.
13
+
14
+ ### Added
15
+
16
+ - **Remote MCP server for claude.ai.** Add
17
+ `https://standup.ilkerbalcilar.com/mcp` as a custom connector, sign in with
18
+ GitHub or gitlab.com — or paste a token for a self-hosted host — and use
19
+ standup-mr from claude.ai on the web and on mobile, with no environment
20
+ variables. One connector can hold several accounts and an optional chat
21
+ webhook. The server lives in `remote/` and is not part of the npm package.
22
+ - **Sign in to your company's GitLab or GitHub Enterprise.** On the connect
23
+ page, type the host: if someone there already set up sign-in, use it;
24
+ otherwise register an OAuth application once, following the exact redirect
25
+ URI and scopes the page shows, and everyone at that host can sign in from
26
+ then on.
27
+ - `server.json` lists the remote endpoint, so registries show both the npm
28
+ package and the hosted server.
29
+ - Both servers declare the project icon in `serverInfo.icons`.
30
+
31
+ ### Fixed
32
+
33
+ - GitLab requests send the token as `Authorization: Bearer` instead of
34
+ `PRIVATE-TOKEN`. Personal access tokens work either way, but OAuth tokens —
35
+ from `glab`'s browser login or from the remote server's sign-in — were
36
+ rejected with 401.
37
+
38
+ ## [0.7.0] - 2026-10-10
39
+
40
+ ### Added
41
+
42
+ - **Tool annotations.** Every MCP tool now declares `readOnlyHint`,
43
+ `destructiveHint`, `idempotentHint` and `openWorldHint`, so a host can tell
44
+ the read-only `get_standup_data` and `get_note_instructions` apart from
45
+ `post_standup_note`, which sends a message other people will see, and warn
46
+ before invoking it.
47
+
48
+ ### Changed
49
+
50
+ - The MCP server registers its tools with `registerTool`, which needs
51
+ `@modelcontextprotocol/sdk` 1.30.0 or later.
52
+
7
53
  ## [0.6.1] - 2026-09-02
8
54
 
9
55
  ### Fixed
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
- | | 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 |
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
- | | GitHub | GitLab |
40
- |---|---|---|
41
- | Flags | `--host` / `--token` | `--host` / `--token` |
42
- | Env | `GITHUB_HOST` / `GITHUB_TOKEN` | `GITLAB_HOST` / `GITLAB_TOKEN` |
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 —
@@ -66,12 +66,55 @@ So if you already use `gh` or `glab`, there is nothing to configure.
66
66
  npx standup-mr fetch --markdown | npx standup-mr post --slack "$SLACK_WEBHOOK_URL"
67
67
  ```
68
68
 
69
- ## The three surfaces
69
+ ## claude.ai (web and mobile)
70
+
71
+ No token, no config file — sign in instead. In claude.ai open **Settings →
72
+ Connectors → Add custom connector** and paste:
73
+
74
+ ```
75
+ https://standup.ilkerbalcilar.com/mcp
76
+ ```
77
+
78
+ Press **Connect**. A page opens where you link the accounts the note should
79
+ read:
80
+
81
+ - **GitHub** — sign in through the standup-mr GitHub App. It only sees the
82
+ repositories it is installed on, so install it on your org too; if the org
83
+ won't approve it, add github.com with a fine-grained token instead.
84
+ - **gitlab.com** — sign in with GitLab.
85
+ - **Your company's GitLab or GitHub Enterprise** — type the host. If someone
86
+ at your company already set up sign-in, you get a "Sign in with …" button.
87
+ Otherwise set it up once: the page shows the exact redirect URI and scopes
88
+ for an OAuth application on your host, you paste its ID and secret, and
89
+ everyone at that host can sign in from then on. A read-only token works too.
90
+ - **Chat webhook** _(optional)_ — Slack, Discord or Google Chat, so Claude can
91
+ post the finished note when you ask it to.
92
+
93
+ Link as many as you like — say, GitHub for side projects and your company's
94
+ GitLab for work — then ask for "my standup" or "my work standup". With
95
+ several accounts linked, Claude asks which one you mean.
96
+
97
+ The connector then works from the claude.ai mobile app as well.
98
+
99
+ **What is stored.** Your provider tokens and webhook URL, encrypted with
100
+ AES-256-GCM, and only to read your merge requests and post your note. The
101
+ access tokens Claude holds are stored as hashes. Disconnecting the connector
102
+ deletes everything; connections unused for 30 days are deleted. The server is
103
+ the code in [`remote/`](remote/).
104
+
105
+ **When to use the local server instead.** A GitLab behind a VPN or on a
106
+ private network is out of reach of a hosted server. Run the stdio server
107
+ below on your own machine — the token never leaves it.
108
+
109
+ ## The four surfaces
70
110
 
71
111
  **CLI** — the core. Emits JSON; zero runtime dependencies.
72
112
 
73
- **MCP server** (`mcp/`) — one tool, `get_standup_data`, for Claude Desktop,
74
- Cursor, or any MCP client. See [`mcp/README.md`](mcp/README.md).
113
+ **MCP server** (`mcp/`) — three tools for Claude Desktop, Claude Code, Cursor,
114
+ or any MCP client that runs a local process. See [`mcp/README.md`](mcp/README.md).
115
+
116
+ **Remote MCP server** (`remote/`) — the same tools over HTTPS with sign-in, for
117
+ claude.ai on the web and on mobile. See [above](#claudeai-web-and-mobile).
75
118
 
76
119
  **Claude Code plugin** — the note-writing playbook, shipped as the `standup`
77
120
  skill. From inside Claude Code:
@@ -93,15 +136,15 @@ paste in the note-writing rules separately.
93
136
 
94
137
  ```json
95
138
  {
96
- "mcpServers": {
97
- "standup": {
98
- "command": "npx",
99
- "args": ["-y", "standup-mr", "mcp"],
100
- "env": {
101
- "GITHUB_TOKEN": "ghp_..."
102
- }
139
+ "mcpServers": {
140
+ "standup": {
141
+ "command": "npx",
142
+ "args": ["-y", "standup-mr", "mcp"],
143
+ "env": {
144
+ "GITHUB_TOKEN": "ghp_..."
145
+ }
146
+ }
103
147
  }
104
- }
105
148
  }
106
149
  ```
107
150
 
@@ -123,11 +166,11 @@ documentation for the current format rather than trusting this file blindly.
123
166
 
124
167
  The server exposes three tools:
125
168
 
126
- | Tool | What it does |
127
- |---|---|
128
- | `get_standup_data` | Reads the provider and returns the report as JSON. Optional `provider`, `host`, `lang`. |
169
+ | Tool | What it does |
170
+ | ----------------------- | -------------------------------------------------------------------------------------------- |
171
+ | `get_standup_data` | Reads the provider and returns the report as JSON. Optional `provider`, `host`, `lang`. |
129
172
  | `get_note_instructions` | Returns the note-writing rules, so the assistant can write the note the way the skill would. |
130
- | `post_standup_note` | Posts a finished note to a Slack, Discord or Google Chat webhook. |
173
+ | `post_standup_note` | Posts a finished note to a Slack, Discord or Google Chat webhook. |
131
174
 
132
175
  `post_standup_note` reads the webhook URL from `STANDUP_WEBHOOK_URL` and never
133
176
  takes it as an argument — anyone holding that URL can post to the channel, so
@@ -196,8 +239,8 @@ _Structured digest — not a written note._
196
239
  ## Blockers
197
240
 
198
241
  - `acme/mobile` !6 — job `quality`
199
- - `npm ERR! code E404`
200
- - `npm ERR! 404 Not Found - GET https://registry.example.com/@acme%2fui`
242
+ - `npm ERR! code E404`
243
+ - `npm ERR! 404 Not Found - GET https://registry.example.com/@acme%2fui`
201
244
  ```
202
245
 
203
246
  The last section is the point of the tool. Every other standup tool can tell you
@@ -205,6 +248,16 @@ that pipeline is red; this one opens the failed job's log and shows you the
205
248
  404 — and a 404 rather than a 403 usually means the token's scope is wrong, not
206
249
  that the package is missing.
207
250
 
251
+ ## Stability
252
+
253
+ Since 1.0.0 the CLI commands and flags, the JSON that `fetch` and
254
+ `get_standup_data` return, the MCP tool names and arguments, and the
255
+ environment variables follow [semantic versioning](https://semver.org/): a
256
+ change that would break an existing setup waits for a major release. New
257
+ fields in the JSON may appear in minor releases, so read it by name rather
258
+ than by position. The hosted server at `standup.ilkerbalcilar.com` is
259
+ updated continuously and is not tied to the npm version.
260
+
208
261
  ## Known limits
209
262
 
210
263
  - **GitHub's events feed is shallow.** It is capped at roughly 300 events over
@@ -232,7 +285,7 @@ pipe `standup fetch` into anything, or import the package, read
232
285
  returns `null` with no error.
233
286
  - `MergeRequest`, `Review` and `Blocker` carry a required `provider` field, and
234
287
  `Provider.getReviews` takes an `Identity` rather than a numeric id.
235
- - The MCP `CollectOptions.provider` is now a provider *name*; inject a
288
+ - The MCP `CollectOptions.provider` is now a provider _name_; inject a
236
289
  `Provider` instance through `providerImpl`.
237
290
 
238
291
  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: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
9
- tr: ["Pazar", "Pazartesi", "Sal\u0131", "\xC7ar\u015Famba", "Per\u015Fembe", "Cuma", "Cumartesi"]
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) return "blocked";
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((match) => match[1]);
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,14 +205,6 @@ function ghToken(host) {
186
205
  return cliCapture("gh", ["auth", "token", "--hostname", host]);
187
206
  }
188
207
 
189
- // src/notify/notify.constants.ts
190
- var PAYLOAD_FIELD = {
191
- slack: "text",
192
- discord: "content",
193
- "google-chat": "text"
194
- };
195
- var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
196
-
197
208
  // src/render/chat.ts
198
209
  var MARKDOWN_NATIVE = ["discord"];
199
210
  var FENCE = /^```/;
@@ -214,6 +225,14 @@ function toChatText(markdown, kind) {
214
225
  }).join("\n");
215
226
  }
216
227
 
228
+ // src/notify/notify.constants.ts
229
+ var PAYLOAD_FIELD = {
230
+ "slack": "text",
231
+ "discord": "content",
232
+ "google-chat": "text"
233
+ };
234
+ var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
235
+
217
236
  // src/notify/notify.ts
218
237
  async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
219
238
  const field = PAYLOAD_FIELD[kind];
@@ -228,7 +247,9 @@ async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
228
247
  body: JSON.stringify({ [field]: toChatText(text, kind) })
229
248
  });
230
249
  if (!response.ok) {
231
- throw new Error(`Webhook rejected the message: HTTP ${response.status}.`);
250
+ throw new Error(
251
+ `Webhook rejected the message: HTTP ${response.status}.`
252
+ );
232
253
  }
233
254
  }
234
255
  var WEBHOOK_HOSTS = {
@@ -279,17 +300,25 @@ function retryAfter(response) {
279
300
  const value = response.headers.get("retry-after");
280
301
  if (!value) return "";
281
302
  const seconds = Number(value);
282
- if (!Number.isFinite(seconds) || seconds <= 0) return `; retry after ${value}`;
303
+ if (!Number.isFinite(seconds) || seconds <= 0)
304
+ return `; retry after ${value}`;
283
305
  return `; retry after ${seconds}s`;
284
306
  }
285
307
  function assertUsable(response, host) {
286
308
  if (response.ok || response.status === 404) return;
287
309
  if (response.status === 403 || response.status === 429) {
288
310
  if (remaining(response) === "0") {
289
- throw new ApiError(`${host} rate limit reached${resetAt(response)}.`, response.status);
311
+ throw new ApiError(
312
+ `${host} rate limit reached${resetAt(response)}.`,
313
+ response.status
314
+ );
290
315
  }
291
316
  const retry = retryAfter(response);
292
- if (retry) throw new ApiError(`${host} rate limit reached${retry}.`, response.status);
317
+ if (retry)
318
+ throw new ApiError(
319
+ `${host} rate limit reached${retry}.`,
320
+ response.status
321
+ );
293
322
  }
294
323
  if (response.status === 401) {
295
324
  throw new ApiError(
@@ -490,8 +519,8 @@ var GitHubProvider = class {
490
519
  }
491
520
  headers() {
492
521
  return {
493
- authorization: `Bearer ${this.token}`,
494
- accept: "application/vnd.github+json",
522
+ "authorization": `Bearer ${this.token}`,
523
+ "accept": "application/vnd.github+json",
495
524
  "x-github-api-version": API_VERSION
496
525
  };
497
526
  }
@@ -543,11 +572,14 @@ var GitHubProvider = class {
543
572
  return rows;
544
573
  }
545
574
  async getLogText(path) {
546
- const first = await this.send(buildUrl(this.api, path), { redirect: "manual" });
575
+ const first = await this.send(buildUrl(this.api, path), {
576
+ redirect: "manual"
577
+ });
547
578
  if (first.status === 404) return "";
548
579
  const location = first.headers.get("location");
549
580
  if (!location) {
550
- if (first.status === 0 || first.status >= 300 && first.status < 400) return "";
581
+ if (first.status === 0 || first.status >= 300 && first.status < 400)
582
+ return "";
551
583
  assertUsable(first, this.host);
552
584
  return first.ok ? await first.text() : "";
553
585
  }
@@ -590,7 +622,9 @@ var GitHubProvider = class {
590
622
  async shapePr(item) {
591
623
  const project = repoFromUrl(item.repository_url);
592
624
  const iid = item.number;
593
- const pull = await this.getJson(`repos/${project}/pulls/${iid}`);
625
+ const pull = await this.getJson(
626
+ `repos/${project}/pulls/${iid}`
627
+ );
594
628
  if (!pull) return null;
595
629
  const sha = pull.head?.sha ?? "";
596
630
  const [checks, reviews] = await Promise.all([
@@ -598,9 +632,15 @@ var GitHubProvider = class {
598
632
  `repos/${project}/commits/${sha}/check-runs`,
599
633
  { per_page: PAGE_SIZE }
600
634
  ) : Promise.resolve(null),
601
- this.getPaged(`repos/${project}/pulls/${iid}/reviews`, {}, 2)
635
+ this.getPaged(
636
+ `repos/${project}/pulls/${iid}/reviews`,
637
+ {},
638
+ 2
639
+ )
602
640
  ]);
603
- const { pipeline, pipelineId } = normalizeChecks(checks?.check_runs ?? []);
641
+ const { pipeline, pipelineId } = normalizeChecks(
642
+ checks?.check_runs ?? []
643
+ );
604
644
  return {
605
645
  provider: "github",
606
646
  project,
@@ -639,7 +679,9 @@ var GitHubProvider = class {
639
679
  `is:pr is:open review-requested:${identity.username}`,
640
680
  SEARCH_CAP
641
681
  );
642
- const cutoff = isoDay(new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY));
682
+ const cutoff = isoDay(
683
+ new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY)
684
+ );
643
685
  const rows = await Promise.all(
644
686
  items.map(async (item) => {
645
687
  const project = repoFromUrl(item.repository_url);
@@ -705,7 +747,10 @@ var GitHubProvider = class {
705
747
  (run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
706
748
  );
707
749
  if (!failed) return null;
708
- const summary = [failed.output?.summary ?? "", failed.output?.text ?? ""].filter(Boolean).join("\n");
750
+ const summary = [
751
+ failed.output?.summary ?? "",
752
+ failed.output?.text ?? ""
753
+ ].filter(Boolean).join("\n");
709
754
  const errors = extractErrors(summary);
710
755
  if (errors.length === 0) return null;
711
756
  return {
@@ -765,7 +810,10 @@ var GitLabProvider = class {
765
810
  const response = await sendWithRetry(
766
811
  this.fetchImpl,
767
812
  url,
768
- { headers: { "PRIVATE-TOKEN": this.token } },
813
+ // Bearer, not PRIVATE-TOKEN: GitLab accepts personal access tokens
814
+ // either way, but OAuth tokens — glab's browser login, and the
815
+ // remote server's sign-in — only as a bearer.
816
+ { headers: { Authorization: `Bearer ${this.token}` } },
769
817
  this.host
770
818
  );
771
819
  assertUsable(response, this.host);
@@ -818,7 +866,9 @@ var GitLabProvider = class {
818
866
  const raw = await this.getPaged("events", {
819
867
  after: isoDay(since)
820
868
  });
821
- const ids = [...new Set(raw.map((e) => e.project_id).filter(Boolean))];
869
+ const ids = [
870
+ ...new Set(raw.map((e) => e.project_id).filter(Boolean))
871
+ ];
822
872
  const paths = new Map(
823
873
  await Promise.all(
824
874
  ids.map(async (id) => [id, await this.projectPath(id)])
@@ -829,7 +879,7 @@ var GitLabProvider = class {
829
879
  return {
830
880
  at: String(event.created_at).slice(0, 16),
831
881
  action: event.action_name,
832
- project: paths.get(event.project_id) ?? "",
882
+ project: paths.get(event.project_id ?? -1) ?? "",
833
883
  targetType: event.target_type ?? "",
834
884
  title: event.target_title ?? "",
835
885
  branch: push.ref ?? "",
@@ -841,7 +891,9 @@ var GitLabProvider = class {
841
891
  async countUnresolved(projectId, iid) {
842
892
  const discussions = await this.getJson(
843
893
  `projects/${projectId}/merge_requests/${iid}/discussions`,
844
- { per_page: PAGE_SIZE2 }
894
+ {
895
+ per_page: PAGE_SIZE2
896
+ }
845
897
  );
846
898
  if (!discussions) return 0;
847
899
  return discussions.filter((discussion) => {
@@ -894,7 +946,9 @@ var GitLabProvider = class {
894
946
  async approvedByMe(projectId, iid, uid) {
895
947
  const approvals = await this.getJson(`projects/${projectId}/merge_requests/${iid}/approvals`);
896
948
  if (!approvals) return false;
897
- return (approvals.approved_by ?? []).some((entry) => entry.user?.id === uid);
949
+ return (approvals.approved_by ?? []).some(
950
+ (entry) => entry.user?.id === uid
951
+ );
898
952
  }
899
953
  async getReviews(identity, today) {
900
954
  const raw = await this.getPaged("merge_requests", {
@@ -902,7 +956,9 @@ var GitLabProvider = class {
902
956
  state: "opened",
903
957
  reviewer_id: identity.id
904
958
  });
905
- const cutoff = isoDay(new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY));
959
+ const cutoff = isoDay(
960
+ new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY)
961
+ );
906
962
  const rows = await Promise.all(
907
963
  raw.map(async (mr) => ({
908
964
  provider: "gitlab",
@@ -914,7 +970,11 @@ var GitLabProvider = class {
914
970
  draft: mr.draft,
915
971
  url: mr.web_url,
916
972
  fresh: String(mr.updated_at).slice(0, 10) >= cutoff,
917
- approvedByMe: await this.approvedByMe(mr.project_id, mr.iid, identity.id)
973
+ approvedByMe: await this.approvedByMe(
974
+ mr.project_id,
975
+ mr.iid,
976
+ identity.id
977
+ )
918
978
  }))
919
979
  );
920
980
  return rows.sort((a, b) => b.updated.localeCompare(a.updated));
@@ -926,7 +986,9 @@ var GitLabProvider = class {
926
986
  ) ?? [];
927
987
  const job = jobs.find((j) => j.status === "failed");
928
988
  if (!job) return null;
929
- const trace = await this.getText(`projects/${mr.projectId}/jobs/${job.id}/trace`);
989
+ const trace = await this.getText(
990
+ `projects/${mr.projectId}/jobs/${job.id}/trace`
991
+ );
930
992
  return {
931
993
  provider: "gitlab",
932
994
  project: mr.project,
@@ -959,7 +1021,12 @@ function hostFrom(cliHost, envHost, probe, fallback, labels) {
959
1021
  if (cliHost) return cliHost;
960
1022
  if (envHost) return envHost;
961
1023
  const found = probe();
962
- return resolveHost(void 0, void 0, found.length > 0 ? found : fallback, labels);
1024
+ return resolveHost(
1025
+ void 0,
1026
+ void 0,
1027
+ found.length > 0 ? found : fallback,
1028
+ labels
1029
+ );
963
1030
  }
964
1031
  function chooseKind(options = {}) {
965
1032
  const env = options.env ?? process.env;
@@ -1012,7 +1079,13 @@ function connect(options = {}) {
1012
1079
  const env = options.env ?? process.env;
1013
1080
  const probe = options.probe ?? DEFAULT_PROBE;
1014
1081
  if (chooseKind(options) === "github") {
1015
- const host2 = hostFrom(options.host, env.GITHUB_HOST, probe.github, [DOT_COM], GITHUB_LABELS);
1082
+ const host2 = hostFrom(
1083
+ options.host,
1084
+ env.GITHUB_HOST,
1085
+ probe.github,
1086
+ [DOT_COM],
1087
+ GITHUB_LABELS
1088
+ );
1016
1089
  const token2 = resolveToken(
1017
1090
  host2,
1018
1091
  options.token,
@@ -1022,7 +1095,13 @@ function connect(options = {}) {
1022
1095
  );
1023
1096
  return new GitHubProvider(host2, token2);
1024
1097
  }
1025
- const host = hostFrom(options.host, env.GITLAB_HOST, probe.gitlab, [], GITLAB_LABELS);
1098
+ const host = hostFrom(
1099
+ options.host,
1100
+ env.GITLAB_HOST,
1101
+ probe.gitlab,
1102
+ [],
1103
+ GITLAB_LABELS
1104
+ );
1026
1105
  const token = resolveToken(
1027
1106
  host,
1028
1107
  options.token,
@@ -1086,10 +1165,18 @@ function eventLine(event) {
1086
1165
  }
1087
1166
  function toMarkdown(report, lang = "en") {
1088
1167
  const base = STRINGS[lang] ?? STRINGS.en;
1089
- const t = { ...base, ...PROVIDER_STRINGS[report.provider][lang] ?? {} };
1168
+ const t = {
1169
+ ...base,
1170
+ ...PROVIDER_STRINGS[report.provider][lang] ?? {}
1171
+ };
1090
1172
  const ref = REF_PREFIX[report.provider];
1091
1173
  const out = [];
1092
- out.push(`# ${report.today.label} \u2014 ${report.user}`, "", `_${t.digest}_`, "");
1174
+ out.push(
1175
+ `# ${report.today.label} \u2014 ${report.user}`,
1176
+ "",
1177
+ `_${t.digest}_`,
1178
+ ""
1179
+ );
1093
1180
  if (report.previousDays.length === 0) {
1094
1181
  out.push(`## ${t.previous}`, "", `_${t.nothing}_`, "");
1095
1182
  }
@@ -1116,7 +1203,10 @@ function toMarkdown(report, lang = "en") {
1116
1203
  }
1117
1204
  const pending = report.reviews.filter((r) => !r.approvedByMe);
1118
1205
  if (pending.length > 0) {
1119
- out.push(`## ${t.reviews} (${report.reviewPendingCount} ${t.pending})`, "");
1206
+ out.push(
1207
+ `## ${t.reviews} (${report.reviewPendingCount} ${t.pending})`,
1208
+ ""
1209
+ );
1120
1210
  for (const review of pending) {
1121
1211
  out.push(
1122
1212
  `- \`${review.project}\` ${ref}${review.iid} ${review.title} \u2014 ${review.author}`
@@ -1127,7 +1217,9 @@ function toMarkdown(report, lang = "en") {
1127
1217
  if (report.blockers.length > 0) {
1128
1218
  out.push(`## ${t.blockers}`, "");
1129
1219
  for (const blocker of report.blockers) {
1130
- out.push(`- \`${blocker.project}\` ${ref}${blocker.mr} \u2014 job \`${blocker.job}\``);
1220
+ out.push(
1221
+ `- \`${blocker.project}\` ${ref}${blocker.mr} \u2014 job \`${blocker.job}\``
1222
+ );
1131
1223
  out.push(...blocker.errors.map((line) => ` - \`${line}\``));
1132
1224
  }
1133
1225
  out.push("");
@@ -1195,21 +1287,29 @@ import { dirname as dirname2, join as join2 } from "path";
1195
1287
  import { fileURLToPath as fileURLToPath2 } from "url";
1196
1288
  function readStandupSkillBody() {
1197
1289
  const moduleDir = dirname2(fileURLToPath2(import.meta.url));
1198
- const skillPath = join2(findPackageRoot(moduleDir), "skills", "standup", "SKILL.md");
1290
+ const skillPath = join2(
1291
+ findPackageRoot(moduleDir),
1292
+ "skills",
1293
+ "standup",
1294
+ "SKILL.md"
1295
+ );
1199
1296
  if (!existsSync2(skillPath)) {
1200
1297
  throw new Error(`Cannot find the standup skill file at ${skillPath}`);
1201
1298
  }
1202
1299
  const content = readFileSync2(skillPath, "utf8");
1203
- const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
1300
+ const withoutFrontmatter = content.replace(
1301
+ /^---\r?\n[\s\S]*?\r?\n---\r?\n/,
1302
+ ""
1303
+ );
1204
1304
  return withoutFrontmatter.replace(/^\n+/, "");
1205
1305
  }
1206
1306
 
1207
1307
  export {
1208
- STALE_DAYS,
1209
1308
  isoDay,
1210
1309
  localAt,
1211
1310
  label,
1212
1311
  previousActiveDays,
1312
+ STALE_DAYS,
1213
1313
  classify,
1214
1314
  markMissingPipelines,
1215
1315
  ConfigError,
@@ -1223,8 +1323,8 @@ export {
1223
1323
  glabToken,
1224
1324
  ghHosts,
1225
1325
  ghToken,
1226
- WEBHOOK_KINDS,
1227
1326
  toChatText,
1327
+ WEBHOOK_KINDS,
1228
1328
  postWebhook,
1229
1329
  inferWebhookKind,
1230
1330
  buildUrl,