@open-everywhere/provider-slack 0.6.0 → 0.8.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 (141) hide show
  1. package/agent/access/slack-attachment-fetch.ts +2 -1
  2. package/agent/app-manifest.ts +15 -1
  3. package/agent/channels/slack-agent-session.ts +249 -0
  4. package/agent/channels/slack-attachments.ts +20 -11
  5. package/agent/channels/slack-bot-ask.ts +101 -0
  6. package/agent/channels/slack-commands.ts +178 -17
  7. package/agent/channels/slack-context.ts +0 -5
  8. package/agent/channels/slack-events.ts +110 -33
  9. package/agent/channels/slack-follow-ups.ts +148 -0
  10. package/agent/channels/slack-friendly-names.ts +55 -0
  11. package/agent/channels/slack-front.ts +262 -27
  12. package/agent/channels/slack-home.ts +9 -3
  13. package/agent/channels/slack-identity.ts +9 -1
  14. package/agent/channels/slack-memory-canvas.ts +197 -54
  15. package/agent/channels/slack-origin-codec.ts +9 -0
  16. package/agent/channels/slack-post-arm.ts +68 -11
  17. package/agent/channels/slack-read.ts +27 -2
  18. package/agent/channels/slack-reply-blocks.ts +180 -0
  19. package/agent/channels/slack-reply-feedback.ts +75 -0
  20. package/agent/channels/slack-shortcut.ts +1 -1
  21. package/agent/channels/slack-voice-clips.ts +82 -0
  22. package/agent/channels/slack-wake-backscroll.ts +0 -5
  23. package/agent/channels/slack-web-api.ts +4 -2
  24. package/agent/channels/slack.ts +81 -19
  25. package/agent/docs-generated.ts +1282 -2
  26. package/agent/ops.ts +14 -21
  27. package/agent/register.ts +25 -0
  28. package/agent/setup/credentials.ts +17 -0
  29. package/agent/setup/page.ts +6 -3
  30. package/agent/surface-parity.ts +2 -0
  31. package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -1
  32. package/dist/agent/access/slack-attachment-fetch.js.map +1 -1
  33. package/dist/agent/app-manifest.d.ts.map +1 -1
  34. package/dist/agent/app-manifest.js +15 -1
  35. package/dist/agent/app-manifest.js.map +1 -1
  36. package/dist/agent/channels/slack-agent-session.d.ts +81 -0
  37. package/dist/agent/channels/slack-agent-session.d.ts.map +1 -0
  38. package/dist/agent/channels/slack-agent-session.js +198 -0
  39. package/dist/agent/channels/slack-agent-session.js.map +1 -0
  40. package/dist/agent/channels/slack-attachments.d.ts +11 -0
  41. package/dist/agent/channels/slack-attachments.d.ts.map +1 -1
  42. package/dist/agent/channels/slack-attachments.js +21 -14
  43. package/dist/agent/channels/slack-attachments.js.map +1 -1
  44. package/dist/agent/channels/slack-bot-ask.d.ts +30 -0
  45. package/dist/agent/channels/slack-bot-ask.d.ts.map +1 -0
  46. package/dist/agent/channels/slack-bot-ask.js +91 -0
  47. package/dist/agent/channels/slack-bot-ask.js.map +1 -0
  48. package/dist/agent/channels/slack-commands.d.ts +113 -10
  49. package/dist/agent/channels/slack-commands.d.ts.map +1 -1
  50. package/dist/agent/channels/slack-commands.js +151 -18
  51. package/dist/agent/channels/slack-commands.js.map +1 -1
  52. package/dist/agent/channels/slack-context.d.ts +1 -2
  53. package/dist/agent/channels/slack-context.d.ts.map +1 -1
  54. package/dist/agent/channels/slack-context.js +0 -4
  55. package/dist/agent/channels/slack-context.js.map +1 -1
  56. package/dist/agent/channels/slack-events.d.ts +47 -97
  57. package/dist/agent/channels/slack-events.d.ts.map +1 -1
  58. package/dist/agent/channels/slack-events.js +90 -22
  59. package/dist/agent/channels/slack-events.js.map +1 -1
  60. package/dist/agent/channels/slack-follow-ups.d.ts +44 -0
  61. package/dist/agent/channels/slack-follow-ups.d.ts.map +1 -0
  62. package/dist/agent/channels/slack-follow-ups.js +129 -0
  63. package/dist/agent/channels/slack-follow-ups.js.map +1 -0
  64. package/dist/agent/channels/slack-friendly-names.d.ts +10 -0
  65. package/dist/agent/channels/slack-friendly-names.d.ts.map +1 -0
  66. package/dist/agent/channels/slack-friendly-names.js +45 -0
  67. package/dist/agent/channels/slack-friendly-names.js.map +1 -0
  68. package/dist/agent/channels/slack-front.d.ts +38 -46
  69. package/dist/agent/channels/slack-front.d.ts.map +1 -1
  70. package/dist/agent/channels/slack-front.js +230 -23
  71. package/dist/agent/channels/slack-front.js.map +1 -1
  72. package/dist/agent/channels/slack-home.d.ts.map +1 -1
  73. package/dist/agent/channels/slack-home.js +10 -3
  74. package/dist/agent/channels/slack-home.js.map +1 -1
  75. package/dist/agent/channels/slack-identity.d.ts +7 -21
  76. package/dist/agent/channels/slack-identity.d.ts.map +1 -1
  77. package/dist/agent/channels/slack-identity.js +3 -1
  78. package/dist/agent/channels/slack-identity.js.map +1 -1
  79. package/dist/agent/channels/slack-memory-canvas.d.ts +21 -5
  80. package/dist/agent/channels/slack-memory-canvas.d.ts.map +1 -1
  81. package/dist/agent/channels/slack-memory-canvas.js +166 -55
  82. package/dist/agent/channels/slack-memory-canvas.js.map +1 -1
  83. package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -1
  84. package/dist/agent/channels/slack-origin-codec.js +10 -0
  85. package/dist/agent/channels/slack-origin-codec.js.map +1 -1
  86. package/dist/agent/channels/slack-post-arm.d.ts +21 -1
  87. package/dist/agent/channels/slack-post-arm.d.ts.map +1 -1
  88. package/dist/agent/channels/slack-post-arm.js +57 -10
  89. package/dist/agent/channels/slack-post-arm.js.map +1 -1
  90. package/dist/agent/channels/slack-read.d.ts +11 -1
  91. package/dist/agent/channels/slack-read.d.ts.map +1 -1
  92. package/dist/agent/channels/slack-read.js +27 -3
  93. package/dist/agent/channels/slack-read.js.map +1 -1
  94. package/dist/agent/channels/slack-reply-blocks.d.ts +29 -0
  95. package/dist/agent/channels/slack-reply-blocks.d.ts.map +1 -0
  96. package/dist/agent/channels/slack-reply-blocks.js +172 -0
  97. package/dist/agent/channels/slack-reply-blocks.js.map +1 -0
  98. package/dist/agent/channels/slack-reply-feedback.d.ts +23 -0
  99. package/dist/agent/channels/slack-reply-feedback.d.ts.map +1 -0
  100. package/dist/agent/channels/slack-reply-feedback.js +63 -0
  101. package/dist/agent/channels/slack-reply-feedback.js.map +1 -0
  102. package/dist/agent/channels/slack-shortcut.d.ts +10 -0
  103. package/dist/agent/channels/slack-shortcut.d.ts.map +1 -1
  104. package/dist/agent/channels/slack-shortcut.js +1 -1
  105. package/dist/agent/channels/slack-shortcut.js.map +1 -1
  106. package/dist/agent/channels/slack-voice-clips.d.ts +16 -0
  107. package/dist/agent/channels/slack-voice-clips.d.ts.map +1 -0
  108. package/dist/agent/channels/slack-voice-clips.js +80 -0
  109. package/dist/agent/channels/slack-voice-clips.js.map +1 -0
  110. package/dist/agent/channels/slack-wake-backscroll.d.ts +0 -2
  111. package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -1
  112. package/dist/agent/channels/slack-wake-backscroll.js +0 -4
  113. package/dist/agent/channels/slack-wake-backscroll.js.map +1 -1
  114. package/dist/agent/channels/slack-web-api.d.ts.map +1 -1
  115. package/dist/agent/channels/slack-web-api.js +4 -2
  116. package/dist/agent/channels/slack-web-api.js.map +1 -1
  117. package/dist/agent/channels/slack.d.ts +22 -1
  118. package/dist/agent/channels/slack.d.ts.map +1 -1
  119. package/dist/agent/channels/slack.js +70 -16
  120. package/dist/agent/channels/slack.js.map +1 -1
  121. package/dist/agent/docs-generated.d.ts.map +1 -1
  122. package/dist/agent/docs-generated.js +1282 -2
  123. package/dist/agent/docs-generated.js.map +1 -1
  124. package/dist/agent/ops.d.ts +1 -1
  125. package/dist/agent/ops.d.ts.map +1 -1
  126. package/dist/agent/ops.js +15 -21
  127. package/dist/agent/ops.js.map +1 -1
  128. package/dist/agent/register.d.ts.map +1 -1
  129. package/dist/agent/register.js +22 -1
  130. package/dist/agent/register.js.map +1 -1
  131. package/dist/agent/setup/credentials.d.ts +12 -0
  132. package/dist/agent/setup/credentials.d.ts.map +1 -1
  133. package/dist/agent/setup/credentials.js +17 -0
  134. package/dist/agent/setup/credentials.js.map +1 -1
  135. package/dist/agent/setup/page.d.ts.map +1 -1
  136. package/dist/agent/setup/page.js +6 -3
  137. package/dist/agent/setup/page.js.map +1 -1
  138. package/dist/agent/surface-parity.d.ts.map +1 -1
  139. package/dist/agent/surface-parity.js +2 -0
  140. package/dist/agent/surface-parity.js.map +1 -1
  141. package/package.json +40 -3
@@ -0,0 +1,148 @@
1
+ // slack-follow-ups — a press on a suggested follow-up under an agent answer (OPE-2516).
2
+ //
3
+ // An answer in a 1:1 DM (the side panel included) can carry up to three follow-up buttons
4
+ // (slack-reply-blocks.ts). Slack sends a press to the interactivity URL as a signed `block_actions`
5
+ // payload whose action id starts `oe_followup:` and whose value is the button's text. The front route
6
+ // classifies it (`slack-front.ts`, FrontDecision `followup-click`), runs the tenant fence, and then:
7
+ // 1. marks the answer as answered: `chat.update` removes the button row and adds one line,
8
+ // "↳ You asked: <text>", so the conversation shows what the person sent and no second press
9
+ // can follow ({@link markFollowUpPressed});
10
+ // 2. wakes the DM seat with the text as the person's next message ({@link followUpMessageEvent}),
11
+ // through the SAME `scheduleDmWake` road a typed DM takes — every gate on that road applies.
12
+ //
13
+ // 🔴 ONLY A 1:1 DM. A press counts only in a `D…` conversation, where the app and ONE person are the
14
+ // only members, so the presser is the person the answer addressed. The choke point shows the buttons
15
+ // nowhere else; this check is the backstop for a payload that names another conversation.
16
+ //
17
+ // 🔴 THE TEXT IS THE BUTTON'S OWN LABEL. Slack signs the payload and sends a `block_actions` only for
18
+ // a block this app posted, so the value is the text this app wrote under the answer and the person
19
+ // saw on the button. Core drops any follow-up too long to show whole, or holding a character that
20
+ // draws nothing (`reply-follow-ups.ts`), and the parse below refuses both again.
21
+
22
+ import type { ParsedMessageEvent } from "./slack-events.js";
23
+ import { slackForm } from "./slack-events.js";
24
+ import { SLACK_FOLLOW_UP_ACTION_PREFIX, SLACK_FOLLOW_UP_BLOCK_ID, SLACK_INVISIBLE_CHARACTER } from "./slack-reply-blocks.js";
25
+ import { slackTsFromMs } from "./slack-shortcut.js";
26
+ import { slackInteractivityTenantIds } from "./slack-tenant-fence.js";
27
+
28
+ /** The longest value a press may carry: core's follow-up cap. A longer value is not one of ours. */
29
+ const FOLLOW_UP_VALUE_MAX = 75;
30
+
31
+ /** One parsed press on a follow-up button. IDS and the button's text only. */
32
+ export interface ParsedSlackFollowUpClick {
33
+ readonly text: string;
34
+ readonly userId: string;
35
+ readonly channelId: string;
36
+ /** The answer the button sits under. */
37
+ readonly messageTs: string;
38
+ /** The answer's thread, when the answer sits in one (a side-panel or agent-session thread). */
39
+ readonly threadTs?: string;
40
+ readonly teamId?: string;
41
+ readonly enterpriseId?: string;
42
+ /** The answer's blocks as Slack sent them back, so the press can rewrite the message. */
43
+ readonly blocks?: readonly unknown[];
44
+ /** The answer's notification text, kept on the rewrite. */
45
+ readonly messageText?: string;
46
+ }
47
+
48
+ const obj = (v: unknown): Record<string, unknown> | undefined => (typeof v === "object" && v !== null ? (v as Record<string, unknown>) : undefined);
49
+ const str = (v: unknown): string | undefined => (typeof v === "string" && v.length > 0 ? v : undefined);
50
+
51
+ /**
52
+ * Parse a decoded interactivity payload into a follow-up press, or `undefined` when it is not one: not
53
+ * a `block_actions`, no `oe_followup:` action, an empty or over-long value, a conversation that is not
54
+ * a 1:1 DM, or a missing user, channel or message. Pure and total: it never throws.
55
+ */
56
+ export function parseSlackFollowUpClick(payload: unknown): ParsedSlackFollowUpClick | undefined {
57
+ const p = obj(payload);
58
+ if (p?.type !== "block_actions" || !Array.isArray(p.actions)) return undefined;
59
+ const action = (p.actions as unknown[]).map(obj).find((a) => typeof a?.action_id === "string" && a.action_id.startsWith(SLACK_FOLLOW_UP_ACTION_PREFIX));
60
+ if (action === undefined) return undefined;
61
+ const text = str(action.value)?.trim();
62
+ if (text === undefined || text.length === 0 || Array.from(text).length > FOLLOW_UP_VALUE_MAX) return undefined;
63
+ // A value with a control or format character holds words no button shows, so it is not one of ours.
64
+ if (SLACK_INVISIBLE_CHARACTER.test(text)) return undefined;
65
+ const userId = str(obj(p.user)?.id);
66
+ const channelId = str(obj(p.channel)?.id) ?? str(obj(p.container)?.channel_id);
67
+ const message = obj(p.message);
68
+ const messageTs = str(message?.ts) ?? str(obj(p.container)?.message_ts);
69
+ if (userId === undefined || channelId === undefined || messageTs === undefined) return undefined;
70
+ if (!channelId.startsWith("D")) return undefined;
71
+ const threadTs = str(message?.thread_ts) ?? str(obj(p.container)?.thread_ts);
72
+ const { teamId, enterpriseId } = slackInteractivityTenantIds(p);
73
+ const blocks = Array.isArray(message?.blocks) ? (message.blocks as unknown[]) : undefined;
74
+ const messageText = str(message?.text);
75
+ return {
76
+ text,
77
+ userId,
78
+ channelId,
79
+ messageTs,
80
+ // A top-level answer names itself as its own thread root on some payloads: that is no thread.
81
+ ...(threadTs !== undefined && threadTs !== messageTs ? { threadTs } : {}),
82
+ ...(teamId === undefined ? {} : { teamId }),
83
+ ...(enterpriseId === undefined ? {} : { enterpriseId }),
84
+ ...(blocks === undefined ? {} : { blocks }),
85
+ ...(messageText === undefined ? {} : { messageText }),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The press as the person's next DM message. The ts is minted from the clock, as a global shortcut's
91
+ * is, because no Slack message stands behind the press. The thread is the answer's thread, so the
92
+ * answer to the follow-up lands where the answer it follows sits.
93
+ */
94
+ export function followUpMessageEvent(click: ParsedSlackFollowUpClick, nowMs: number = Date.now()): ParsedMessageEvent {
95
+ return {
96
+ channelId: click.channelId,
97
+ ts: slackTsFromMs(nowMs),
98
+ ...(click.threadTs === undefined ? {} : { threadTs: click.threadTs }),
99
+ ...(click.teamId === undefined ? {} : { teamId: click.teamId }),
100
+ ...(click.enterpriseId === undefined ? {} : { enterpriseId: click.enterpriseId }),
101
+ userId: click.userId,
102
+ channelType: "im",
103
+ text: click.text,
104
+ // The person chose these words by pressing them, so they are the person's own text.
105
+ authoredText: click.text,
106
+ isAppMention: false,
107
+ };
108
+ }
109
+
110
+ /** The dedupe key of one press: one answer takes one press, so a double press or a redelivery wakes once. */
111
+ export function followUpPressKey(click: ParsedSlackFollowUpClick): string {
112
+ return `slack-followup:${click.channelId}:${click.messageTs}`;
113
+ }
114
+
115
+ /** The line that replaces the buttons once the person pressed one. Plain text: it draws no mention. */
116
+ export function followUpPressedLine(text: string): string {
117
+ return `↳ You asked: ${text}`;
118
+ }
119
+
120
+ /**
121
+ * The answer's blocks after a press: the follow-up row removed, and one context line naming what the
122
+ * person sent. `undefined` when the blocks hold no follow-up row — then there is nothing to rewrite.
123
+ */
124
+ export function blocksAfterFollowUp(blocks: readonly unknown[], text: string): unknown[] | undefined {
125
+ const kept = blocks.filter((b) => obj(b)?.block_id !== SLACK_FOLLOW_UP_BLOCK_ID);
126
+ if (kept.length === blocks.length) return undefined;
127
+ return [...kept, { type: "context", elements: [{ type: "plain_text", text: followUpPressedLine(text), emoji: true }] }];
128
+ }
129
+
130
+ /**
131
+ * Rewrite the answer after a press (see the header). Answers whether Slack took the rewrite. Never
132
+ * throws: a refused rewrite costs only the line, never the person's message, which wakes regardless.
133
+ */
134
+ export async function markFollowUpPressed(
135
+ click: ParsedSlackFollowUpClick,
136
+ form: (method: string, body: string) => Promise<Record<string, unknown> | undefined> = slackForm,
137
+ ): Promise<boolean> {
138
+ try {
139
+ const blocks = click.blocks === undefined ? undefined : blocksAfterFollowUp(click.blocks, click.text);
140
+ if (blocks === undefined) return false;
141
+ const body = new URLSearchParams({ channel: click.channelId, ts: click.messageTs, blocks: JSON.stringify(blocks) });
142
+ if (click.messageText !== undefined) body.set("text", click.messageText);
143
+ const res = await form("chat.update", body.toString());
144
+ return res?.ok === true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
@@ -0,0 +1,55 @@
1
+ // slack-friendly-names — the Slack name resolver for the friendly-names guard (OPE-2482).
2
+ //
3
+ // The guard (core `channels/friendly-outbound.ts`) asks this resolver for a display name before it
4
+ // renders a raw Slack id in text a person reads. A member id answers the person's display name, a
5
+ // channel id answers `#name`, and the workspace id answers the install's recorded team name. Every
6
+ // lookup rides a cache that already exists (`users.info`, `conversations.info`), so a name costs one
7
+ // Slack call per id per instance. Any failure answers undefined: the id then renders as a mention or
8
+ // plain words, never as itself.
9
+
10
+ import type { FriendlyNameResolver } from "@open-everywhere/core/agent/adapter-api.js";
11
+ import { fetchChannelDisplayName, fetchUserDisplayName } from "./slack-events.js";
12
+ import { slackInstallState } from "../setup/install-state.js";
13
+
14
+ /** The lookups the resolver uses. Live code passes none; a test injects each. */
15
+ export interface SlackFriendlyNameDeps {
16
+ readonly userName?: (id: string) => Promise<string | undefined>;
17
+ readonly channelName?: (id: string) => Promise<string | undefined>;
18
+ readonly teamName?: (id: string) => Promise<string | undefined>;
19
+ }
20
+
21
+ // The install record is read at most once per TEAM_NAME_TTL_MS, and only a FOUND name is kept: a post
22
+ // path must not read the store per message, and a deployment that installs (or renames its workspace)
23
+ // after boot must still be named without a restart.
24
+ const TEAM_NAME_TTL_MS = 30 * 60_000;
25
+ let teamNameMemo: { readonly teamId: string; readonly name: string; readonly at: number } | undefined;
26
+
27
+ /** The workspace name, when the recorded install is that workspace. */
28
+ async function liveTeamName(id: string): Promise<string | undefined> {
29
+ if (teamNameMemo && teamNameMemo.teamId === id && Date.now() - teamNameMemo.at < TEAM_NAME_TTL_MS) return teamNameMemo.name;
30
+ const install = await slackInstallState().catch(() => undefined);
31
+ if (install?.teamId === id && install.teamName) {
32
+ teamNameMemo = { teamId: id, name: install.teamName, at: Date.now() };
33
+ return install.teamName;
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ /** Build the Slack resolver. */
39
+ export function slackFriendlyNameResolver(deps: SlackFriendlyNameDeps = {}): FriendlyNameResolver {
40
+ return async (id) => {
41
+ if (/^[UW]/.test(id)) {
42
+ const name = await (deps.userName ?? fetchUserDisplayName)(id);
43
+ return name ? { label: name, kind: "person" } : undefined;
44
+ }
45
+ if (/^[CG]/.test(id)) {
46
+ const name = await (deps.channelName ?? fetchChannelDisplayName)(id);
47
+ return name ? { label: `#${name.replace(/^#/, "")}`, kind: "channel" } : undefined;
48
+ }
49
+ if (/^T/.test(id)) {
50
+ const name = await (deps.teamName ?? liveTeamName)(id);
51
+ return name ? { label: name, kind: "server" } : undefined;
52
+ }
53
+ return undefined;
54
+ };
55
+ }