@cuylabs/channel-slack-agent-core 0.8.0 → 0.9.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.
Files changed (43) hide show
  1. package/README.md +4 -2
  2. package/dist/adapter/index.d.ts +6 -5
  3. package/dist/adapter/index.js +2 -2
  4. package/dist/{adapter-B3CI611y.d.ts → adapter-vbqtraAr.d.ts} +1 -1
  5. package/dist/app-surface.d.ts +6 -5
  6. package/dist/app-surface.js +4 -4
  7. package/dist/app.d.ts +6 -5
  8. package/dist/app.js +5 -5
  9. package/dist/assistant/index.d.ts +5 -4
  10. package/dist/assistant/index.js +2 -2
  11. package/dist/{chunk-TCNJY7QA.js → chunk-D4CSEAIF.js} +1 -1
  12. package/dist/{chunk-7YZWCSML.js → chunk-FJP6ZFUB.js} +1 -1
  13. package/dist/{chunk-6T6N4MRK.js → chunk-GKZRDNEB.js} +2 -2
  14. package/dist/{chunk-YSDFYHPC.js → chunk-HHXAXSG6.js} +2 -2
  15. package/dist/{chunk-2R7B7NJR.js → chunk-JU5R6JZG.js} +1 -1
  16. package/dist/{chunk-236WN6JD.js → chunk-KAEZPS3U.js} +1 -1
  17. package/dist/chunk-OP27SSZU.js +409 -0
  18. package/dist/{chunk-FQWFB54C.js → chunk-XA7U3GRN.js} +1 -1
  19. package/dist/express-assistant.d.ts +4 -3
  20. package/dist/express-assistant.js +3 -3
  21. package/dist/express.d.ts +5 -4
  22. package/dist/express.js +3 -3
  23. package/dist/history/index.d.ts +6 -5
  24. package/dist/index.d.ts +11 -10
  25. package/dist/index.js +10 -34
  26. package/dist/interactive/index.d.ts +5 -65
  27. package/dist/interactive/index.js +3 -27
  28. package/dist/interactive-BigrPKnu.d.ts +30 -0
  29. package/dist/{options-C7-VXmhD.d.ts → options-ByNm2o89.d.ts} +2 -2
  30. package/dist/{options-BcDReOJv.d.ts → options-CGUfVStV.d.ts} +1 -1
  31. package/dist/shared/index.d.ts +7 -76
  32. package/dist/shared/index.js +1 -1
  33. package/dist/socket.d.ts +6 -5
  34. package/dist/socket.js +5 -5
  35. package/dist/{types-CRWzJB5G.d.ts → types-BeGPexio.d.ts} +2 -2
  36. package/dist/{types-Crpil4kb.d.ts → types-Bz4OYEAV.d.ts} +6 -55
  37. package/docs/concepts/interactive-requests.md +7 -7
  38. package/docs/reference/boundary.md +5 -3
  39. package/docs/reference/exports.md +18 -18
  40. package/package.json +2 -2
  41. package/dist/chunk-X7ILLZZP.js +0 -1046
  42. package/dist/interactive-o_NZb-Xg.d.ts +0 -47
  43. /package/dist/{chunk-TMADMHBN.js → chunk-VBGQD6JT.js} +0 -0
@@ -1,1046 +0,0 @@
1
- // src/interactive/store.ts
2
- function createInMemorySlackInteractiveRequestStore() {
3
- const records = /* @__PURE__ */ new Map();
4
- return {
5
- async get(requestId) {
6
- const record = records.get(requestId);
7
- return record ? cloneRecord(record) : void 0;
8
- },
9
- async upsert(record) {
10
- const existing = records.get(record.id);
11
- if (existing?.status === "resolved") {
12
- return cloneRecord(existing);
13
- }
14
- const next = existing ? {
15
- ...existing,
16
- ...record,
17
- target: record.target ?? existing.target,
18
- resolution: record.resolution ?? existing.resolution,
19
- updatedAt: nowIso()
20
- } : { ...record };
21
- records.set(record.id, cloneRecord(next));
22
- return cloneRecord(next);
23
- },
24
- async attachTarget(requestId, target) {
25
- const existing = records.get(requestId);
26
- if (!existing) return void 0;
27
- const next = {
28
- ...existing,
29
- target: cloneTarget(target),
30
- updatedAt: nowIso()
31
- };
32
- records.set(requestId, cloneRecord(next));
33
- return cloneRecord(next);
34
- },
35
- async resolve(requestId, resolution) {
36
- const existing = records.get(requestId);
37
- if (!existing) return void 0;
38
- if (existing.status === "resolved") {
39
- return cloneRecord(existing);
40
- }
41
- const next = {
42
- ...existing,
43
- status: "resolved",
44
- resolution: cloneResolution(resolution),
45
- updatedAt: nowIso()
46
- };
47
- records.set(requestId, cloneRecord(next));
48
- return cloneRecord(next);
49
- },
50
- async delete(requestId) {
51
- records.delete(requestId);
52
- }
53
- };
54
- }
55
- function nowIso() {
56
- return (/* @__PURE__ */ new Date()).toISOString();
57
- }
58
- function cloneRecord(record) {
59
- return {
60
- ...record,
61
- request: structuredClone(record.request),
62
- ...record.target ? { target: cloneTarget(record.target) } : {},
63
- ...record.resolution ? { resolution: cloneResolution(record.resolution) } : {}
64
- };
65
- }
66
- function cloneTarget(target) {
67
- return { ...target };
68
- }
69
- function cloneResolution(resolution) {
70
- return structuredClone(resolution);
71
- }
72
-
73
- // src/interactive/postgres-store.ts
74
- var DEFAULT_TABLE = "channel_slack_interactive_requests";
75
- var DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
76
- var DEFAULT_PRUNE_BATCH_SIZE = 1e3;
77
- var DEFAULT_PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1e3;
78
- function createPostgresSlackInteractiveRequestStore({
79
- client,
80
- connectionString,
81
- ensureSchema = true,
82
- onPruneError,
83
- pruneBatchSize = DEFAULT_PRUNE_BATCH_SIZE,
84
- pruneIntervalMs = DEFAULT_PRUNE_INTERVAL_MS,
85
- retentionMs = DEFAULT_RETENTION_MS,
86
- schema,
87
- tableName = DEFAULT_TABLE
88
- }) {
89
- let activeClient = client;
90
- let ownsClient = false;
91
- let initialized;
92
- let pruneTimer;
93
- async function getClient() {
94
- if (activeClient) {
95
- return activeClient;
96
- }
97
- if (!connectionString) {
98
- throw new Error(
99
- "connectionString is required when a Postgres Slack interactive request client is not provided"
100
- );
101
- }
102
- const Pool = await importPostgresPoolConstructor();
103
- activeClient = new Pool({ connectionString });
104
- ownsClient = true;
105
- return activeClient;
106
- }
107
- async function ensureInitialized() {
108
- const currentClient = await getClient();
109
- initialized ??= initializePostgresSlackInteractiveRequestStore({
110
- client: currentClient,
111
- ensureSchema,
112
- schema,
113
- tableName
114
- });
115
- await initialized;
116
- startPruneTimer();
117
- return currentClient;
118
- }
119
- function startPruneTimer() {
120
- if (pruneTimer || pruneIntervalMs <= 0) {
121
- return;
122
- }
123
- pruneTimer = setInterval(() => {
124
- void prune().catch((error) => {
125
- onPruneError?.(error);
126
- });
127
- }, pruneIntervalMs);
128
- pruneTimer.unref?.();
129
- }
130
- async function prune() {
131
- const currentClient = await ensureInitialized();
132
- return prunePostgresSlackInteractiveRequestStore({
133
- client: currentClient,
134
- pruneBatchSize,
135
- retentionMs,
136
- schema,
137
- tableName
138
- });
139
- }
140
- return {
141
- async get(requestId) {
142
- const currentClient = await ensureInitialized();
143
- const result = await currentClient.query(
144
- `SELECT * FROM ${qualifiedTableName({
145
- schema,
146
- tableName
147
- })} WHERE id = $1::text LIMIT 1`,
148
- [requestId]
149
- );
150
- return rowToRecord(result.rows[0]);
151
- },
152
- async upsert(record) {
153
- const currentClient = await ensureInitialized();
154
- const result = await currentClient.query(
155
- `INSERT INTO ${qualifiedTableName({
156
- schema,
157
- tableName
158
- })} (
159
- id,
160
- kind,
161
- request,
162
- status,
163
- created_at,
164
- updated_at,
165
- target,
166
- resolution
167
- )
168
- VALUES (
169
- $1::text,
170
- $2::text,
171
- $3::jsonb,
172
- $4::text,
173
- $5::timestamptz,
174
- $6::timestamptz,
175
- $7::jsonb,
176
- $8::jsonb
177
- )
178
- ON CONFLICT (id) DO UPDATE SET
179
- kind = CASE
180
- WHEN ${qualifiedTableName({ schema, tableName })}.status = 'resolved'
181
- THEN ${qualifiedTableName({ schema, tableName })}.kind
182
- ELSE EXCLUDED.kind
183
- END,
184
- request = CASE
185
- WHEN ${qualifiedTableName({ schema, tableName })}.status = 'resolved'
186
- THEN ${qualifiedTableName({ schema, tableName })}.request
187
- ELSE EXCLUDED.request
188
- END,
189
- status = CASE
190
- WHEN ${qualifiedTableName({ schema, tableName })}.status = 'resolved'
191
- THEN ${qualifiedTableName({ schema, tableName })}.status
192
- ELSE EXCLUDED.status
193
- END,
194
- target = CASE
195
- WHEN ${qualifiedTableName({ schema, tableName })}.status = 'resolved'
196
- THEN ${qualifiedTableName({ schema, tableName })}.target
197
- ELSE COALESCE(EXCLUDED.target, ${qualifiedTableName({
198
- schema,
199
- tableName
200
- })}.target)
201
- END,
202
- resolution = CASE
203
- WHEN ${qualifiedTableName({ schema, tableName })}.status = 'resolved'
204
- THEN ${qualifiedTableName({ schema, tableName })}.resolution
205
- ELSE COALESCE(EXCLUDED.resolution, ${qualifiedTableName({
206
- schema,
207
- tableName
208
- })}.resolution)
209
- END,
210
- updated_at = CASE
211
- WHEN ${qualifiedTableName({ schema, tableName })}.status = 'resolved'
212
- THEN ${qualifiedTableName({ schema, tableName })}.updated_at
213
- ELSE now()
214
- END
215
- RETURNING *`,
216
- [
217
- record.id,
218
- record.kind,
219
- JSON.stringify(record.request),
220
- record.status,
221
- record.createdAt,
222
- record.updatedAt,
223
- record.target ? JSON.stringify(record.target) : null,
224
- record.resolution ? JSON.stringify(record.resolution) : null
225
- ]
226
- );
227
- return requireReturnedRecord(result.rows[0], record.id);
228
- },
229
- async attachTarget(requestId, target) {
230
- const currentClient = await ensureInitialized();
231
- const result = await currentClient.query(
232
- `UPDATE ${qualifiedTableName({ schema, tableName })}
233
- SET target = $2::jsonb, updated_at = now()
234
- WHERE id = $1::text
235
- RETURNING *`,
236
- [requestId, JSON.stringify(target)]
237
- );
238
- return rowToRecord(result.rows[0]);
239
- },
240
- async resolve(requestId, resolution) {
241
- const currentClient = await ensureInitialized();
242
- const result = await currentClient.query(
243
- `UPDATE ${qualifiedTableName({ schema, tableName })}
244
- SET status = 'resolved',
245
- resolution = $2::jsonb,
246
- updated_at = now()
247
- WHERE id = $1::text AND status <> 'resolved'
248
- RETURNING *`,
249
- [requestId, JSON.stringify(resolution)]
250
- );
251
- const resolved = rowToRecord(result.rows[0]);
252
- if (resolved) {
253
- return resolved;
254
- }
255
- const existing = await currentClient.query(
256
- `SELECT * FROM ${qualifiedTableName({
257
- schema,
258
- tableName
259
- })} WHERE id = $1::text LIMIT 1`,
260
- [requestId]
261
- );
262
- return rowToRecord(existing.rows[0]);
263
- },
264
- async delete(requestId) {
265
- const currentClient = await ensureInitialized();
266
- await currentClient.query(
267
- `DELETE FROM ${qualifiedTableName({
268
- schema,
269
- tableName
270
- })} WHERE id = $1::text`,
271
- [requestId]
272
- );
273
- },
274
- prune,
275
- async close() {
276
- if (pruneTimer) {
277
- clearInterval(pruneTimer);
278
- pruneTimer = void 0;
279
- }
280
- if (ownsClient) {
281
- await activeClient?.end?.();
282
- }
283
- }
284
- };
285
- }
286
- async function initializePostgresSlackInteractiveRequestStore({
287
- client,
288
- ensureSchema = true,
289
- schema,
290
- tableName = DEFAULT_TABLE
291
- }) {
292
- if (schema && ensureSchema) {
293
- await client.query(
294
- `CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(schema)}`
295
- );
296
- }
297
- const table = qualifiedTableName({ schema, tableName });
298
- await client.query(`
299
- CREATE TABLE IF NOT EXISTS ${table} (
300
- id text PRIMARY KEY,
301
- kind text NOT NULL CHECK (kind IN ('approval', 'human-input')),
302
- request jsonb NOT NULL,
303
- status text NOT NULL CHECK (status IN ('pending', 'resolved')),
304
- target jsonb,
305
- resolution jsonb,
306
- created_at timestamptz NOT NULL DEFAULT now(),
307
- updated_at timestamptz NOT NULL DEFAULT now()
308
- )
309
- `);
310
- const indexPrefix = interactiveIndexPrefix({ schema, tableName });
311
- await client.query(
312
- `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(
313
- `${indexPrefix}_status_updated_idx`
314
- )} ON ${table} (status, updated_at DESC)`
315
- );
316
- await client.query(
317
- `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(
318
- `${indexPrefix}_updated_idx`
319
- )} ON ${table} (updated_at DESC)`
320
- );
321
- }
322
- async function prunePostgresSlackInteractiveRequestStore({
323
- client,
324
- pruneBatchSize = DEFAULT_PRUNE_BATCH_SIZE,
325
- retentionMs = DEFAULT_RETENTION_MS,
326
- schema,
327
- tableName = DEFAULT_TABLE
328
- }) {
329
- if (retentionMs <= 0) {
330
- return { deleted: 0 };
331
- }
332
- const batchSize = Math.max(1, Math.floor(pruneBatchSize));
333
- const result = await client.query(
334
- `WITH expired AS (
335
- SELECT id
336
- FROM ${qualifiedTableName({ schema, tableName })}
337
- WHERE updated_at < now() - ($1::bigint * interval '1 millisecond')
338
- ORDER BY updated_at ASC
339
- LIMIT $2::integer
340
- )
341
- DELETE FROM ${qualifiedTableName({ schema, tableName })} target
342
- USING expired
343
- WHERE target.id = expired.id`,
344
- [Math.max(1, Math.floor(retentionMs)), batchSize]
345
- );
346
- return { deleted: result.rowCount ?? 0 };
347
- }
348
- function rowToRecord(row) {
349
- if (!row) return void 0;
350
- return cloneRecord({
351
- id: row.id,
352
- kind: row.kind,
353
- request: readJsonValue(
354
- row.request
355
- ),
356
- status: row.status,
357
- createdAt: toIsoString(row.created_at),
358
- updatedAt: toIsoString(row.updated_at),
359
- ...row.target ? {
360
- target: readJsonValue(
361
- row.target
362
- )
363
- } : {},
364
- ...row.resolution ? {
365
- resolution: readJsonValue(
366
- row.resolution
367
- )
368
- } : {}
369
- });
370
- }
371
- function requireReturnedRecord(row, requestId) {
372
- const record = rowToRecord(row);
373
- if (!record) {
374
- throw new Error(
375
- `Postgres Slack interactive request store did not return request ${requestId}.`
376
- );
377
- }
378
- return record;
379
- }
380
- function readJsonValue(value) {
381
- if (typeof value === "string") {
382
- return JSON.parse(value);
383
- }
384
- return value;
385
- }
386
- function toIsoString(value) {
387
- return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
388
- }
389
- function qualifiedTableName({
390
- schema,
391
- tableName
392
- }) {
393
- return schema ? `${quoteIdentifier(schema)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
394
- }
395
- function quoteIdentifier(value) {
396
- return `"${value.replace(/"/g, '""')}"`;
397
- }
398
- function interactiveIndexPrefix({
399
- schema,
400
- tableName
401
- }) {
402
- const raw = [schema, tableName].filter(Boolean).join("_");
403
- return raw.replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "channel_slack_interactive";
404
- }
405
- async function importPostgresPoolConstructor() {
406
- const dynamicImport = new Function(
407
- "specifier",
408
- "return import(specifier)"
409
- );
410
- try {
411
- const pg = await dynamicImport("pg");
412
- return pg.Pool;
413
- } catch (error) {
414
- throw new Error(
415
- `The "pg" package is required when using connectionString with createPostgresSlackInteractiveRequestStore. Install pg or pass a client. ${formatImportError(
416
- error
417
- )}`
418
- );
419
- }
420
- }
421
- function formatImportError(error) {
422
- return error instanceof Error ? error.message : String(error);
423
- }
424
-
425
- // src/interactive/blocks.ts
426
- var MAX_BLOCK_TEXT = 2800;
427
- function buildApprovalRequestMessage(request, actionIds) {
428
- const rememberScopes = request.rememberScopes ?? [];
429
- const canRemember = rememberScopes.length > 0 || Boolean(request.defaultRememberScope);
430
- const text = `Approval required for ${request.tool}`;
431
- return {
432
- text,
433
- blocks: [
434
- section(
435
- `*Approval required*
436
- ${escapeMrkdwn(request.description || request.tool)}`
437
- ),
438
- fields([
439
- `*Tool*
440
- ${escapeMrkdwn(request.tool)}`,
441
- `*Risk*
442
- ${escapeMrkdwn(request.risk)}`
443
- ]),
444
- section(`*Arguments*
445
- \`\`\`${truncate(formatArgs(request.args))}\`\`\``),
446
- {
447
- type: "actions",
448
- elements: [
449
- button("Allow", "primary", actionIds.approvalAllow, {
450
- requestId: request.id
451
- }),
452
- button("Deny", "danger", actionIds.approvalDeny, {
453
- requestId: request.id
454
- }),
455
- ...canRemember ? [
456
- button("Remember", void 0, actionIds.approvalRemember, {
457
- requestId: request.id,
458
- rememberScope: request.defaultRememberScope ?? rememberScopes[0]
459
- })
460
- ] : []
461
- ]
462
- }
463
- ]
464
- };
465
- }
466
- function buildHumanInputRequestMessage(request, actionIds) {
467
- const text = request.title || "Input required";
468
- const confirmLabel = request.confirmLabel ?? "Submit";
469
- const denyLabel = request.denyLabel ?? "Cancel";
470
- if (request.kind === "confirm") {
471
- return {
472
- text,
473
- blocks: [
474
- section(`*${escapeMrkdwn(text)}*
475
- ${escapeMrkdwn(request.question)}`),
476
- {
477
- type: "actions",
478
- elements: [
479
- button(confirmLabel, "primary", actionIds.humanConfirm, {
480
- requestId: request.id
481
- }),
482
- button(denyLabel, "danger", actionIds.humanDeny, {
483
- requestId: request.id
484
- })
485
- ]
486
- }
487
- ]
488
- };
489
- }
490
- return {
491
- text,
492
- blocks: [
493
- section(`*${escapeMrkdwn(text)}*
494
- ${escapeMrkdwn(request.question)}`),
495
- {
496
- type: "actions",
497
- elements: [
498
- button(confirmLabel, "primary", actionIds.humanOpen, {
499
- requestId: request.id
500
- }),
501
- button(denyLabel, "danger", actionIds.humanDeny, {
502
- requestId: request.id
503
- })
504
- ]
505
- }
506
- ]
507
- };
508
- }
509
- function buildResolvedMessage(label, resolution) {
510
- const text = resolution.kind === "approval" ? resolution.action === "deny" ? "Approval denied" : "Approval granted" : "Input submitted";
511
- return {
512
- text,
513
- blocks: [section(`*${escapeMrkdwn(text)}*
514
- ${escapeMrkdwn(label)}`)]
515
- };
516
- }
517
- function buildHumanInputModal(request, actionIds) {
518
- const title = truncatePlain(request.title || "Input required", 24);
519
- const submit = truncatePlain(request.confirmLabel ?? "Submit", 24);
520
- const close = truncatePlain(request.denyLabel ?? "Cancel", 24);
521
- if (request.kind === "choice") {
522
- const options = (request.options ?? []).slice(0, 100).map((option) => ({
523
- text: {
524
- type: "plain_text",
525
- text: truncatePlain(option.label, 75)
526
- },
527
- value: option.value ?? option.label,
528
- ...option.description ? {
529
- description: {
530
- type: "plain_text",
531
- text: truncatePlain(option.description, 75)
532
- }
533
- } : {}
534
- }));
535
- return {
536
- type: "modal",
537
- callback_id: actionIds.humanSubmit,
538
- private_metadata: JSON.stringify({ requestId: request.id }),
539
- title: { type: "plain_text", text: title },
540
- submit: { type: "plain_text", text: submit },
541
- close: { type: "plain_text", text: close },
542
- blocks: [
543
- {
544
- type: "input",
545
- block_id: "input",
546
- label: {
547
- type: "plain_text",
548
- text: truncatePlain(request.question, 200)
549
- },
550
- element: {
551
- type: request.allowMultiple ? "checkboxes" : "radio_buttons",
552
- action_id: "value",
553
- options
554
- }
555
- }
556
- ]
557
- };
558
- }
559
- return {
560
- type: "modal",
561
- callback_id: actionIds.humanSubmit,
562
- private_metadata: JSON.stringify({ requestId: request.id }),
563
- title: { type: "plain_text", text: title },
564
- submit: { type: "plain_text", text: submit },
565
- close: { type: "plain_text", text: close },
566
- blocks: [
567
- {
568
- type: "input",
569
- block_id: "input",
570
- label: {
571
- type: "plain_text",
572
- text: truncatePlain(request.question, 200)
573
- },
574
- element: {
575
- type: "plain_text_input",
576
- action_id: "value",
577
- multiline: true,
578
- ...request.placeholder ? {
579
- placeholder: {
580
- type: "plain_text",
581
- text: truncatePlain(request.placeholder, 150)
582
- }
583
- } : {}
584
- }
585
- }
586
- ]
587
- };
588
- }
589
- function encodeActionValue(payload) {
590
- return JSON.stringify(payload);
591
- }
592
- function decodeActionValue(value) {
593
- if (typeof value !== "string") return {};
594
- try {
595
- const parsed = JSON.parse(value);
596
- return parsed && typeof parsed === "object" ? parsed : {};
597
- } catch {
598
- return {};
599
- }
600
- }
601
- function button(text, style, actionId, value) {
602
- return {
603
- type: "button",
604
- text: { type: "plain_text", text },
605
- action_id: actionId,
606
- value: encodeActionValue(value),
607
- ...style ? { style } : {}
608
- };
609
- }
610
- function section(text) {
611
- return { type: "section", text: { type: "mrkdwn", text: truncate(text) } };
612
- }
613
- function fields(items) {
614
- return {
615
- type: "section",
616
- fields: items.map((text) => ({ type: "mrkdwn", text: truncate(text) }))
617
- };
618
- }
619
- function formatArgs(args) {
620
- if (typeof args === "string") return args;
621
- try {
622
- return JSON.stringify(args, null, 2);
623
- } catch {
624
- return String(args);
625
- }
626
- }
627
- function truncate(value, max = MAX_BLOCK_TEXT) {
628
- return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
629
- }
630
- function truncatePlain(value, max) {
631
- const normalized = value.trim() || "Input";
632
- return normalized.length <= max ? normalized : `${normalized.slice(0, max - 3)}...`;
633
- }
634
- function escapeMrkdwn(value) {
635
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
636
- }
637
-
638
- // src/interactive/controller.ts
639
- import { openSlackModal } from "@cuylabs/channel-slack/views";
640
- var DEFAULT_NAMESPACE = "agent_slack";
641
- var DEFAULT_REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
642
- var installedActionIds = /* @__PURE__ */ new WeakMap();
643
- function createSlackInteractiveController(options = {}) {
644
- const store = options.store ?? createInMemorySlackInteractiveRequestStore();
645
- const actionIds = resolveActionIds(
646
- options.namespace ?? DEFAULT_NAMESPACE,
647
- options.actionIds
648
- );
649
- const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
650
- const waiters = /* @__PURE__ */ new Map();
651
- const pendingIds = /* @__PURE__ */ new Set();
652
- async function ensurePending(kind, request) {
653
- const existing = await store.get(request.id);
654
- if (existing) {
655
- pendingIds.add(request.id);
656
- return existing;
657
- }
658
- const createdAt = nowIso();
659
- const record = await store.upsert({
660
- id: request.id,
661
- kind,
662
- request,
663
- status: "pending",
664
- createdAt,
665
- updatedAt: createdAt
666
- });
667
- pendingIds.add(request.id);
668
- return record;
669
- }
670
- async function waitForResolution(kind, request, waitOptions = {}) {
671
- const existing = await ensurePending(kind, request);
672
- if (existing.status === "resolved" && existing.resolution) {
673
- return existing.resolution;
674
- }
675
- if (waiters.has(request.id)) {
676
- throw new Error(
677
- `Slack interactive request is already waiting: ${request.id}. Resolve or cancel the in-flight request before requesting again.`
678
- );
679
- }
680
- return await new Promise((resolve, reject) => {
681
- const cleanupCallbacks = [];
682
- const timeoutMs = waitOptions.timeoutMs ?? requestTimeoutMs;
683
- if (timeoutMs > 0) {
684
- const timeoutId = setTimeout(() => {
685
- void cancel(request.id, "Slack interactive request timed out.");
686
- }, timeoutMs);
687
- cleanupCallbacks.push(() => clearTimeout(timeoutId));
688
- }
689
- let abortImmediately = false;
690
- if (waitOptions.signal) {
691
- const onAbort = () => {
692
- void cancel(request.id, "Slack interactive request aborted.");
693
- };
694
- if (waitOptions.signal.aborted) {
695
- abortImmediately = true;
696
- } else {
697
- waitOptions.signal.addEventListener("abort", onAbort, {
698
- once: true
699
- });
700
- cleanupCallbacks.push(
701
- () => waitOptions.signal?.removeEventListener("abort", onAbort)
702
- );
703
- }
704
- }
705
- waiters.set(request.id, {
706
- resolve,
707
- reject,
708
- cleanup: () => {
709
- for (const cleanup of cleanupCallbacks.splice(0)) {
710
- cleanup();
711
- }
712
- }
713
- });
714
- if (abortImmediately) {
715
- void cancel(request.id, "Slack interactive request aborted.");
716
- }
717
- });
718
- }
719
- async function resolveRequest(requestId, resolution) {
720
- const record = await store.resolve(requestId, resolution);
721
- const waiter = waiters.get(requestId);
722
- if (waiter) {
723
- waiters.delete(requestId);
724
- waiter.cleanup();
725
- waiter.resolve(resolution);
726
- }
727
- if (record) {
728
- pendingIds.delete(requestId);
729
- await options.onResolve?.(requestId, resolution);
730
- }
731
- return record;
732
- }
733
- async function cancel(requestId, reason = "Cancelled") {
734
- const waiter = waiters.get(requestId);
735
- if (waiter) {
736
- waiters.delete(requestId);
737
- waiter.cleanup();
738
- waiter.reject(new Error(reason));
739
- }
740
- const existing = await store.get(requestId);
741
- if (existing?.status === "pending") {
742
- await store.delete(requestId);
743
- pendingIds.delete(requestId);
744
- return true;
745
- }
746
- pendingIds.delete(requestId);
747
- return Boolean(waiter);
748
- }
749
- async function cancelAll(reason = "Cancelled") {
750
- await Promise.all(
751
- [...pendingIds].map((requestId) => cancel(requestId, reason))
752
- );
753
- }
754
- async function handleInteractiveRequest(context) {
755
- const request = context.request;
756
- const record = await ensurePending(context.kind, request);
757
- if (record.target) {
758
- return true;
759
- }
760
- const message = context.kind === "approval" ? buildApprovalRequestMessage(
761
- request,
762
- actionIds
763
- ) : buildHumanInputRequestMessage(
764
- request,
765
- actionIds
766
- );
767
- const ref = await context.responder.postMessage(message);
768
- await store.attachTarget(request.id, {
769
- channel: ref.channel,
770
- ts: ref.ts,
771
- userId: context.user.userId,
772
- teamId: context.user.teamId,
773
- ...context.slackActivity.threadTs ? { threadTs: context.slackActivity.threadTs } : {}
774
- });
775
- return true;
776
- }
777
- function install(app) {
778
- assertActionIdsCanInstall(app, actionIds);
779
- app.action(actionIds.approvalAllow, async (args) => {
780
- await handleAction(args, {
781
- kind: "approval",
782
- action: "allow"
783
- });
784
- });
785
- app.action(actionIds.approvalDeny, async (args) => {
786
- await handleAction(args, {
787
- kind: "approval",
788
- action: "deny"
789
- });
790
- });
791
- app.action(actionIds.approvalRemember, async (args) => {
792
- const value = firstActionValue(args);
793
- const rememberScope = typeof value.rememberScope === "string" ? value.rememberScope : void 0;
794
- await handleAction(args, {
795
- kind: "approval",
796
- action: "remember",
797
- ...rememberScope ? { rememberScope } : {}
798
- });
799
- });
800
- app.action(actionIds.humanConfirm, async (args) => {
801
- await handleAction(args, {
802
- kind: "human-input",
803
- response: { kind: "confirm", confirmed: true, text: "Confirmed" }
804
- });
805
- });
806
- app.action(actionIds.humanDeny, async (args) => {
807
- await handleAction(args, {
808
- kind: "human-input",
809
- response: { kind: "confirm", confirmed: false, text: "Cancelled" }
810
- });
811
- });
812
- app.action(actionIds.humanOpen, async (args) => {
813
- await openHumanInputModal(args);
814
- });
815
- app.view(actionIds.humanSubmit, async (args) => {
816
- await submitHumanInputModal(args);
817
- });
818
- }
819
- async function handleAction(args, resolutionInput) {
820
- const actionArgs = args;
821
- await actionArgs.ack();
822
- const requestId = extractRequestId(firstActionValue(args));
823
- if (!requestId) return;
824
- const record = await store.get(requestId);
825
- if (!record || record.status === "resolved") {
826
- return;
827
- }
828
- const actor = extractActor(actionArgs.body);
829
- if (!await isAuthorized(record, actor)) {
830
- await postEphemeral(
831
- actionArgs,
832
- "Only the original requester can resolve this request."
833
- );
834
- return;
835
- }
836
- const resolution = resolutionInput;
837
- const resolved = await resolveRequest(requestId, resolution);
838
- if (resolved?.target) {
839
- await updateOriginalMessage(actionArgs, resolved, resolution);
840
- }
841
- }
842
- async function openHumanInputModal(args) {
843
- const actionArgs = args;
844
- await actionArgs.ack();
845
- const requestId = extractRequestId(firstActionValue(args));
846
- if (!requestId || !actionArgs.body.trigger_id) return;
847
- const record = await store.get(requestId);
848
- if (!record || record.kind !== "human-input") return;
849
- const actor = extractActor(actionArgs.body);
850
- if (!await isAuthorized(record, actor)) {
851
- await postEphemeral(
852
- actionArgs,
853
- "Only the original requester can answer this request."
854
- );
855
- return;
856
- }
857
- await openSlackModal({
858
- client: actionArgs.client,
859
- triggerId: actionArgs.body.trigger_id,
860
- view: buildHumanInputModal(
861
- record.request,
862
- actionIds
863
- )
864
- });
865
- }
866
- async function submitHumanInputModal(args) {
867
- const viewArgs = args;
868
- await viewArgs.ack();
869
- const requestId = extractRequestIdFromView(viewArgs.view);
870
- if (!requestId) return;
871
- const record = await store.get(requestId);
872
- if (!record || record.kind !== "human-input" || record.status === "resolved") {
873
- return;
874
- }
875
- const actor = extractActor(viewArgs.body);
876
- if (!await isAuthorized(record, actor)) {
877
- return;
878
- }
879
- const response = responseFromView(
880
- record.request,
881
- viewArgs.view
882
- );
883
- const resolution = {
884
- kind: "human-input",
885
- response
886
- };
887
- const resolved = await resolveRequest(requestId, resolution);
888
- if (resolved?.target) {
889
- await viewArgs.client.chat.update({
890
- channel: resolved.target.channel,
891
- ts: resolved.target.ts,
892
- ...buildResolvedMessage("Slack response received.", resolution)
893
- });
894
- }
895
- }
896
- async function isAuthorized(record, actor) {
897
- if (options.authorize) {
898
- return await options.authorize(record, actor);
899
- }
900
- return record.target?.userId === actor.userId;
901
- }
902
- async function updateOriginalMessage(args, record, resolution) {
903
- const target = record.target;
904
- if (!target) return;
905
- const label = resolution.kind === "approval" ? `${resolution.action} selected.` : resolution.response.text;
906
- await args.client.chat.update({
907
- channel: target.channel,
908
- ts: target.ts,
909
- ...buildResolvedMessage(label, resolution)
910
- });
911
- }
912
- return {
913
- actionIds,
914
- store,
915
- approval: {
916
- async onRequest(request, options2) {
917
- const resolution = await waitForResolution(
918
- "approval",
919
- request,
920
- options2
921
- );
922
- if (resolution.kind !== "approval") {
923
- throw new Error(
924
- `Unexpected human-input resolution for ${request.id}.`
925
- );
926
- }
927
- return {
928
- action: resolution.action,
929
- ...resolution.feedback ? { feedback: resolution.feedback } : {},
930
- ...resolution.rememberScope ? { rememberScope: resolution.rememberScope } : {}
931
- };
932
- }
933
- },
934
- humanInput: {
935
- async onRequest(request, options2) {
936
- const resolution = await waitForResolution(
937
- "human-input",
938
- request,
939
- options2
940
- );
941
- if (resolution.kind !== "human-input") {
942
- throw new Error(`Unexpected approval resolution for ${request.id}.`);
943
- }
944
- return resolution.response;
945
- }
946
- },
947
- cancel,
948
- cancelAll,
949
- handleInteractiveRequest,
950
- install
951
- };
952
- }
953
- function resolveActionIds(namespace, overrides) {
954
- const prefix = normalizeActionIdNamespace(namespace);
955
- return {
956
- approvalAllow: `${prefix}_approval_allow`,
957
- approvalDeny: `${prefix}_approval_deny`,
958
- approvalRemember: `${prefix}_approval_remember`,
959
- humanConfirm: `${prefix}_human_confirm`,
960
- humanDeny: `${prefix}_human_deny`,
961
- humanOpen: `${prefix}_human_open`,
962
- humanSubmit: `${prefix}_human_submit`,
963
- ...overrides ?? {}
964
- };
965
- }
966
- function normalizeActionIdNamespace(namespace) {
967
- const trimmed = namespace.trim();
968
- if (!trimmed) {
969
- throw new Error("Slack interactive action namespace cannot be empty.");
970
- }
971
- return trimmed;
972
- }
973
- function assertActionIdsCanInstall(app, actionIds) {
974
- const ids = Object.values(actionIds);
975
- const duplicateWithinController = ids.find(
976
- (id, index) => ids.indexOf(id) !== index
977
- );
978
- if (duplicateWithinController) {
979
- throw new Error(
980
- `Duplicate Slack interactive action id configured: ${duplicateWithinController}`
981
- );
982
- }
983
- const appKey = app;
984
- const installed = installedActionIds.get(appKey) ?? /* @__PURE__ */ new Set();
985
- const duplicate = ids.find((id) => installed.has(id));
986
- if (duplicate) {
987
- throw new Error(
988
- `Slack interactive action id '${duplicate}' is already installed on this Bolt app. Provide a unique createSlackInteractiveController({ namespace }) or actionIds config.`
989
- );
990
- }
991
- for (const id of ids) {
992
- installed.add(id);
993
- }
994
- installedActionIds.set(appKey, installed);
995
- }
996
- function firstActionValue(args) {
997
- const body = args.body;
998
- return decodeActionValue(body.actions?.[0]?.value);
999
- }
1000
- function extractRequestId(value) {
1001
- return typeof value.requestId === "string" && value.requestId.length > 0 ? value.requestId : void 0;
1002
- }
1003
- function extractRequestIdFromView(view) {
1004
- return extractRequestId(decodeActionValue(view.private_metadata));
1005
- }
1006
- function extractActor(body) {
1007
- return {
1008
- userId: body.user?.id ?? "unknown",
1009
- ...body.user?.team_id ?? body.team?.id ? { teamId: body.user?.team_id ?? body.team?.id } : {}
1010
- };
1011
- }
1012
- async function postEphemeral(args, text) {
1013
- const channel = args.body.channel?.id;
1014
- const user = args.body.user?.id;
1015
- if (!channel || !user || !args.client.chat.postEphemeral) return;
1016
- await args.client.chat.postEphemeral({ channel, user, text });
1017
- }
1018
- function responseFromView(request, view) {
1019
- const input = view.state?.values?.input?.value;
1020
- if (request.kind === "choice") {
1021
- const selected = input?.selected_options?.map((option) => option.value ?? "").filter(Boolean) ?? (input?.selected_option?.value ? [input.selected_option.value] : []);
1022
- return {
1023
- kind: "choice",
1024
- selected,
1025
- text: selected.join(", ")
1026
- };
1027
- }
1028
- const text = input?.value ?? "";
1029
- return { kind: "text", text };
1030
- }
1031
-
1032
- export {
1033
- createInMemorySlackInteractiveRequestStore,
1034
- nowIso,
1035
- cloneRecord,
1036
- createPostgresSlackInteractiveRequestStore,
1037
- initializePostgresSlackInteractiveRequestStore,
1038
- prunePostgresSlackInteractiveRequestStore,
1039
- buildApprovalRequestMessage,
1040
- buildHumanInputRequestMessage,
1041
- buildResolvedMessage,
1042
- buildHumanInputModal,
1043
- encodeActionValue,
1044
- decodeActionValue,
1045
- createSlackInteractiveController
1046
- };