standup-mr 0.5.0 → 0.6.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 +33 -0
- package/README.md +15 -1
- package/dist/{chunk-S2PFIUNI.js → chunk-NJO6LFYN.js} +30 -2
- package/dist/cli.js +14 -8
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/mcp/server.js +31 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ 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.6.1] - 2026-09-02
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- A note posted to Google Chat or Slack arrived with its formatting showing:
|
|
12
|
+
`**bold**` as literal asterisks, `## Dün` as a literal heading marker.
|
|
13
|
+
Neither renders standard Markdown — both use a single asterisk for bold and
|
|
14
|
+
have no headings — while the note is Markdown throughout. Bold is now
|
|
15
|
+
converted and headings become bold lines for those two. Discord speaks
|
|
16
|
+
Markdown natively and is sent untouched, and fenced blocks pass through
|
|
17
|
+
verbatim so a code sample keeps its asterisks.
|
|
18
|
+
|
|
19
|
+
## [0.6.0] - 2026-09-02
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Google Chat webhooks.** `--google-chat URL` on the CLI, `kind:
|
|
24
|
+
"google-chat"` in `post_standup_note`, and `chat.googleapis.com` added to
|
|
25
|
+
host inference so neither is usually needed. Google Chat takes the same
|
|
26
|
+
`{"text"}` body as Slack, so this was only ever a naming gap — but telling
|
|
27
|
+
someone to pass `--slack` for a Google Chat webhook is a hack, not a
|
|
28
|
+
feature.
|
|
29
|
+
|
|
30
|
+
Anything else that accepts a Slack-shaped body — Mattermost, Rocket.Chat, an
|
|
31
|
+
n8n or Zapier endpoint — already works by passing the slack kind.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `standup post` builds its flags and its error message from the list of
|
|
36
|
+
implemented channels instead of a hand-written pair, so adding a channel is
|
|
37
|
+
one map entry. The MCP tool's `kind` enum is now tested against that same
|
|
38
|
+
list: a channel can no longer reach the CLI and quietly miss MCP.
|
|
39
|
+
|
|
7
40
|
## [0.5.0] - 2026-09-02
|
|
8
41
|
|
|
9
42
|
### Added
|
package/README.md
CHANGED
|
@@ -30,6 +30,10 @@ npx standup-mr fetch --markdown # structured digest
|
|
|
30
30
|
npx standup-mr fetch --lang tr # Turkish date labels
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
```bash
|
|
34
|
+
npx standup-mr fetch --markdown | npx standup-mr post --google-chat "$URL"
|
|
35
|
+
```
|
|
36
|
+
|
|
33
37
|
### Identity
|
|
34
38
|
|
|
35
39
|
| | GitHub | GitLab |
|
|
@@ -123,13 +127,23 @@ The server exposes three tools:
|
|
|
123
127
|
|---|---|
|
|
124
128
|
| `get_standup_data` | Reads the provider and returns the report as JSON. Optional `provider`, `host`, `lang`. |
|
|
125
129
|
| `get_note_instructions` | Returns the note-writing rules, so the assistant can write the note the way the skill would. |
|
|
126
|
-
| `post_standup_note` | Posts a finished note to a Slack or
|
|
130
|
+
| `post_standup_note` | Posts a finished note to a Slack, Discord or Google Chat webhook. |
|
|
127
131
|
|
|
128
132
|
`post_standup_note` reads the webhook URL from `STANDUP_WEBHOOK_URL` and never
|
|
129
133
|
takes it as an argument — anyone holding that URL can post to the channel, so
|
|
130
134
|
it belongs with the tokens, not in a transcript. The payload shape is inferred
|
|
131
135
|
from the URL host; `kind` is only needed when a proxy hides it.
|
|
132
136
|
|
|
137
|
+
Three shapes are implemented: `slack` and `google-chat` both post `{"text"}`,
|
|
138
|
+
`discord` posts `{"content"}`.
|
|
139
|
+
|
|
140
|
+
Slack and Google Chat do not render standard Markdown, so the note is rewritten
|
|
141
|
+
on the way out: `**bold**` becomes `*bold*` and a `##` heading becomes a bold
|
|
142
|
+
line. Inline code, fenced blocks and their contents are left alone. Discord
|
|
143
|
+
speaks Markdown natively and is sent untouched. Anything else that accepts a Slack-shaped body —
|
|
144
|
+
Mattermost, Rocket.Chat, an n8n or Zapier endpoint — works today by passing
|
|
145
|
+
`kind: "slack"`, or `--slack URL` on the CLI.
|
|
146
|
+
|
|
133
147
|
Outside MCP, the same rules are available on stdout:
|
|
134
148
|
|
|
135
149
|
```bash
|
|
@@ -187,7 +187,32 @@ function ghToken(host) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// src/notify/notify.constants.ts
|
|
190
|
-
var PAYLOAD_FIELD = {
|
|
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
|
+
// src/render/chat.ts
|
|
198
|
+
var MARKDOWN_NATIVE = ["discord"];
|
|
199
|
+
var FENCE = /^```/;
|
|
200
|
+
function convertLine(line) {
|
|
201
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
202
|
+
if (heading) return `*${heading[1]}*`;
|
|
203
|
+
return line.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "*$1*");
|
|
204
|
+
}
|
|
205
|
+
function toChatText(markdown, kind) {
|
|
206
|
+
if (MARKDOWN_NATIVE.includes(kind)) return markdown;
|
|
207
|
+
let fenced = false;
|
|
208
|
+
return markdown.split("\n").map((line) => {
|
|
209
|
+
if (FENCE.test(line)) {
|
|
210
|
+
fenced = !fenced;
|
|
211
|
+
return line;
|
|
212
|
+
}
|
|
213
|
+
return fenced ? line : convertLine(line);
|
|
214
|
+
}).join("\n");
|
|
215
|
+
}
|
|
191
216
|
|
|
192
217
|
// src/notify/notify.ts
|
|
193
218
|
async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
|
|
@@ -200,7 +225,7 @@ async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
|
|
|
200
225
|
const response = await fetchImpl(url, {
|
|
201
226
|
method: "POST",
|
|
202
227
|
headers: { "Content-Type": "application/json" },
|
|
203
|
-
body: JSON.stringify({ [field]: text })
|
|
228
|
+
body: JSON.stringify({ [field]: toChatText(text, kind) })
|
|
204
229
|
});
|
|
205
230
|
if (!response.ok) {
|
|
206
231
|
throw new Error(`Webhook rejected the message: HTTP ${response.status}.`);
|
|
@@ -208,6 +233,7 @@ async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
|
|
|
208
233
|
}
|
|
209
234
|
var WEBHOOK_HOSTS = {
|
|
210
235
|
"hooks.slack.com": "slack",
|
|
236
|
+
"chat.googleapis.com": "google-chat",
|
|
211
237
|
"discord.com": "discord",
|
|
212
238
|
"discordapp.com": "discord",
|
|
213
239
|
"ptb.discord.com": "discord",
|
|
@@ -1197,6 +1223,8 @@ export {
|
|
|
1197
1223
|
glabToken,
|
|
1198
1224
|
ghHosts,
|
|
1199
1225
|
ghToken,
|
|
1226
|
+
WEBHOOK_KINDS,
|
|
1227
|
+
toChatText,
|
|
1200
1228
|
postWebhook,
|
|
1201
1229
|
inferWebhookKind,
|
|
1202
1230
|
buildUrl,
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConfigError,
|
|
4
|
+
WEBHOOK_KINDS,
|
|
4
5
|
buildReport,
|
|
5
6
|
connect,
|
|
6
7
|
findPackageRoot,
|
|
7
8
|
postWebhook,
|
|
8
9
|
readStandupSkillBody,
|
|
9
10
|
toMarkdown
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-NJO6LFYN.js";
|
|
11
12
|
|
|
12
13
|
// src/cli/cli.ts
|
|
13
14
|
import { realpathSync } from "fs";
|
|
@@ -21,7 +22,7 @@ var USAGE = `standup \u2014 standup notes from merge request state
|
|
|
21
22
|
Usage:
|
|
22
23
|
standup fetch [--provider github|gitlab] [--host H] [--token T]
|
|
23
24
|
[--lang en|tr] [--markdown]
|
|
24
|
-
standup post (--slack URL | --discord URL) [--text TEXT]
|
|
25
|
+
standup post (--slack URL | --discord URL | --google-chat URL) [--text TEXT]
|
|
25
26
|
standup mcp
|
|
26
27
|
standup instructions
|
|
27
28
|
|
|
@@ -73,22 +74,27 @@ async function main(argv) {
|
|
|
73
74
|
const { values } = parseArgs({
|
|
74
75
|
args: rest,
|
|
75
76
|
options: {
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
...Object.fromEntries(
|
|
78
|
+
WEBHOOK_KINDS.map((kind2) => [kind2, { type: "string" }])
|
|
79
|
+
),
|
|
78
80
|
text: { type: "string", default: "-" }
|
|
79
81
|
}
|
|
80
82
|
});
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const urls = values;
|
|
84
|
+
const kind = WEBHOOK_KINDS.find((candidate) => urls[candidate]);
|
|
85
|
+
if (!kind) {
|
|
86
|
+
const flags = WEBHOOK_KINDS.map((candidate) => `--${candidate} URL`).join(" | ");
|
|
87
|
+
process.stderr.write(`Pass one of: ${flags}.
|
|
88
|
+
`);
|
|
84
89
|
return 1;
|
|
85
90
|
}
|
|
91
|
+
const url = urls[kind];
|
|
86
92
|
const text = values.text === "-" ? await readStdin() : values.text;
|
|
87
93
|
if (!text.trim()) {
|
|
88
94
|
process.stderr.write("Nothing to post: empty text.\n");
|
|
89
95
|
return 1;
|
|
90
96
|
}
|
|
91
|
-
await postWebhook(url, text,
|
|
97
|
+
await postWebhook(url, text, kind);
|
|
92
98
|
return 0;
|
|
93
99
|
}
|
|
94
100
|
if (command === "mcp") {
|
package/dist/index.d.ts
CHANGED
|
@@ -125,6 +125,7 @@ declare function previousActiveDays(eventDates: Set<string>, today: Date): Array
|
|
|
125
125
|
declare const PAYLOAD_FIELD: {
|
|
126
126
|
readonly slack: "text";
|
|
127
127
|
readonly discord: "content";
|
|
128
|
+
readonly 'google-chat': "text";
|
|
128
129
|
};
|
|
129
130
|
|
|
130
131
|
type WebhookKind = keyof typeof PAYLOAD_FIELD;
|
|
@@ -276,6 +277,8 @@ interface SelectOptions {
|
|
|
276
277
|
declare function chooseKind(options?: SelectOptions): ProviderKind;
|
|
277
278
|
declare function connect(options?: SelectOptions): Provider;
|
|
278
279
|
|
|
280
|
+
declare function toChatText(markdown: string, kind: WebhookKind): string;
|
|
281
|
+
|
|
279
282
|
declare function toMarkdown(report: StandupReport, lang?: string): string;
|
|
280
283
|
|
|
281
284
|
declare function buildReport(provider: Provider, today: Date, lang?: string, lookbackDays?: number): Promise<StandupReport>;
|
|
@@ -284,4 +287,4 @@ declare function readStandupSkillBody(): string;
|
|
|
284
287
|
|
|
285
288
|
declare function extractErrors(rawTrace: string, limit?: number): string[];
|
|
286
289
|
|
|
287
|
-
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, inferWebhookKind, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, readStandupSkillBody, repoFromUrl, resolveHost, resolveToken, sendWithRetry, toMarkdown, undiagnosed, unreachable };
|
|
290
|
+
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, inferWebhookKind, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, readStandupSkillBody, repoFromUrl, resolveHost, resolveToken, sendWithRetry, toChatText, toMarkdown, undiagnosed, unreachable };
|
package/dist/index.js
CHANGED
|
@@ -41,10 +41,11 @@ import {
|
|
|
41
41
|
resolveHost,
|
|
42
42
|
resolveToken,
|
|
43
43
|
sendWithRetry,
|
|
44
|
+
toChatText,
|
|
44
45
|
toMarkdown,
|
|
45
46
|
undiagnosed,
|
|
46
47
|
unreachable
|
|
47
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-NJO6LFYN.js";
|
|
48
49
|
export {
|
|
49
50
|
ApiError,
|
|
50
51
|
ConfigError,
|
|
@@ -87,6 +88,7 @@ export {
|
|
|
87
88
|
resolveHost,
|
|
88
89
|
resolveToken,
|
|
89
90
|
sendWithRetry,
|
|
91
|
+
toChatText,
|
|
90
92
|
toMarkdown,
|
|
91
93
|
undiagnosed,
|
|
92
94
|
unreachable
|
package/dist/mcp/server.js
CHANGED
|
@@ -32,7 +32,32 @@ function packageVersion(moduleUrl) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/notify/notify.constants.ts
|
|
35
|
-
var PAYLOAD_FIELD = {
|
|
35
|
+
var PAYLOAD_FIELD = {
|
|
36
|
+
slack: "text",
|
|
37
|
+
discord: "content",
|
|
38
|
+
"google-chat": "text"
|
|
39
|
+
};
|
|
40
|
+
var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
|
|
41
|
+
|
|
42
|
+
// src/render/chat.ts
|
|
43
|
+
var MARKDOWN_NATIVE = ["discord"];
|
|
44
|
+
var FENCE = /^```/;
|
|
45
|
+
function convertLine(line) {
|
|
46
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
47
|
+
if (heading) return `*${heading[1]}*`;
|
|
48
|
+
return line.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "*$1*");
|
|
49
|
+
}
|
|
50
|
+
function toChatText(markdown, kind) {
|
|
51
|
+
if (MARKDOWN_NATIVE.includes(kind)) return markdown;
|
|
52
|
+
let fenced = false;
|
|
53
|
+
return markdown.split("\n").map((line) => {
|
|
54
|
+
if (FENCE.test(line)) {
|
|
55
|
+
fenced = !fenced;
|
|
56
|
+
return line;
|
|
57
|
+
}
|
|
58
|
+
return fenced ? line : convertLine(line);
|
|
59
|
+
}).join("\n");
|
|
60
|
+
}
|
|
36
61
|
|
|
37
62
|
// src/notify/notify.ts
|
|
38
63
|
async function postWebhook(url, text2, kind = "slack", fetchImpl = fetch) {
|
|
@@ -45,7 +70,7 @@ async function postWebhook(url, text2, kind = "slack", fetchImpl = fetch) {
|
|
|
45
70
|
const response = await fetchImpl(url, {
|
|
46
71
|
method: "POST",
|
|
47
72
|
headers: { "Content-Type": "application/json" },
|
|
48
|
-
body: JSON.stringify({ [field]: text2 })
|
|
73
|
+
body: JSON.stringify({ [field]: toChatText(text2, kind) })
|
|
49
74
|
});
|
|
50
75
|
if (!response.ok) {
|
|
51
76
|
throw new Error(`Webhook rejected the message: HTTP ${response.status}.`);
|
|
@@ -53,6 +78,7 @@ async function postWebhook(url, text2, kind = "slack", fetchImpl = fetch) {
|
|
|
53
78
|
}
|
|
54
79
|
var WEBHOOK_HOSTS = {
|
|
55
80
|
"hooks.slack.com": "slack",
|
|
81
|
+
"chat.googleapis.com": "google-chat",
|
|
56
82
|
"discord.com": "discord",
|
|
57
83
|
"discordapp.com": "discord",
|
|
58
84
|
"ptb.discord.com": "discord",
|
|
@@ -1117,13 +1143,13 @@ async function runStandupTool(args, collector = collect) {
|
|
|
1117
1143
|
var WEBHOOK_URL_ENV = "STANDUP_WEBHOOK_URL";
|
|
1118
1144
|
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.
|
|
1119
1145
|
|
|
1120
|
-
The webhook URL is read from ${WEBHOOK_URL_ENV}, never from an argument \u2014 a webhook URL is a credential, since anyone holding it can post to the channel. Slack and Discord payload shapes are supported; the shape is inferred from the URL host, and \`kind\` is only needed for a proxied or self-hosted endpoint whose host gives nothing away. A missing URL, an unrecognised host, or a webhook that rejects the message comes back as an error rather than a silent success.`;
|
|
1146
|
+
The webhook URL is read from ${WEBHOOK_URL_ENV}, never from an argument \u2014 a webhook URL is a credential, since anyone holding it can post to the channel. Slack and Discord payload shapes are supported; the shape is inferred from the URL host, and \`kind\` is only needed for a proxied or self-hosted endpoint whose host gives nothing away \u2014 Mattermost and Rocket.Chat take the slack shape. A missing URL, an unrecognised host, or a webhook that rejects the message comes back as an error rather than a silent success.`;
|
|
1121
1147
|
var POST_TOOL_SCHEMA = {
|
|
1122
1148
|
text: z.string().min(1).describe(
|
|
1123
1149
|
"The note to post, as the chat should render it. Slack and Discord both take Markdown, so send the written note rather than the raw JSON from get_standup_data."
|
|
1124
1150
|
),
|
|
1125
|
-
kind: z.enum(["slack", "discord"]).optional().describe(
|
|
1126
|
-
'Which payload shape to send: slack
|
|
1151
|
+
kind: z.enum(["slack", "discord", "google-chat"]).optional().describe(
|
|
1152
|
+
'Which payload shape to send: slack and google-chat post {"text"}, discord posts {"content"}. Omit it \u2014 the shape is inferred from the webhook host. Pass it only when the host is not recognisable, e.g. a proxy in front of the real webhook.'
|
|
1127
1153
|
)
|
|
1128
1154
|
};
|
|
1129
1155
|
var INSTRUCTIONS_TOOL_DESCRIPTION = "Returns the note-writing rules \u2014 the playbook for turning get_standup_data output into a standup note a person would actually say out loud: how to group the previous day by theme, how to derive today from open merge requests, and how to report a blocker from its job log. Read-only, no arguments, no network. Call it once before writing the first note; the rules do not change between calls.";
|