pm-slack 2026.6.2 → 2026.6.4-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 +31 -1
- package/README.md +198 -12
- package/dist/index.d.ts +196 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +953 -59
- package/dist/index.js.map +1 -1
- package/manifest.json +3 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2026.6.
|
|
3
|
+
## 2026.6.4-1 - 2026-06-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- preflight: fail-fast Slack webhook validation gate ([pm-slack-676l](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/features/pm-slack-676l.toon))
|
|
8
|
+
|
|
9
|
+
## 2026.06.04 - 2026-06-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Lifecycle event taxonomy + assignee mentions + action buttons ([pm-slack-c3q7](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/features/pm-slack-c3q7.toon))
|
|
14
|
+
|
|
15
|
+
## 2026.06.03 - 2026-06-02
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Deep feature enhancement 2026-06-03 ([pm-slack-l3ux](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/features/pm-slack-l3ux.toon))
|
|
20
|
+
- Digest reads pm store toon/json files directly \(no new SDK service\) ([pm-slack-06ti](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/decisions/pm-slack-06ti.toon))
|
|
21
|
+
|
|
22
|
+
### Other
|
|
23
|
+
|
|
24
|
+
- Best-effort hook policy unchanged; strict creds only in test/digest real-post ([pm-slack-b08f](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/decisions/pm-slack-b08f.toon))
|
|
25
|
+
- Routing model: PM\_SLACK\_ROUTES JSON map + per-event channel override ([pm-slack-i3ir](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/decisions/pm-slack-i3ir.toon))
|
|
26
|
+
- Unit tests + README + functional test + release 2026.6.3 ([pm-slack-ffs4](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/tasks/pm-slack-ffs4.toon))
|
|
27
|
+
- Strict missing-creds CommandError for real posts; keep best-effort hook ([pm-slack-7fk4](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/tasks/pm-slack-7fk4.toon))
|
|
28
|
+
- slack digest command: activity summary over --since/--days ([pm-slack-nga4](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/tasks/pm-slack-nga4.toon))
|
|
29
|
+
- slack test command: offline preview, no network, --json ([pm-slack-36xw](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/tasks/pm-slack-36xw.toon))
|
|
30
|
+
- Event/type routing to multiple webhooks/channels \(PM\_SLACK\_ROUTES\) ([pm-slack-747x](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/tasks/pm-slack-747x.toon))
|
|
31
|
+
- Format toggle: --format text\|blockkit for hook + commands ([pm-slack-1fbz](https://github.com/unbraind/pm-slack/blob/main/.agents/pm/tasks/pm-slack-1fbz.toon))
|
|
32
|
+
|
|
33
|
+
## 2026.06.02 - 2026-06-02
|
|
4
34
|
|
|
5
35
|
### Added
|
|
6
36
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Slack notifications for [pm-cli](https://github.com/unbraind/pm-cli) item lifecycle events.
|
|
4
4
|
|
|
5
|
-
Fires after `create`, `close`, and `
|
|
5
|
+
Fires after `create`, `close`, `block`, `cancel`, `open`, `start`, `unblock`, and `reopen` lifecycle transitions and posts a formatted message to a Slack incoming webhook. Optionally maps assignees to Slack `@mentions` and adds Block Kit action buttons linking to the item / GitHub URL.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -33,10 +33,14 @@ pm install github.com/unbraind/pm-slack --project
|
|
|
33
33
|
|
|
34
34
|
| Variable | Required | Default | Description |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
-
| `PM_SLACK_WEBHOOK` |
|
|
37
|
-
| `PM_SLACK_CHANNEL` | No | — |
|
|
36
|
+
| `PM_SLACK_WEBHOOK` | Usually | — | Slack incoming webhook URL (optional if every routing rule carries its own `webhook`) |
|
|
37
|
+
| `PM_SLACK_CHANNEL` | No | — | Default channel hint appended to messages (e.g. `#pm-alerts`) |
|
|
38
38
|
| `PM_SLACK_MIN_PRIORITY` | No | `1` | Minimum priority to notify (1=critical, 2=high, 3=medium, 4=low) |
|
|
39
|
-
| `PM_SLACK_EVENTS` | No |
|
|
39
|
+
| `PM_SLACK_EVENTS` | No | all events | Comma-separated list of events to notify on. Status-name aliases (`canceled`, `in_progress`, …) accepted |
|
|
40
|
+
| `PM_SLACK_FORMAT` | No | `blockkit` | Default message format: `blockkit` (rich) or `text` (plain mrkdwn) |
|
|
41
|
+
| `PM_SLACK_ROUTES` | No | — | JSON array of routing rules (see [Routing](#routing-by-event-type-or-status)) |
|
|
42
|
+
| `PM_SLACK_ASSIGNEE_MAP` | No | — | Comma list of `name=slackId` pairs mapping assignees to Slack `@mentions`, e.g. `alice=U123,bob=U456` |
|
|
43
|
+
| `PM_SLACK_MENTION_ASSIGNEE` | No | auto | Set `0`/`false` to disable assignee mentions even when a map is set (auto-enabled when `PM_SLACK_ASSIGNEE_MAP` is non-empty) |
|
|
40
44
|
|
|
41
45
|
Export them in your shell profile or `.env`:
|
|
42
46
|
|
|
@@ -45,6 +49,7 @@ export PM_SLACK_WEBHOOK="<slack-webhook-url>"
|
|
|
45
49
|
export PM_SLACK_CHANNEL="#pm-alerts"
|
|
46
50
|
export PM_SLACK_MIN_PRIORITY=2 # only critical + high
|
|
47
51
|
export PM_SLACK_EVENTS="create,close" # skip block notifications
|
|
52
|
+
export PM_SLACK_FORMAT="text" # plain text instead of Block Kit
|
|
48
53
|
```
|
|
49
54
|
|
|
50
55
|
---
|
|
@@ -55,7 +60,17 @@ export PM_SLACK_EVENTS="create,close" # skip block notifications
|
|
|
55
60
|
|---|---|---|
|
|
56
61
|
| `create` | `pm add`, `pm create`, `pm new` | Fires when a new item is created |
|
|
57
62
|
| `close` | `pm close`, `pm done`, `pm complete` | Fires when an item is closed/resolved |
|
|
58
|
-
| `block` | `pm update --status blocked
|
|
63
|
+
| `block` | `pm update --status blocked` | Fires when an item's status changes to blocked |
|
|
64
|
+
| `cancel` | `pm update --status canceled` | Fires when an item is canceled (reason read from `close_reason`) |
|
|
65
|
+
| `open` | `pm update --status open` (e.g. `draft → open`) | Fires when an item is published/opened |
|
|
66
|
+
| `start` | `pm claim`, `pm start-task`, `pm update --status in_progress` | Fires when work begins on an item |
|
|
67
|
+
| `unblock` | `pm update --status open` from a `blocked` state | Distinguished when the result carries the prior status |
|
|
68
|
+
| `reopen` | `pm update --status open` from a `closed`/`canceled` state | Distinguished when the result carries the prior status |
|
|
69
|
+
|
|
70
|
+
`unblock` / `reopen` are refinements of an `open` transition: when pm's hook
|
|
71
|
+
result exposes the previous status, the more specific event is used; otherwise
|
|
72
|
+
the transition surfaces as `open`. Status-name aliases (`canceled`,
|
|
73
|
+
`in_progress`, `reopened`, …) are accepted anywhere an event name is.
|
|
59
74
|
|
|
60
75
|
---
|
|
61
76
|
|
|
@@ -115,12 +130,184 @@ PM_SLACK_EVENTS="create,close"
|
|
|
115
130
|
|
|
116
131
|
---
|
|
117
132
|
|
|
133
|
+
## Message Formats
|
|
134
|
+
|
|
135
|
+
Notifications render as rich Slack **Block Kit** by default (header, a fields
|
|
136
|
+
grid with item id / type / event / priority / status / author, an optional
|
|
137
|
+
reason section, and a context footer). Set the format to plain `text` for
|
|
138
|
+
minimal/legacy channels:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
export PM_SLACK_FORMAT="text" # plain mrkdwn, no blocks
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Per-command, the `--format blockkit|text` flag overrides the env var for
|
|
145
|
+
`slack notify`, `slack test`, and `slack digest`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Routing (by event, type, or status)
|
|
150
|
+
|
|
151
|
+
`PM_SLACK_ROUTES` is an optional JSON array of rules that send specific events,
|
|
152
|
+
item types, or statuses to a different webhook and/or channel. Routing is purely
|
|
153
|
+
additive — with no rules configured, behavior is unchanged.
|
|
154
|
+
|
|
155
|
+
Each rule is `{ "match": "<selector>", "webhook"?: "...", "channel"?: "..." }`.
|
|
156
|
+
Selectors:
|
|
157
|
+
|
|
158
|
+
| Selector | Matches |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `create` / `close` / `block` | that lifecycle event |
|
|
161
|
+
| `type:<itemType>` | items of that type (case-insensitive) |
|
|
162
|
+
| `status:<status>` | items in that status (case-insensitive) |
|
|
163
|
+
| `*` / `all` | everything (use as a catch-all) |
|
|
164
|
+
|
|
165
|
+
The **first matching rule wins**; any field a rule omits falls back to the
|
|
166
|
+
default `PM_SLACK_WEBHOOK` / `PM_SLACK_CHANNEL`.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
export PM_SLACK_ROUTES='[
|
|
170
|
+
{ "match": "block", "channel": "#urgent" },
|
|
171
|
+
{ "match": "type:Bug", "webhook": "https://hooks.slack.com/services/AAA/BBB/CCC" }
|
|
172
|
+
]'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Assignee @mentions
|
|
178
|
+
|
|
179
|
+
Map a pm item's `assignee` to a Slack user (or group) id so notifications
|
|
180
|
+
@-mention the responsible person. Set `PM_SLACK_ASSIGNEE_MAP` to a comma list of
|
|
181
|
+
`name=id` pairs:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
export PM_SLACK_ASSIGNEE_MAP="alice=U0123ABC,bob=U0456DEF,team=<!subteam^S0789GHI>"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- A raw id (`U0123ABC`) is wrapped as `<@U0123ABC>`.
|
|
188
|
+
- A pre-wrapped token (e.g. `<!subteam^S0789GHI>` for a group) is used verbatim.
|
|
189
|
+
- Mentions auto-enable in the hook whenever the map is non-empty; set
|
|
190
|
+
`PM_SLACK_MENTION_ASSIGNEE=0` to force them off.
|
|
191
|
+
- For ad-hoc posts, `pm slack notify --assignee alice --mention-assignee` resolves
|
|
192
|
+
the mention from the same map. When no mapping exists, the raw assignee name is
|
|
193
|
+
still shown (blockkit) — output is byte-identical to before when no map is set.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Action buttons (item / GitHub links)
|
|
198
|
+
|
|
199
|
+
When a pm item carries a URL, the Block Kit notification adds an action button
|
|
200
|
+
linking to it (`View on GitHub` for github.com URLs, otherwise `Open item`). The
|
|
201
|
+
URL is read from the first present of these item fields: `github_url`,
|
|
202
|
+
`html_url`, `url`, `source_url`, `link` (camelCase variants accepted). The
|
|
203
|
+
plain-`text` format is unchanged. For ad-hoc posts:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pm slack notify --title 'Deploy' --on close --url 'https://github.com/unbraind/pm-slack/pull/3' --dry-run
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Commands
|
|
212
|
+
|
|
213
|
+
### `pm slack notify`
|
|
214
|
+
|
|
215
|
+
Manually post a Block Kit (or text) message for an ad-hoc note.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pm slack notify --text 'Release shipped :rocket:' --dry-run
|
|
219
|
+
pm slack notify --title 'Deploy done' --on close --format text --channel '#releases'
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `pm slack test`
|
|
223
|
+
|
|
224
|
+
Build and **print** a sample notification in the chosen format **without
|
|
225
|
+
posting** — fully offline, no webhook required. Great for previewing formatting.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pm slack test --format blockkit # rich preview
|
|
229
|
+
pm slack test --format text --on close # plain-text preview of a close event
|
|
230
|
+
pm slack test --on block --json # machine-readable payload (with global --json)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Flags: `--format blockkit|text`, `--on create|close|block`, `--title`,
|
|
234
|
+
`--channel`, `--json`.
|
|
235
|
+
|
|
236
|
+
### `pm slack digest`
|
|
237
|
+
|
|
238
|
+
Produce a single summary of recent activity (created / closed / blocked /
|
|
239
|
+
in-progress) over a time window, as Block Kit or text. Reads the pm store
|
|
240
|
+
directly. Use `--dry-run` to preview without posting.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
pm slack digest --days 7 --dry-run
|
|
244
|
+
pm slack digest --since 2026-06-01 --format text --dry-run
|
|
245
|
+
pm slack digest --days 1 --format blockkit # real post (needs a webhook)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Flags: `--since <date>`, `--days <n>` (default 7), `--format blockkit|text`,
|
|
249
|
+
`--channel`, `--webhook`, `--dry-run`, `--json`.
|
|
250
|
+
|
|
251
|
+
A real (non-`--dry-run`) `slack digest` post without a configured webhook fails
|
|
252
|
+
with a clear error (exit 1) rather than crashing.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Webhook preflight gate
|
|
257
|
+
|
|
258
|
+
The Slack-posting commands (`slack notify`, `slack digest`) run a **fail-fast
|
|
259
|
+
preflight** that validates webhook configuration **before** anything is posted:
|
|
260
|
+
|
|
261
|
+
- The effective webhook is resolved with precedence `--webhook` flag →
|
|
262
|
+
`PM_SLACK_WEBHOOK` → a per-rule webhook in `PM_SLACK_ROUTES`.
|
|
263
|
+
- If no webhook is configured, or the configured URL is not a valid `http(s)`
|
|
264
|
+
URL, the command **aborts immediately** with a clear, actionable error and a
|
|
265
|
+
**non-zero exit** (`2`, usage error) — nothing is posted, no network call is
|
|
266
|
+
made.
|
|
267
|
+
- On a valid configuration the gate is a **silent pass-through**.
|
|
268
|
+
|
|
269
|
+
Validation is **purely syntactic/environment-based — no network call**, so it is
|
|
270
|
+
cheap and offline-safe. It does *not* verify the webhook actually works; a valid
|
|
271
|
+
but dead webhook still passes the gate and surfaces at the network layer.
|
|
272
|
+
|
|
273
|
+
What is **not** gated:
|
|
274
|
+
|
|
275
|
+
- `slack test` — an offline preview that never posts and works with no webhook.
|
|
276
|
+
- `--dry-run` on `slack notify` / `slack digest` — also an offline preview.
|
|
277
|
+
|
|
278
|
+
The gate is registered both as a scoped `registerPreflight` override (for the
|
|
279
|
+
documented `preflight` capability and a visible warning) and — because the pm
|
|
280
|
+
runtime treats errors thrown from a preflight override as non-fatal — enforced in
|
|
281
|
+
the command handlers themselves, which throw a `CommandError` that genuinely
|
|
282
|
+
aborts the command.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# no webhook configured:
|
|
286
|
+
pm slack notify --text 'hi' # → error + exit 2, nothing posted
|
|
287
|
+
pm slack notify --text 'hi' --dry-run # → offline preview, exit 0
|
|
288
|
+
pm slack test --on close # → offline preview, exit 0
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
118
293
|
## Error Handling
|
|
119
294
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
295
|
+
The **lifecycle hook is best-effort** and never breaks your pm command:
|
|
296
|
+
|
|
297
|
+
- If `PM_SLACK_WEBHOOK` is not set, the hook silently skips all notifications and logs a debug message.
|
|
298
|
+
- If the webhook URL is invalid, the hook logs an error and continues without crashing pm-cli.
|
|
299
|
+
- HTTP errors from Slack (non-2xx responses), network failures, and timeouts (10s) are logged but never fail the underlying `pm` command.
|
|
300
|
+
|
|
301
|
+
The **`slack notify` and `slack digest` commands fail fast** on misconfiguration
|
|
302
|
+
(see [Webhook preflight gate](#webhook-preflight-gate)):
|
|
303
|
+
|
|
304
|
+
- A real (non-`--dry-run`) post without a configured webhook, or with an invalid
|
|
305
|
+
webhook URL, fails with a clear `CommandError` and a non-zero exit **before**
|
|
306
|
+
any post is attempted.
|
|
307
|
+
- Use `--dry-run` to build and preview with no webhook and no network call.
|
|
308
|
+
- Once past the gate, `slack notify` treats a *network/HTTP* failure as
|
|
309
|
+
best-effort (warns, exits 0); `slack digest` reports a post failure as an
|
|
310
|
+
error (exit 1).
|
|
124
311
|
|
|
125
312
|
---
|
|
126
313
|
|
|
@@ -141,12 +328,11 @@ The extension uses only Node.js built-ins (`node:https`) — no external runtime
|
|
|
141
328
|
```json
|
|
142
329
|
{
|
|
143
330
|
"name": "pm-slack",
|
|
144
|
-
"version": "0.1.0",
|
|
145
331
|
"description": "Slack notifications for pm item lifecycle events",
|
|
146
332
|
"author": "@unbraind",
|
|
147
|
-
"entry": "index.js",
|
|
333
|
+
"entry": "./dist/index.js",
|
|
148
334
|
"priority": 50,
|
|
149
|
-
"capabilities": ["hooks", "
|
|
335
|
+
"capabilities": ["commands", "hooks", "schema", "preflight"]
|
|
150
336
|
}
|
|
151
337
|
```
|
|
152
338
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,20 @@
|
|
|
13
13
|
* PM_SLACK_WEBHOOK (required) Slack incoming webhook URL
|
|
14
14
|
* PM_SLACK_CHANNEL (optional) Override channel, e.g. #pm-alerts
|
|
15
15
|
* PM_SLACK_MIN_PRIORITY (optional) Minimum priority to notify (1=critical … 4=low), default 1 (critical only; set 4 for all)
|
|
16
|
-
* PM_SLACK_EVENTS (optional) Comma-separated subset of hook events:
|
|
16
|
+
* PM_SLACK_EVENTS (optional) Comma-separated subset of hook events:
|
|
17
|
+
* create,close,block,cancel,open,start,unblock,reopen (default: all).
|
|
18
|
+
* Status-name aliases (e.g. "canceled","in_progress") are accepted.
|
|
19
|
+
* PM_SLACK_FORMAT (optional) Default notification format: "blockkit" (default) or "text"
|
|
20
|
+
* PM_SLACK_ASSIGNEE_MAP (optional) Comma list of "name=slackId" pairs mapping a pm item's
|
|
21
|
+
* assignee to a Slack @mention, e.g. "alice=U123,bob=U456".
|
|
22
|
+
* PM_SLACK_MENTION_ASSIGNEE (optional) Set to 0/false to disable assignee @mentions even when a map is set
|
|
23
|
+
* (mentions auto-enable in the hook whenever PM_SLACK_ASSIGNEE_MAP is non-empty).
|
|
24
|
+
* PM_SLACK_ROUTES (optional) JSON array of routing rules to send specific
|
|
25
|
+
* events/types/statuses to different webhooks/channels.
|
|
26
|
+
* Each rule: { "match": "<selector>", "webhook"?: "...", "channel"?: "..." }
|
|
27
|
+
* Selector forms: an event ("create"|"close"|"block"),
|
|
28
|
+
* "type:<itemType>" (case-insensitive), or "status:<status>".
|
|
29
|
+
* First matching rule wins; unset fields fall back to the defaults.
|
|
17
30
|
*/
|
|
18
31
|
import type { AfterCommandHookContext } from "@unbrained/pm-cli/sdk";
|
|
19
32
|
declare class CommandError extends Error {
|
|
@@ -31,25 +44,182 @@ interface PmItem {
|
|
|
31
44
|
blocked_reason?: string;
|
|
32
45
|
closedReason?: string;
|
|
33
46
|
blockedReason?: string;
|
|
47
|
+
cancel_reason?: string;
|
|
48
|
+
cancelReason?: string;
|
|
34
49
|
author?: string;
|
|
50
|
+
assignee?: string;
|
|
51
|
+
url?: string;
|
|
52
|
+
github_url?: string;
|
|
53
|
+
githubUrl?: string;
|
|
54
|
+
html_url?: string;
|
|
55
|
+
htmlUrl?: string;
|
|
56
|
+
source_url?: string;
|
|
57
|
+
sourceUrl?: string;
|
|
58
|
+
link?: string;
|
|
59
|
+
}
|
|
60
|
+
type EventKind = "create" | "close" | "block" | "cancel" | "open" | "start" | "unblock" | "reopen";
|
|
61
|
+
type MessageFormat = "blockkit" | "text";
|
|
62
|
+
/** A single routing rule parsed from PM_SLACK_ROUTES. */
|
|
63
|
+
interface RouteRule {
|
|
64
|
+
/** Raw selector string, e.g. "block", "type:Bug", "status:blocked". */
|
|
65
|
+
match: string;
|
|
66
|
+
webhook?: string;
|
|
67
|
+
channel?: string;
|
|
68
|
+
}
|
|
69
|
+
/** Resolved destination for a notification after applying routing rules. */
|
|
70
|
+
interface RouteTarget {
|
|
71
|
+
webhookUrl: string;
|
|
72
|
+
channel?: string;
|
|
35
73
|
}
|
|
36
|
-
type EventKind = "create" | "close" | "block";
|
|
37
74
|
interface SlackBlock {
|
|
38
75
|
type: string;
|
|
39
76
|
[key: string]: unknown;
|
|
40
77
|
}
|
|
78
|
+
interface SlackPayload {
|
|
79
|
+
text: string;
|
|
80
|
+
blocks?: SlackBlock[];
|
|
81
|
+
mrkdwn?: boolean;
|
|
82
|
+
channel?: string;
|
|
83
|
+
thread_ts?: string;
|
|
84
|
+
}
|
|
85
|
+
declare function normalizeEvent(token: string): EventKind | null;
|
|
41
86
|
declare function parseEvents(spec: string | undefined): Set<EventKind>;
|
|
87
|
+
/**
|
|
88
|
+
* Normalize a format spec to a known MessageFormat. Accepts a few friendly
|
|
89
|
+
* aliases ("block"/"blocks" → blockkit, "plain"/"txt" → text). Falls back to
|
|
90
|
+
* the supplied default when the spec is empty or unrecognized.
|
|
91
|
+
*/
|
|
92
|
+
declare function parseFormat(spec: string | undefined, fallback?: MessageFormat): MessageFormat;
|
|
93
|
+
declare function parseRoutes(spec: string | undefined): RouteRule[];
|
|
94
|
+
/**
|
|
95
|
+
* Does a single route rule match this event/item? Selector forms:
|
|
96
|
+
* - bare event name: "create" | "close" | "block"
|
|
97
|
+
* - "type:<itemType>" (case-insensitive on item.type)
|
|
98
|
+
* - "status:<status>" (case-insensitive on item.status)
|
|
99
|
+
* - "*" / "all" (matches everything — useful as a catch-all)
|
|
100
|
+
*/
|
|
101
|
+
declare function ruleMatches(rule: RouteRule, event: EventKind, item: PmItem): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the destination (webhook + channel) for an event/item given the
|
|
104
|
+
* configured routes and the default webhook/channel. First matching rule wins;
|
|
105
|
+
* any field a rule omits falls back to the default. Returns null only when no
|
|
106
|
+
* webhook can be resolved at all.
|
|
107
|
+
*/
|
|
108
|
+
declare function selectRoute(event: EventKind, item: PmItem, routes: RouteRule[], defaultWebhook: string, defaultChannel?: string): RouteTarget | null;
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the effective webhook for a posting command from an explicit
|
|
111
|
+
* `--webhook` flag (highest precedence) or PM_SLACK_WEBHOOK. PM_SLACK_ROUTES
|
|
112
|
+
* may also carry per-rule webhooks; a present route webhook is accepted as a
|
|
113
|
+
* valid source so route-only configurations are not falsely rejected.
|
|
114
|
+
*/
|
|
115
|
+
declare function resolveEffectiveWebhook(webhookFlag: string | undefined): {
|
|
116
|
+
webhookUrl: string;
|
|
117
|
+
source: "flag" | "env" | "route" | "none";
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Validate webhook configuration for a Slack-posting command. Throws a
|
|
121
|
+
* CommandError (exit 2 / USAGE) with an actionable message when no webhook is
|
|
122
|
+
* configured or the configured webhook URL is syntactically invalid. Returns
|
|
123
|
+
* silently (pass-through) on a valid config. Performs NO network I/O.
|
|
124
|
+
*
|
|
125
|
+
* `commandLabel` is woven into the message (e.g. "slack digest") so the user
|
|
126
|
+
* sees exactly which command was gated.
|
|
127
|
+
*/
|
|
128
|
+
declare function assertWebhookConfigured(webhookFlag: string | undefined, commandLabel: string): void;
|
|
42
129
|
declare function meetsMinPriority(item: PmItem, minPriority: Priority): boolean;
|
|
43
|
-
|
|
130
|
+
/** Parse PM_SLACK_ASSIGNEE_MAP into a case-insensitive name→id lookup. */
|
|
131
|
+
declare function parseAssigneeMap(spec: string | undefined): Map<string, string>;
|
|
132
|
+
/**
|
|
133
|
+
* Resolve an item's assignee to a Slack mention token using the map. Returns a
|
|
134
|
+
* ready-to-embed mention string (e.g. `<@U123>`) or undefined when there's no
|
|
135
|
+
* assignee or no mapping. A raw id is wrapped as `<@id>`; a token already
|
|
136
|
+
* containing `<` is passed through unchanged (supports `<!subteam^S123>` etc).
|
|
137
|
+
*/
|
|
138
|
+
declare function resolveAssigneeMention(item: PmItem, map: Map<string, string>): string | undefined;
|
|
139
|
+
declare function isHttpUrl(value: unknown): value is string;
|
|
140
|
+
/** Resolve a primary URL for the item, plus whether it points at GitHub. */
|
|
141
|
+
declare function resolveItemUrl(item: PmItem, override?: string): {
|
|
142
|
+
url: string;
|
|
143
|
+
isGithub: boolean;
|
|
144
|
+
} | undefined;
|
|
145
|
+
declare function buildTextMessage(item: PmItem, event: EventKind, channel?: string, mention?: string): string;
|
|
44
146
|
interface BlockKitOptions {
|
|
45
147
|
channel?: string;
|
|
46
148
|
/** Extra free-form body appended as a section (e.g. from `--text`). */
|
|
47
149
|
note?: string;
|
|
150
|
+
/** Pre-resolved Slack mention token for the assignee (e.g. `<@U123>`). */
|
|
151
|
+
mention?: string;
|
|
152
|
+
/** Resolved item/GitHub URL for an action button. */
|
|
153
|
+
link?: {
|
|
154
|
+
url: string;
|
|
155
|
+
isGithub: boolean;
|
|
156
|
+
};
|
|
48
157
|
}
|
|
49
158
|
declare function buildItemBlockKit(item: PmItem, event: EventKind, opts?: BlockKitOptions): {
|
|
50
159
|
blocks: SlackBlock[];
|
|
51
160
|
fallback: string;
|
|
52
161
|
};
|
|
162
|
+
/**
|
|
163
|
+
* Build a ready-to-post SlackPayload for an item event in the requested format.
|
|
164
|
+
* - "blockkit": rich Block Kit blocks + plain-text fallback in `text`.
|
|
165
|
+
* - "text": plain mrkdwn only (no `blocks`), for minimal/legacy channels.
|
|
166
|
+
*/
|
|
167
|
+
declare function buildItemPayload(item: PmItem, event: EventKind, format: MessageFormat, opts?: BlockKitOptions): SlackPayload;
|
|
168
|
+
/** Fields we read off each stored item for digest purposes. */
|
|
169
|
+
interface DigestItem extends PmItem {
|
|
170
|
+
created_at?: string;
|
|
171
|
+
updated_at?: string;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Minimal parser for a stored pm item file. Handles the toon scalar form
|
|
175
|
+
* (`key: value`, optionally quoted) and JSON. Only top-level scalar fields are
|
|
176
|
+
* extracted; nested/array sections are ignored. Returns null when no id found.
|
|
177
|
+
*/
|
|
178
|
+
declare function parseStoredItem(content: string, ext: string): DigestItem | null;
|
|
179
|
+
/**
|
|
180
|
+
* Resolve a `--since <date>` / `--days <n>` window into an epoch-ms cutoff.
|
|
181
|
+
* `since` (ISO date or datetime) wins when both are given; otherwise `days`
|
|
182
|
+
* back from `now`. Defaults to 7 days. Returns the cutoff in ms.
|
|
183
|
+
*/
|
|
184
|
+
declare function resolveWindow(since: string | undefined, days: number | undefined, now?: number): {
|
|
185
|
+
cutoffMs: number;
|
|
186
|
+
label: string;
|
|
187
|
+
};
|
|
188
|
+
type DigestBucket = "created" | "closed" | "blocked" | "in_progress";
|
|
189
|
+
interface DigestSummary {
|
|
190
|
+
windowLabel: string;
|
|
191
|
+
cutoffMs: number;
|
|
192
|
+
counts: Record<DigestBucket, number>;
|
|
193
|
+
buckets: Record<DigestBucket, DigestItem[]>;
|
|
194
|
+
total: number;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Aggregate store items into digest buckets for the window. An item counts as:
|
|
198
|
+
* - created if created_at >= cutoff
|
|
199
|
+
* - closed if it is in a closed/done status AND updated_at >= cutoff
|
|
200
|
+
* - blocked if status === blocked AND updated_at >= cutoff
|
|
201
|
+
* - in_progress if status === in_progress AND updated_at >= cutoff
|
|
202
|
+
* An item may appear in multiple buckets (e.g. created and blocked).
|
|
203
|
+
*/
|
|
204
|
+
declare function aggregateDigest(items: DigestItem[], cutoffMs: number, windowLabel: string): DigestSummary;
|
|
205
|
+
declare function buildDigestText(summary: DigestSummary, channel?: string): string;
|
|
206
|
+
declare function buildDigestBlockKit(summary: DigestSummary, channel?: string): {
|
|
207
|
+
blocks: SlackBlock[];
|
|
208
|
+
fallback: string;
|
|
209
|
+
};
|
|
210
|
+
declare function buildDigestPayload(summary: DigestSummary, format: MessageFormat, channel?: string): SlackPayload;
|
|
211
|
+
/** Map a (normalized) status string to the EventKind for a transition into it. */
|
|
212
|
+
declare function statusToEvent(status: string | undefined): EventKind | null;
|
|
213
|
+
/**
|
|
214
|
+
* Detect the lifecycle event for a completed command. Strategy:
|
|
215
|
+
* 1. Create/close commands map directly.
|
|
216
|
+
* 2. Status-changing commands (update/set/lifecycle aliases) derive the event
|
|
217
|
+
* from the *resulting* status (preferring the explicit --status option,
|
|
218
|
+
* falling back to the result item's status).
|
|
219
|
+
* 3. When the result carries the prior status (`previousStatus`), refine
|
|
220
|
+
* open→reopen (from a closed/canceled state) and open→unblock (from
|
|
221
|
+
* blocked) so terminal-recovery transitions are distinguishable.
|
|
222
|
+
*/
|
|
53
223
|
declare function detectEvent(ctx: AfterCommandHookContext): EventKind | null;
|
|
54
224
|
declare function extractItem(ctx: AfterCommandHookContext): PmItem | null;
|
|
55
225
|
declare const _default: {
|
|
@@ -60,11 +230,34 @@ declare const _default: {
|
|
|
60
230
|
export default _default;
|
|
61
231
|
export declare const __test__: {
|
|
62
232
|
buildItemBlockKit: typeof buildItemBlockKit;
|
|
233
|
+
buildItemPayload: typeof buildItemPayload;
|
|
63
234
|
buildTextMessage: typeof buildTextMessage;
|
|
64
235
|
parseEvents: typeof parseEvents;
|
|
236
|
+
normalizeEvent: typeof normalizeEvent;
|
|
237
|
+
parseFormat: typeof parseFormat;
|
|
238
|
+
parseRoutes: typeof parseRoutes;
|
|
239
|
+
ruleMatches: typeof ruleMatches;
|
|
240
|
+
selectRoute: typeof selectRoute;
|
|
65
241
|
detectEvent: typeof detectEvent;
|
|
242
|
+
statusToEvent: typeof statusToEvent;
|
|
66
243
|
extractItem: typeof extractItem;
|
|
67
244
|
meetsMinPriority: typeof meetsMinPriority;
|
|
245
|
+
parseAssigneeMap: typeof parseAssigneeMap;
|
|
246
|
+
resolveAssigneeMention: typeof resolveAssigneeMention;
|
|
247
|
+
resolveItemUrl: typeof resolveItemUrl;
|
|
248
|
+
isHttpUrl: typeof isHttpUrl;
|
|
249
|
+
EVENT_META: Record<EventKind, {
|
|
250
|
+
verb: string;
|
|
251
|
+
emoji: string;
|
|
252
|
+
}>;
|
|
253
|
+
parseStoredItem: typeof parseStoredItem;
|
|
254
|
+
resolveWindow: typeof resolveWindow;
|
|
255
|
+
aggregateDigest: typeof aggregateDigest;
|
|
256
|
+
buildDigestText: typeof buildDigestText;
|
|
257
|
+
buildDigestBlockKit: typeof buildDigestBlockKit;
|
|
258
|
+
buildDigestPayload: typeof buildDigestPayload;
|
|
259
|
+
resolveEffectiveWebhook: typeof resolveEffectiveWebhook;
|
|
260
|
+
assertWebhookConfigured: typeof assertWebhookConfigured;
|
|
68
261
|
EXIT_CODE: {
|
|
69
262
|
readonly GENERIC_FAILURE: 1;
|
|
70
263
|
readonly USAGE: 2;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|