@zibby/workflow-templates 0.3.0 → 0.4.2

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.
Files changed (34) hide show
  1. package/browser-test-automation/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  2. package/browser-test-automation/package.json +1 -0
  3. package/code-analysis/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  4. package/code-analysis/nodes/generate-code-node.js +12 -2
  5. package/index.js +235 -0
  6. package/notify-lark/README.md +88 -0
  7. package/notify-lark/graph.mjs +43 -0
  8. package/notify-lark/icon.png +0 -0
  9. package/notify-lark/nodes/notify-lark-node.js +303 -0
  10. package/notify-lark/package.json +18 -0
  11. package/notify-lark/state.js +85 -0
  12. package/notify-notion/README.md +71 -0
  13. package/notify-notion/graph.mjs +64 -0
  14. package/notify-notion/icon.png +0 -0
  15. package/notify-notion/nodes/notify-notion-node.js +342 -0
  16. package/notify-notion/package.json +19 -0
  17. package/notify-notion/state.js +110 -0
  18. package/notify-slack/README.md +94 -0
  19. package/notify-slack/graph.mjs +51 -0
  20. package/notify-slack/icon.png +0 -0
  21. package/notify-slack/nodes/notify-slack-node.js +268 -0
  22. package/notify-slack/package.json +18 -0
  23. package/notify-slack/state.js +112 -0
  24. package/package.json +17 -3
  25. package/sentry-triage/graph.mjs +81 -0
  26. package/sentry-triage/icon.png +0 -0
  27. package/sentry-triage/nodes/classify-node.js +38 -0
  28. package/sentry-triage/nodes/dispatch-alerts-node.js +191 -0
  29. package/sentry-triage/nodes/fetch-issues-node.js +52 -0
  30. package/sentry-triage/nodes/filter-noise-node.js +112 -0
  31. package/sentry-triage/package.json +18 -0
  32. package/sentry-triage/prompts/classify.md +76 -0
  33. package/sentry-triage/prompts/fetch-issues.md +66 -0
  34. package/sentry-triage/state.js +134 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * notify-lark node — deterministic, no LLM.
3
+ *
4
+ * Sends a Lark Interactive Card via /open-apis/im/v1/messages. Same
5
+ * design philosophy as notify-slack: parent makes all the decisions
6
+ * (severity, title, body, who to mention), this node just renders the
7
+ * card and POSTs it.
8
+ *
9
+ * Auth differs from Slack:
10
+ * - resolveIntegrationToken('lark') returns { appId, appSecret, host }
11
+ * not a ready-to-use bearer token.
12
+ * - We exchange app credentials for a `tenant_access_token` via
13
+ * /open-apis/auth/v3/tenant_access_token/internal, then use that as
14
+ * Bearer auth on the message send. Same flow @zibby/skills/lark.js
15
+ * uses; we reimplement here so the node stays decoupled from the
16
+ * MCP-tool path (which only the LLM nodes use).
17
+ *
18
+ * The token has ~2h TTL — we cache per-process so a parent dispatching
19
+ * many alerts only pays the auth round-trip once.
20
+ *
21
+ * Severity → Lark header template (named themes, not hex):
22
+ * low → grey
23
+ * medium → yellow
24
+ * high → orange
25
+ * critical → red
26
+ *
27
+ * The Lark API quirk: `content` on the message body must be a
28
+ * JSON-encoded STRING (not the object itself). Easy to miss.
29
+ */
30
+
31
+ import { z } from 'zod';
32
+ import { SKILLS } from '@zibby/core';
33
+ import { resolveIntegrationToken } from '@zibby/core/backend-client.js';
34
+ // Universal renderer — see notify-slack-node.js for the design note;
35
+ // when state.report is present the legacy buildLarkCard path is bypassed.
36
+ import { reportToLarkCard } from '@zibby/skills/report';
37
+
38
+ const SEVERITY_TEMPLATE = Object.freeze({
39
+ low: 'grey',
40
+ medium: 'yellow',
41
+ high: 'orange',
42
+ critical: 'red',
43
+ });
44
+
45
+ const SEVERITY_EMOJI = Object.freeze({
46
+ low: '⚪',
47
+ medium: '🟡',
48
+ high: '🟠',
49
+ critical: '🚨',
50
+ });
51
+
52
+ const NotifyLarkOutputSchema = z.object({
53
+ delivered: z.boolean(),
54
+ receiveId: z.string(),
55
+ receiveIdType: z.string(),
56
+ messageId: z.string().optional(),
57
+ });
58
+
59
+ /**
60
+ * Build the Lark Interactive Card body. Returns the card OBJECT (not
61
+ * the JSON-stringified form the API expects — callers stringify it).
62
+ * Exposed for unit tests so we can pin the rendered shape.
63
+ */
64
+ export function buildLarkCard(input) {
65
+ const {
66
+ severity = 'medium',
67
+ title,
68
+ body,
69
+ sentryLink,
70
+ affectedUsers,
71
+ events,
72
+ release,
73
+ firstSeen,
74
+ codeSnippet,
75
+ actionUrl,
76
+ actionLabel,
77
+ mentions,
78
+ } = input;
79
+
80
+ const elements = [];
81
+
82
+ // Metadata grid — 2-column layout via `is_short: true` on each field.
83
+ const fields = [];
84
+ if (typeof affectedUsers === 'number') {
85
+ fields.push({
86
+ is_short: true,
87
+ text: { tag: 'lark_md', content: `**Users affected**\n${affectedUsers}` },
88
+ });
89
+ }
90
+ if (typeof events === 'number') {
91
+ fields.push({
92
+ is_short: true,
93
+ text: { tag: 'lark_md', content: `**Events**\n${events}` },
94
+ });
95
+ }
96
+ if (release) {
97
+ fields.push({
98
+ is_short: true,
99
+ text: { tag: 'lark_md', content: `**Release**\n\`${release}\`` },
100
+ });
101
+ }
102
+ if (firstSeen) {
103
+ fields.push({
104
+ is_short: true,
105
+ text: { tag: 'lark_md', content: `**First seen**\n${firstSeen}` },
106
+ });
107
+ }
108
+ if (fields.length > 0) {
109
+ elements.push({ tag: 'div', fields });
110
+ }
111
+
112
+ // Body — rendered as lark_md (Lark's markdown variant).
113
+ if (body && body.trim().length > 0) {
114
+ elements.push({
115
+ tag: 'div',
116
+ text: { tag: 'lark_md', content: body },
117
+ });
118
+ }
119
+
120
+ // Code snippet — fenced triple-backtick block.
121
+ if (codeSnippet) {
122
+ elements.push({
123
+ tag: 'div',
124
+ text: { tag: 'lark_md', content: '```\n' + codeSnippet + '\n```' },
125
+ });
126
+ }
127
+
128
+ // Action buttons — same logic as Slack: primary "Open in Sentry"
129
+ // when sentryLink set, plus secondary actionUrl/Label pair if both
130
+ // supplied.
131
+ const actions = [];
132
+ if (sentryLink) {
133
+ actions.push({
134
+ tag: 'button',
135
+ text: { tag: 'plain_text', content: 'Open in Sentry' },
136
+ type: 'primary',
137
+ url: sentryLink,
138
+ });
139
+ }
140
+ if (actionUrl && actionLabel) {
141
+ actions.push({
142
+ tag: 'button',
143
+ text: { tag: 'plain_text', content: actionLabel.slice(0, 40) },
144
+ type: 'default',
145
+ url: actionUrl,
146
+ });
147
+ }
148
+ if (actions.length > 0) {
149
+ elements.push({ tag: 'action', actions });
150
+ }
151
+
152
+ // Mentions — small-text note at bottom. Caller pre-formatted as
153
+ // `<at user_id="ou_xxx">@name</at>` strings.
154
+ if (Array.isArray(mentions) && mentions.length > 0) {
155
+ elements.push({
156
+ tag: 'note',
157
+ elements: [{ tag: 'lark_md', content: mentions.join(' ') }],
158
+ });
159
+ }
160
+
161
+ const emoji = SEVERITY_EMOJI[severity] || SEVERITY_EMOJI.medium;
162
+ const template = SEVERITY_TEMPLATE[severity] || SEVERITY_TEMPLATE.medium;
163
+
164
+ return {
165
+ config: { wide_screen_mode: true },
166
+ header: {
167
+ template,
168
+ title: {
169
+ tag: 'plain_text',
170
+ content: `${emoji} ${severity.toUpperCase()} ${title}`.slice(0, 150),
171
+ },
172
+ },
173
+ elements,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Infer the Lark receive_id_type query param from the id prefix.
179
+ * Mirrors @zibby/skills/lark.js's inferReceiveIdType — duplicated to
180
+ * keep the node self-contained.
181
+ */
182
+ export function inferReceiveIdType(id) {
183
+ if (!id || typeof id !== 'string') return 'chat_id';
184
+ if (id.startsWith('oc_')) return 'chat_id';
185
+ if (id.startsWith('ou_')) return 'open_id';
186
+ if (id.startsWith('on_')) return 'union_id';
187
+ if (id.startsWith('cli_')) return 'app_id';
188
+ if (id.includes('@')) return 'email';
189
+ return 'chat_id';
190
+ }
191
+
192
+ // Per-process cache. Tenant access tokens last ~2h; we cache 100m to
193
+ // avoid the boundary case where a 119-min-old token is rejected mid-call.
194
+ const TOKEN_TTL_MS = 100 * 60 * 1000;
195
+ let _tenantTokenCache = null;
196
+
197
+ async function getTenantAccessToken() {
198
+ const { appId, appSecret, host } = await resolveIntegrationToken('lark');
199
+ if (
200
+ _tenantTokenCache
201
+ && _tenantTokenCache.appId === appId
202
+ && _tenantTokenCache.expiresAt > Date.now()
203
+ ) {
204
+ return { token: _tenantTokenCache.token, host };
205
+ }
206
+ const url = `${host}/open-apis/auth/v3/tenant_access_token/internal`;
207
+ const res = await fetch(url, {
208
+ method: 'POST',
209
+ headers: { 'Content-Type': 'application/json' },
210
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
211
+ });
212
+ const data = await res.json().catch(() => ({}));
213
+ if (data.code !== 0) {
214
+ const e = new Error(`Lark tenant_access_token failed: ${data.msg || data.code}`);
215
+ e.code = data.code;
216
+ throw e;
217
+ }
218
+ _tenantTokenCache = {
219
+ appId,
220
+ token: data.tenant_access_token,
221
+ expiresAt: Date.now() + TOKEN_TTL_MS,
222
+ };
223
+ return { token: data.tenant_access_token, host };
224
+ }
225
+
226
+ /** Test hook — resets the per-process token cache. */
227
+ export function _resetTokenCache() { _tenantTokenCache = null; }
228
+
229
+ /**
230
+ * Send the Lark card. Exposed for unit tests so we can stub fetch +
231
+ * assert payload shape without going through token resolution.
232
+ */
233
+ export async function postToLark({ token, host, receiveId, receiveIdType, card }) {
234
+ const url = `${host}/open-apis/im/v1/messages?receive_id_type=${encodeURIComponent(receiveIdType)}`;
235
+ const res = await fetch(url, {
236
+ method: 'POST',
237
+ headers: {
238
+ Authorization: `Bearer ${token}`,
239
+ 'Content-Type': 'application/json; charset=utf-8',
240
+ },
241
+ body: JSON.stringify({
242
+ receive_id: receiveId,
243
+ msg_type: 'interactive',
244
+ // Lark wants `content` as a JSON-encoded STRING, not an object.
245
+ content: JSON.stringify(card),
246
+ }),
247
+ });
248
+ const data = await res.json().catch(() => ({}));
249
+ if (data.code !== 0) {
250
+ const e = new Error(`Lark send message failed: ${data.msg || `code ${data.code} / http ${res.status}`}`);
251
+ e.code = data.code || 'unknown';
252
+ e.status = res.status;
253
+ throw e;
254
+ }
255
+ return { messageId: data.data?.message_id };
256
+ }
257
+
258
+ export const notifyLarkNode = {
259
+ name: 'notify_lark',
260
+ // Declares Lark as a hard requirement for marketplace gating (same
261
+ // pattern as notify-slack — see that file's comment for rationale).
262
+ skills: [SKILLS.LARK],
263
+ outputSchema: NotifyLarkOutputSchema,
264
+ timeout: 15 * 1000,
265
+ execute: async (context) => {
266
+ const state = (context?.state && typeof context.state.getAll === 'function')
267
+ ? context.state.getAll()
268
+ : context;
269
+
270
+ const receiveId = state.receiveId;
271
+ if (!receiveId) {
272
+ throw new Error('notify-lark: input.receiveId is required');
273
+ }
274
+ if (!state.report && !state.title) {
275
+ throw new Error('notify-lark: input.title is required when input.report is absent');
276
+ }
277
+ const receiveIdType = inferReceiveIdType(receiveId);
278
+
279
+ const { token, host } = await getTenantAccessToken();
280
+ // Two rendering paths (symmetric with notify-slack):
281
+ // 1. state.report present → reportToLarkCard renders the full
282
+ // structured digest. Legacy fields are ignored.
283
+ // 2. Legacy severity/title/body shape → buildLarkCard renders
284
+ // the simple alert (sentry-triage et al.).
285
+ const card = (state.report && typeof state.report === 'object')
286
+ ? reportToLarkCard(state.report)
287
+ : buildLarkCard(state);
288
+ const { messageId } = await postToLark({
289
+ token,
290
+ host,
291
+ receiveId,
292
+ receiveIdType,
293
+ card,
294
+ });
295
+
296
+ return {
297
+ delivered: true,
298
+ receiveId,
299
+ receiveIdType,
300
+ messageId,
301
+ };
302
+ },
303
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "notify-lark",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Post a structured alert card to a Lark / Feishu chat — child workflow, dispatched by parent workflows via sub-graph.",
7
+ "main": "graph.mjs",
8
+ "scripts": {
9
+ "test": "vitest run"
10
+ },
11
+ "dependencies": {
12
+ "@zibby/core": "^0.5.1",
13
+ "zod": "^3.23.0"
14
+ },
15
+ "devDependencies": {
16
+ "vitest": "^2.1.5"
17
+ }
18
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * notify-lark — three-schema state model.
3
+ *
4
+ * Mirrors notify-slack's input shape EXACTLY (provider-neutral fields)
5
+ * so a parent workflow that wants to fan-out alerts to both Slack and
6
+ * Lark can use the same `input: (state) => ({...})` block for both
7
+ * dispatchSubgraph calls. The notifier-specific bits (`receiveId` vs
8
+ * `channel`, mention syntax) are caller-supplied; we don't try to
9
+ * translate between providers.
10
+ *
11
+ * Lark differences vs Slack:
12
+ * - Target is `receiveId` (chat_id `oc_…`, open_id `ou_…`, or email)
13
+ * — Lark needs the right `receive_id_type` query param, inferred
14
+ * from the id prefix.
15
+ * - Color is a theme template name (`red`, `orange`, `yellow`, `grey`,
16
+ * `purple`, `green`), not a hex.
17
+ * - Mention syntax: `<at user_id="ou_xxx">name</at>` — caller passes
18
+ * these strings ready-formatted, same as Slack.
19
+ */
20
+
21
+ import { z } from 'zod';
22
+
23
+ export const SEVERITIES = /** @type {const} */ (['low', 'medium', 'high', 'critical']);
24
+
25
+ export const notifyLarkInputSchema = z.object({
26
+ severity: z.enum(SEVERITIES)
27
+ .default('medium')
28
+ .describe('Alert severity. Drives header color + mention strategy.'),
29
+
30
+ // Required in legacy-mode; rich-mode (when `report` is set) sources
31
+ // the title from `report.title` instead. See notify-slack/state.js
32
+ // for the symmetric design.
33
+ title: z.string().min(1).max(300).optional()
34
+ .describe('Card header text. Required when `report` is absent (rich-mode sources from report.title).'),
35
+
36
+ body: z.string().max(3000).optional()
37
+ .describe('Body text. Supports Lark lark_md (e.g. **bold**, `code`, [text](url)).'),
38
+
39
+ receiveId: z.string().min(1).max(120)
40
+ .describe(
41
+ 'Lark target. Format determines receive_id_type automatically:\n' +
42
+ ' oc_… → chat_id (group/DM)\n' +
43
+ ' ou_… → open_id (user)\n' +
44
+ ' on_… → union_id\n' +
45
+ ' email → email',
46
+ ),
47
+
48
+ // Sentry-flavored fields (same shape as notify-slack).
49
+ sentryLink: z.string().url().optional()
50
+ .describe('Sentry issue URL — adds an "Open in Sentry" primary button.'),
51
+ affectedUsers: z.number().int().min(0).optional(),
52
+ events: z.number().int().min(0).optional(),
53
+ release: z.string().max(120).optional(),
54
+ firstSeen: z.string().optional(),
55
+ codeSnippet: z.string().max(2000).optional(),
56
+ actionUrl: z.string().url().optional(),
57
+ actionLabel: z.string().max(40).optional(),
58
+
59
+ // Mentions — caller-supplied Lark <at> strings.
60
+ mentions: z.array(z.string().max(200))
61
+ .max(20)
62
+ .optional()
63
+ .describe('Lark @-mentions, e.g. [\'<at user_id="ou_alice">@Alice</at>\'].'),
64
+
65
+ idempotencyKey: z.string().max(128).optional(),
66
+
67
+ // ── Rich-report mode ──────────────────────────────────────────────
68
+ // When `report` is present, the node renders the report-object via
69
+ // reportToLarkCard() and IGNORES the legacy severity/title/body
70
+ // fields. See notify-slack/state.js for the symmetric design.
71
+ report: z.record(z.any()).optional()
72
+ .describe('Rich report-object (see @zibby/skills/report). When set, supersedes severity/title/body — the node renders a full Lark Card.'),
73
+ });
74
+
75
+ export const notifyLarkContextSchema = z.object({
76
+ notify_lark: z.object({
77
+ delivered: z.boolean(),
78
+ receiveId: z.string(),
79
+ receiveIdType: z.string(),
80
+ messageId: z.string().optional(),
81
+ }).optional(),
82
+ });
83
+
84
+ export const notifyLarkStateSchema =
85
+ notifyLarkInputSchema.merge(notifyLarkContextSchema);
@@ -0,0 +1,71 @@
1
+ # notify-notion
2
+
3
+ A reusable **child workflow** that posts a structured page (or appends blocks) to a Notion workspace.
4
+
5
+ Designed to be dispatched via sub-graph from any parent workflow — most commonly digest workflows (`ai-spend-weekly-digest`) that want to archive a weekly report to a Notion database, or alert workflows (`sentry-triage`) that want a durable record of each incident.
6
+
7
+ ## What it does
8
+
9
+ - Takes a provider-neutral payload (severity, title, body, target, optional Sentry-flavored fields)
10
+ - Builds a Notion page (or block-children array) — including rich-report mode that delegates to `@zibby/skills/report`'s `reportToNotionBlocks`
11
+ - Posts via `POST /v1/pages` (when `databaseId` is supplied) or `PATCH /v1/blocks/{pageId}/children` (when `pageId` is supplied)
12
+ - Returns the page id + URL so the parent can link to it from downstream notifications
13
+
14
+ No LLM call — single deterministic API request, typically ~1-2s depending on block count.
15
+
16
+ ## Dispatch shape (parent workflow)
17
+
18
+ ```js
19
+ import { WorkflowGraph, SKILLS } from '@zibby/core';
20
+
21
+ const g = new WorkflowGraph();
22
+ g.addNode('archive_to_notion', {
23
+ workflow: 'notify-notion',
24
+ input: (state) => ({
25
+ // Either databaseId OR pageId — not both:
26
+ databaseId: process.env.NOTION_INCIDENT_DB,
27
+ severity: 'critical',
28
+ title: 'Checkout: TypeError on session.user.id',
29
+ body: '12 users affected in the last 1h.',
30
+ sentryLink: 'https://sentry.io/.../1234567890/',
31
+ affectedUsers: 12,
32
+ events: 47,
33
+ release: '1.42.0',
34
+ }),
35
+ });
36
+ ```
37
+
38
+ For digest workflows, pass a `report` object instead of severity/title/body — the node renders the full structured page via `reportToNotionBlocks`:
39
+
40
+ ```js
41
+ g.addNode('archive_digest', {
42
+ workflow: 'notify-notion',
43
+ input: (state) => ({
44
+ databaseId: process.env.NOTION_REPORTS_DB,
45
+ report: state.analyze.report, // produced by an upstream digest node
46
+ }),
47
+ });
48
+ ```
49
+
50
+ ## Output
51
+
52
+ ```ts
53
+ {
54
+ delivered: boolean,
55
+ pageId: string,
56
+ pageUrl?: string, // only set on the create branch (Notion returns it)
57
+ blocksCount?: number // diagnostic — how many blocks were posted
58
+ }
59
+ ```
60
+
61
+ ## Auth
62
+
63
+ The bot's OAuth token is resolved via `resolveIntegrationToken('notion')`. Connect your workspace in **Settings → Integrations → Notion** in the Zibby dashboard before running.
64
+
65
+ ## Failure modes
66
+
67
+ - **401** — Notion rejected the token (revoked, or wrong workspace).
68
+ - **404** — Page/database not found, OR the Notion integration isn't added to the target page (Notion's quirk — share the page with the integration explicitly).
69
+ - **429** — Rate-limited. The parent should backoff + retry.
70
+
71
+ All three surface as typed errors with `.status` set; the parent's `onComplete` sees a failed execution row and can re-dispatch.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * notify-notion — single-node child workflow.
3
+ *
4
+ * Companion to notify-slack / notify-lark; same provider-neutral input
5
+ * shape so a parent can dispatch the same payload to all three (just
6
+ * swap the destination field: `channel` → `receiveId` → `databaseId`
7
+ * or `pageId`).
8
+ *
9
+ * Two write modes:
10
+ * - Create a NEW page in a database (`input.databaseId` set)
11
+ * - APPEND blocks to an existing page (`input.pageId` set)
12
+ * Exactly one must be supplied — the node throws clearly if neither
13
+ * or both are present.
14
+ *
15
+ * Returns `{ delivered, pageId, pageUrl, blocksCount }`. The `pageUrl`
16
+ * is only populated on the create-page branch (Notion returns the URL
17
+ * in the response); appended pages reuse the caller-supplied pageId.
18
+ *
19
+ * Example dispatch shape (parent workflow):
20
+ *
21
+ * g.addNode('notify', { workflow: 'notify-notion',
22
+ * input: (state) => ({
23
+ * severity: 'critical',
24
+ * title: 'Checkout: TypeError',
25
+ * body: '12 users affected in last 1h',
26
+ * databaseId: 'abc123...', // create new page in this DB
27
+ * sentryLink: 'https://sentry.io/.../1234567890/',
28
+ * }),
29
+ * });
30
+ *
31
+ * For digest / report workflows, drop the legacy severity/title/body
32
+ * fields and pass a `report` object instead — the node delegates to
33
+ * @zibby/skills's reportToNotionBlocks() for the full structured page.
34
+ */
35
+
36
+ import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
37
+ import { notifyNotionNode } from './nodes/notify-notion-node.js';
38
+ import {
39
+ notifyNotionInputSchema,
40
+ notifyNotionContextSchema,
41
+ } from './state.js';
42
+
43
+ export class NotifyNotionAgent extends WorkflowAgent {
44
+ buildGraph() {
45
+ const graph = new WorkflowGraph();
46
+ graph
47
+ .setInputSchema(notifyNotionInputSchema)
48
+ .setContextSchema(notifyNotionContextSchema);
49
+
50
+ graph.addNode('notify_notion', notifyNotionNode);
51
+ graph.setEntryPoint('notify_notion');
52
+ graph.addEdge('notify_notion', 'END');
53
+
54
+ return graph;
55
+ }
56
+
57
+ async onComplete(result) {
58
+ const delivered = !!result?.state?.notify_notion?.delivered;
59
+ const pageId = result?.state?.notify_notion?.pageId || '?';
60
+ console.log(`[notify-notion] ${delivered ? 'delivered' : 'failed'} (pageId=${pageId})`);
61
+ }
62
+ }
63
+
64
+ export default NotifyNotionAgent;
Binary file