@cuylabs/channel-slack 0.1.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.
- package/LICENSE +201 -0
- package/README.md +168 -0
- package/dist/activity-ByrD9Ftr.d.ts +66 -0
- package/dist/assistant.d.ts +58 -0
- package/dist/assistant.js +188 -0
- package/dist/bolt.d.ts +344 -0
- package/dist/bolt.js +705 -0
- package/dist/chunk-BODPT4I6.js +322 -0
- package/dist/chunk-FPCE5V5Y.js +292 -0
- package/dist/chunk-FX2JOVX5.js +405 -0
- package/dist/chunk-JZG4IETE.js +141 -0
- package/dist/chunk-NE57BLLU.js +0 -0
- package/dist/chunk-TWJGVDA2.js +108 -0
- package/dist/core.d.ts +425 -0
- package/dist/core.js +42 -0
- package/dist/diagnostics.d.ts +105 -0
- package/dist/diagnostics.js +8 -0
- package/dist/feedback.d.ts +137 -0
- package/dist/feedback.js +128 -0
- package/dist/history.d.ts +266 -0
- package/dist/history.js +747 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +57 -0
- package/dist/logging-Bl3HfcC8.d.ts +8 -0
- package/dist/policy.d.ts +130 -0
- package/dist/policy.js +16 -0
- package/dist/setup.d.ts +165 -0
- package/dist/setup.js +453 -0
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +43 -0
- package/dist/targets.d.ts +113 -0
- package/dist/targets.js +484 -0
- package/dist/users.d.ts +109 -0
- package/dist/users.js +240 -0
- package/docs/concepts/activity.md +33 -0
- package/docs/concepts/bolt-runtime.md +30 -0
- package/docs/concepts/message-policy.md +49 -0
- package/docs/concepts/setup-requirements.md +44 -0
- package/docs/concepts/supplemental-history.md +55 -0
- package/docs/recipes/app-mention-handler.md +34 -0
- package/docs/recipes/assistant-thread-handler.md +28 -0
- package/docs/recipes/generate-slack-manifest.md +28 -0
- package/docs/recipes/history-visibility.md +36 -0
- package/docs/recipes/socket-mode-app.md +29 -0
- package/docs/reference/channel-slack-boundary.md +50 -0
- package/docs/reference/exports.md +32 -0
- package/package.json +130 -0
package/dist/users.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/users/profile.ts
|
|
2
|
+
var DEFAULT_MAX_ENTRIES = 5e3;
|
|
3
|
+
var DEFAULT_TTL_MS = 30 * 60 * 1e3;
|
|
4
|
+
var DEFAULT_ERROR_TTL_MS = 5 * 60 * 1e3;
|
|
5
|
+
function createSlackUserProfileResolver(options = {}) {
|
|
6
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
8
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
9
|
+
const errorTtlMs = options.errorTtlMs ?? Math.min(ttlMs, DEFAULT_ERROR_TTL_MS);
|
|
10
|
+
function cacheProfile(key, profile, entryTtlMs) {
|
|
11
|
+
if (!key || entryTtlMs <= 0 || !Number.isFinite(entryTtlMs)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
cache.set(key, {
|
|
15
|
+
expiresAt: Date.now() + entryTtlMs,
|
|
16
|
+
profile
|
|
17
|
+
});
|
|
18
|
+
trimCache(cache, maxEntries);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
async resolve(input) {
|
|
22
|
+
const fallback = { userId: input.userId };
|
|
23
|
+
const key = input.teamId ? `${input.teamId}:${input.userId}` : void 0;
|
|
24
|
+
const cached = key ? cache.get(key) : void 0;
|
|
25
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
26
|
+
return cached.profile;
|
|
27
|
+
}
|
|
28
|
+
if (cached && key) {
|
|
29
|
+
cache.delete(key);
|
|
30
|
+
}
|
|
31
|
+
const botToken = input.botToken ?? options.botToken;
|
|
32
|
+
if (!botToken && !options.client) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const client = await resolveClient(options, botToken);
|
|
37
|
+
const response = await client.users.info({
|
|
38
|
+
user: input.userId,
|
|
39
|
+
...botToken ? { token: botToken } : {}
|
|
40
|
+
});
|
|
41
|
+
const profile = normalizeSlackUserProfile(input.userId, response);
|
|
42
|
+
cacheProfile(key, profile, ttlMs);
|
|
43
|
+
return profile;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
options.onLookupError?.({ ...input, error });
|
|
46
|
+
cacheProfile(key, fallback, errorTtlMs);
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
clear() {
|
|
51
|
+
cache.clear();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function resolveClient(options, botToken) {
|
|
56
|
+
if (options.client) {
|
|
57
|
+
return options.client;
|
|
58
|
+
}
|
|
59
|
+
if (options.clientFactory && botToken) {
|
|
60
|
+
return options.clientFactory(botToken);
|
|
61
|
+
}
|
|
62
|
+
const { WebClient } = await import("@slack/web-api");
|
|
63
|
+
return new WebClient(botToken);
|
|
64
|
+
}
|
|
65
|
+
function normalizeSlackUserProfile(userId, response) {
|
|
66
|
+
const user = readRecord(readRecord(response)?.user);
|
|
67
|
+
const profile = readRecord(user?.profile);
|
|
68
|
+
const displayName = readString(profile?.display_name_normalized) ?? readString(profile?.display_name) ?? readString(profile?.real_name_normalized) ?? readString(profile?.real_name) ?? readString(user?.real_name) ?? readString(user?.name);
|
|
69
|
+
const realName = readString(profile?.real_name_normalized) ?? readString(profile?.real_name) ?? readString(user?.real_name);
|
|
70
|
+
const email = readString(profile?.email) ?? readString(user?.email);
|
|
71
|
+
return {
|
|
72
|
+
userId,
|
|
73
|
+
...displayName ? { displayName } : {},
|
|
74
|
+
...realName ? { realName } : {},
|
|
75
|
+
...email ? { email } : {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function trimCache(cache, maxEntries) {
|
|
79
|
+
if (!Number.isFinite(maxEntries) || maxEntries <= 0) {
|
|
80
|
+
cache.clear();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
while (cache.size > maxEntries) {
|
|
84
|
+
const firstKey = cache.keys().next().value;
|
|
85
|
+
if (firstKey === void 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
cache.delete(firstKey);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readRecord(value) {
|
|
92
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
93
|
+
}
|
|
94
|
+
function readString(value) {
|
|
95
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/users/mentions.ts
|
|
99
|
+
var SLACK_USER_MENTION_PATTERN = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi;
|
|
100
|
+
var DEFAULT_MAX_LOOKUPS = 20;
|
|
101
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
102
|
+
async function enrichSlackUserMentions(input, options = {}) {
|
|
103
|
+
const request = typeof input === "string" ? { text: input } : input;
|
|
104
|
+
const text = request.text;
|
|
105
|
+
const teamId = request.teamId;
|
|
106
|
+
const botToken = request.botToken ?? options.botToken;
|
|
107
|
+
const mentions = collectUniqueUserMentions(text);
|
|
108
|
+
if (mentions.length === 0) {
|
|
109
|
+
return emptyResult(text);
|
|
110
|
+
}
|
|
111
|
+
const maxLookups = resolveNonNegativeInteger(
|
|
112
|
+
options.maxLookups,
|
|
113
|
+
DEFAULT_MAX_LOOKUPS
|
|
114
|
+
);
|
|
115
|
+
const concurrency = resolvePositiveInteger(
|
|
116
|
+
options.concurrency,
|
|
117
|
+
DEFAULT_CONCURRENCY
|
|
118
|
+
);
|
|
119
|
+
const lookups = mentions.slice(0, maxLookups);
|
|
120
|
+
const skippedUserIds = mentions.slice(maxLookups).map((entry) => entry.userId);
|
|
121
|
+
const profileResolver = options.profileResolver ?? createSlackUserProfileResolver(options);
|
|
122
|
+
const labelByUserId = /* @__PURE__ */ new Map();
|
|
123
|
+
const unresolvedUserIds = /* @__PURE__ */ new Set();
|
|
124
|
+
await mapWithConcurrency(lookups, concurrency, async (lookup) => {
|
|
125
|
+
try {
|
|
126
|
+
const profile = await profileResolver.resolve({
|
|
127
|
+
userId: lookup.userId,
|
|
128
|
+
...teamId ? { teamId } : {},
|
|
129
|
+
...botToken ? { botToken } : {}
|
|
130
|
+
});
|
|
131
|
+
const label = normalizeLabel(
|
|
132
|
+
(options.formatLabel ?? defaultMentionLabel)({
|
|
133
|
+
userId: lookup.userId,
|
|
134
|
+
mention: lookup.mention,
|
|
135
|
+
profile
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
if (label) {
|
|
139
|
+
labelByUserId.set(lookup.userId, label);
|
|
140
|
+
} else {
|
|
141
|
+
unresolvedUserIds.add(lookup.userId);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
options.onResolveError?.({
|
|
145
|
+
userId: lookup.userId,
|
|
146
|
+
...teamId ? { teamId } : {},
|
|
147
|
+
error
|
|
148
|
+
});
|
|
149
|
+
unresolvedUserIds.add(lookup.userId);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
const lookedUpUserIds = lookups.map((entry) => entry.userId);
|
|
153
|
+
return {
|
|
154
|
+
text: replaceUserMentions(text, labelByUserId),
|
|
155
|
+
lookedUpUserIds,
|
|
156
|
+
enrichedUserIds: lookedUpUserIds.filter(
|
|
157
|
+
(userId) => labelByUserId.has(userId)
|
|
158
|
+
),
|
|
159
|
+
unresolvedUserIds: lookedUpUserIds.filter(
|
|
160
|
+
(userId) => unresolvedUserIds.has(userId)
|
|
161
|
+
),
|
|
162
|
+
skippedUserIds
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function collectUniqueUserMentions(text) {
|
|
166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
167
|
+
const mentions = [];
|
|
168
|
+
SLACK_USER_MENTION_PATTERN.lastIndex = 0;
|
|
169
|
+
for (const match of text.matchAll(SLACK_USER_MENTION_PATTERN)) {
|
|
170
|
+
const rawUserId = match[1];
|
|
171
|
+
if (!rawUserId) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const userId = rawUserId.toUpperCase();
|
|
175
|
+
if (seen.has(userId)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
seen.add(userId);
|
|
179
|
+
mentions.push({
|
|
180
|
+
userId,
|
|
181
|
+
mention: `<@${userId}>`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return mentions;
|
|
185
|
+
}
|
|
186
|
+
function replaceUserMentions(text, labelByUserId) {
|
|
187
|
+
if (labelByUserId.size === 0) {
|
|
188
|
+
return text;
|
|
189
|
+
}
|
|
190
|
+
SLACK_USER_MENTION_PATTERN.lastIndex = 0;
|
|
191
|
+
return text.replace(
|
|
192
|
+
SLACK_USER_MENTION_PATTERN,
|
|
193
|
+
(mention, rawUserId) => labelByUserId.get(rawUserId.toUpperCase()) ?? mention
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
function defaultMentionLabel(context) {
|
|
197
|
+
const displayName = normalizeLabel(
|
|
198
|
+
context.profile.displayName ?? context.profile.realName
|
|
199
|
+
);
|
|
200
|
+
return displayName ? `${context.mention} (${displayName})` : void 0;
|
|
201
|
+
}
|
|
202
|
+
function normalizeLabel(value) {
|
|
203
|
+
const trimmed = value?.trim();
|
|
204
|
+
return trimmed || void 0;
|
|
205
|
+
}
|
|
206
|
+
function emptyResult(text) {
|
|
207
|
+
return {
|
|
208
|
+
text,
|
|
209
|
+
lookedUpUserIds: [],
|
|
210
|
+
enrichedUserIds: [],
|
|
211
|
+
unresolvedUserIds: [],
|
|
212
|
+
skippedUserIds: []
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function resolveNonNegativeInteger(value, fallback) {
|
|
216
|
+
return value !== void 0 && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
217
|
+
}
|
|
218
|
+
function resolvePositiveInteger(value, fallback) {
|
|
219
|
+
return value !== void 0 && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
220
|
+
}
|
|
221
|
+
async function mapWithConcurrency(values, concurrency, run) {
|
|
222
|
+
if (values.length === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let nextIndex = 0;
|
|
226
|
+
const workerCount = Math.min(concurrency, values.length);
|
|
227
|
+
await Promise.all(
|
|
228
|
+
Array.from({ length: workerCount }, async () => {
|
|
229
|
+
while (nextIndex < values.length) {
|
|
230
|
+
const index = nextIndex;
|
|
231
|
+
nextIndex += 1;
|
|
232
|
+
await run(values[index]);
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
export {
|
|
238
|
+
createSlackUserProfileResolver,
|
|
239
|
+
enrichSlackUserMentions
|
|
240
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Activity
|
|
2
|
+
|
|
3
|
+
An activity is the normalized message shape an agent adapter receives from
|
|
4
|
+
Slack. It is intentionally smaller than a raw Slack event and does not depend on
|
|
5
|
+
Bolt.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { parseSlackMentionActivity } from "@cuylabs/channel-slack/core";
|
|
9
|
+
|
|
10
|
+
const activity = parseSlackMentionActivity(event);
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`SlackActivityInfo` contains:
|
|
14
|
+
|
|
15
|
+
- `channelId`: Slack channel, DM, group, or thread surface ID.
|
|
16
|
+
- `channelType`: normalized surface type such as `dm`, `channel`, `group`, or
|
|
17
|
+
`thread`.
|
|
18
|
+
- `userId`: author of the message.
|
|
19
|
+
- `teamId`: workspace ID when Slack provided one.
|
|
20
|
+
- `threadTs`: parent thread timestamp for threaded replies.
|
|
21
|
+
- `messageTs`: timestamp of the inbound message.
|
|
22
|
+
- `parentUserId`: Slack's parent-thread user marker when present.
|
|
23
|
+
- `actionToken`: request-bound Slack Assistant action token when present.
|
|
24
|
+
- `text`: model-facing text with leading mentions stripped where appropriate.
|
|
25
|
+
- `isMention`: whether this was an app mention event.
|
|
26
|
+
|
|
27
|
+
The parser extracts text from plain text, rich text blocks, section/header/
|
|
28
|
+
context blocks, image or video alt/title text, and legacy attachments. This lets
|
|
29
|
+
agent adapters process the visible Slack message instead of only `event.text`.
|
|
30
|
+
|
|
31
|
+
Use `isProcessableMessage` before parsing passive `message` events. It filters
|
|
32
|
+
bot messages, message edits, deletions, reply fan-out events, and events without
|
|
33
|
+
an identifiable user.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Bolt Runtime
|
|
2
|
+
|
|
3
|
+
The Bolt helpers create Slack transports and auth wiring. They deliberately do
|
|
4
|
+
not register handlers, own prompts, run agents, or choose deployment policy.
|
|
5
|
+
|
|
6
|
+
## HTTP
|
|
7
|
+
|
|
8
|
+
`createSlackBoltApp` creates a Bolt `App`, `ExpressReceiver`, and Express app
|
|
9
|
+
for Events API and interactivity requests.
|
|
10
|
+
|
|
11
|
+
Supported auth modes:
|
|
12
|
+
|
|
13
|
+
- single-workspace bot token.
|
|
14
|
+
- Bolt-managed OAuth with an installation store.
|
|
15
|
+
- custom `authorize` function for externally managed installs.
|
|
16
|
+
|
|
17
|
+
## Socket Mode
|
|
18
|
+
|
|
19
|
+
`createSlackSocketBoltApp` creates a Socket Mode Bolt app from the same auth
|
|
20
|
+
shape plus `SLACK_APP_TOKEN`.
|
|
21
|
+
|
|
22
|
+
`createSlackSocketModeRuntime` returns receiver options and a Slack SDK logger
|
|
23
|
+
that can:
|
|
24
|
+
|
|
25
|
+
- acquire a process lock for single-instance Socket Mode deployments.
|
|
26
|
+
- tune ping/pong receiver settings.
|
|
27
|
+
- redact sensitive log fields.
|
|
28
|
+
- optionally trip a restart guard after repeated websocket health warnings.
|
|
29
|
+
|
|
30
|
+
Use `runtime.close()` during shutdown so a process lock is released.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Message Policy
|
|
2
|
+
|
|
3
|
+
Message policy decides whether a parsed Slack activity should become an agent
|
|
4
|
+
turn. It is separate from parsing so products can choose their own behavior
|
|
5
|
+
without duplicating Slack event mechanics.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { createSlackMessagePolicyResolver } from "@cuylabs/channel-slack/policy";
|
|
9
|
+
|
|
10
|
+
const policy = createSlackMessagePolicyResolver({
|
|
11
|
+
messagePolicy: "mentioned-threads",
|
|
12
|
+
threadReplyPolicy: "original-user",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const decision = policy.resolve(activity);
|
|
16
|
+
if (!decision.accepted) return;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Channel Message Policy
|
|
20
|
+
|
|
21
|
+
`messagePolicy` controls passive channel messages:
|
|
22
|
+
|
|
23
|
+
- `disabled`: accept DMs and direct mentions only.
|
|
24
|
+
- `mentioned-threads`: accept replies in threads where the bot was mentioned.
|
|
25
|
+
- `allowed-channels`: accept passive messages only from configured channel IDs.
|
|
26
|
+
- `any-added-channel`: accept passive messages anywhere the app is present.
|
|
27
|
+
|
|
28
|
+
DMs and direct mentions are always accepted unless they are duplicates.
|
|
29
|
+
|
|
30
|
+
## Thread Reply Policy
|
|
31
|
+
|
|
32
|
+
`threadReplyPolicy` controls non-mentioned replies inside remembered mentioned
|
|
33
|
+
threads:
|
|
34
|
+
|
|
35
|
+
- `mention-required`: require another direct mention.
|
|
36
|
+
- `original-user`: allow only the original thread user.
|
|
37
|
+
- `anyone`: allow any user in the thread.
|
|
38
|
+
|
|
39
|
+
## State
|
|
40
|
+
|
|
41
|
+
The resolver tracks two state sets:
|
|
42
|
+
|
|
43
|
+
- mentioned thread keys for continuation policy.
|
|
44
|
+
- accepted message keys for duplicate suppression.
|
|
45
|
+
|
|
46
|
+
The in-memory store is enough for single-process development. Production
|
|
47
|
+
multi-worker deployments should pass an async durable store to
|
|
48
|
+
`createAsyncSlackMessagePolicyResolver` and implement `claimAcceptedMessage` as
|
|
49
|
+
an insert-if-absent operation.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Setup Requirements
|
|
2
|
+
|
|
3
|
+
Setup helpers translate package features into the Slack app configuration they
|
|
4
|
+
require.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
import { getSlackSetupRequirements } from "@cuylabs/channel-slack/setup";
|
|
8
|
+
|
|
9
|
+
const requirements = getSlackSetupRequirements({
|
|
10
|
+
preset: "agent-app",
|
|
11
|
+
transport: "socket",
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Presets
|
|
16
|
+
|
|
17
|
+
- `assistant`: Slack Assistant surface and feedback controls.
|
|
18
|
+
- `channel-adapter`: app mentions and direct messages.
|
|
19
|
+
- `agent-app`: Assistant, app mentions, direct messages, and feedback.
|
|
20
|
+
|
|
21
|
+
Pass `preset: false` and provide `features` when an app needs exact control.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
Feature selections produce:
|
|
26
|
+
|
|
27
|
+
- required bot scopes.
|
|
28
|
+
- optional bot scopes.
|
|
29
|
+
- app-level token scopes for Socket Mode.
|
|
30
|
+
- bot events.
|
|
31
|
+
- Slack settings such as Assistant view, event subscriptions, interactivity, and
|
|
32
|
+
Socket Mode.
|
|
33
|
+
- environment variables needed by the chosen transport.
|
|
34
|
+
|
|
35
|
+
## Manifests
|
|
36
|
+
|
|
37
|
+
`createSlackAppManifest` produces a Slack app manifest from the same
|
|
38
|
+
requirements. `compareSlackAppManifest` checks an existing manifest-like object
|
|
39
|
+
against requirements and returns path-level findings.
|
|
40
|
+
|
|
41
|
+
## Inspection
|
|
42
|
+
|
|
43
|
+
`inspectSlackAppSetup` combines requirements with a live token inspection. It
|
|
44
|
+
does not mutate Slack configuration.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Supplemental History
|
|
2
|
+
|
|
3
|
+
Supplemental history loads Slack conversation context for a single agent turn.
|
|
4
|
+
The package does not decide whether your model should use this context. It
|
|
5
|
+
returns a prompt string and a context-fragment payload that host runtimes can
|
|
6
|
+
place into their own turn model.
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import {
|
|
10
|
+
createSlackSupplementalHistoryVisibilityPolicy,
|
|
11
|
+
loadSlackTurnHistoryContext,
|
|
12
|
+
} from "@cuylabs/channel-slack/history";
|
|
13
|
+
|
|
14
|
+
const history = await loadSlackTurnHistoryContext({
|
|
15
|
+
client,
|
|
16
|
+
activity,
|
|
17
|
+
botUserId,
|
|
18
|
+
visibilityPolicy: createSlackSupplementalHistoryVisibilityPolicy({
|
|
19
|
+
mode: "current-user-plus-assistant",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Sources
|
|
25
|
+
|
|
26
|
+
The loader can include three sources:
|
|
27
|
+
|
|
28
|
+
- `thread`: messages from the current Slack thread.
|
|
29
|
+
- `channel`: recent messages from the current channel when thread history is not
|
|
30
|
+
usable.
|
|
31
|
+
- `origin-channel`: recent messages from a different source channel, useful for
|
|
32
|
+
routed workflows such as incident channels.
|
|
33
|
+
|
|
34
|
+
Thread history is preferred. Top-level channel history is a fallback when no
|
|
35
|
+
usable thread history exists unless `includeTopLevelChannelFallback` is false.
|
|
36
|
+
|
|
37
|
+
## Visibility
|
|
38
|
+
|
|
39
|
+
Visibility policy filters fetched messages before they become model-visible.
|
|
40
|
+
Built-in modes are:
|
|
41
|
+
|
|
42
|
+
- `all`
|
|
43
|
+
- `current-user-plus-assistant`
|
|
44
|
+
- `original-user-plus-assistant`
|
|
45
|
+
- `allowed-users-plus-assistant`
|
|
46
|
+
|
|
47
|
+
Custom policies can implement the same `filter(messages, context)` interface.
|
|
48
|
+
If a visibility policy throws, that source is omitted and reported as an error
|
|
49
|
+
decision.
|
|
50
|
+
|
|
51
|
+
## Unavailable History
|
|
52
|
+
|
|
53
|
+
Slack often rejects history reads because the app is missing a scope, not in a
|
|
54
|
+
channel, or blocked from the conversation. These expected failures are returned
|
|
55
|
+
in `unavailable` with hints instead of throwing from the top-level loader.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# App Mention Handler
|
|
2
|
+
|
|
3
|
+
This recipe shows the package boundary. The agent call is intentionally left to
|
|
4
|
+
the host application.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
import {
|
|
8
|
+
markdownToSlackMrkdwn,
|
|
9
|
+
parseSlackMentionActivity,
|
|
10
|
+
} from "@cuylabs/channel-slack/core";
|
|
11
|
+
import { createSlackMessagePolicyResolver } from "@cuylabs/channel-slack/policy";
|
|
12
|
+
|
|
13
|
+
const policy = createSlackMessagePolicyResolver({
|
|
14
|
+
messagePolicy: "mentioned-threads",
|
|
15
|
+
threadReplyPolicy: "original-user",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
app.event("app_mention", async ({ event, client }) => {
|
|
19
|
+
const activity = parseSlackMentionActivity(event);
|
|
20
|
+
const decision = policy.resolve(activity);
|
|
21
|
+
if (!decision.accepted) return;
|
|
22
|
+
|
|
23
|
+
const answer = await runAgent({
|
|
24
|
+
input: decision.text,
|
|
25
|
+
slack: activity,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await client.chat.postMessage({
|
|
29
|
+
channel: activity.channelId,
|
|
30
|
+
thread_ts: activity.threadTs ?? activity.messageTs,
|
|
31
|
+
text: markdownToSlackMrkdwn(answer),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Assistant Thread Handler
|
|
2
|
+
|
|
3
|
+
Bolt assistant handlers deliver a broad message shape. Use the Assistant parser
|
|
4
|
+
to normalize only parseable user messages.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
import { parseSlackMessageActivityFromMessageEvent } from "@cuylabs/channel-slack/assistant";
|
|
8
|
+
import { markdownToSlackMrkdwn } from "@cuylabs/channel-slack/core";
|
|
9
|
+
|
|
10
|
+
assistant.userMessage(async ({ client, message, say }) => {
|
|
11
|
+
const activity = parseSlackMessageActivityFromMessageEvent(message);
|
|
12
|
+
if (!activity) return;
|
|
13
|
+
|
|
14
|
+
const answer = await runAgent({
|
|
15
|
+
input: activity.text,
|
|
16
|
+
slack: activity,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await say({
|
|
20
|
+
text: markdownToSlackMrkdwn(answer),
|
|
21
|
+
thread_ts: activity.threadTs,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
When using Bolt Assistant thread context, pass
|
|
27
|
+
`createSlackAssistantThreadContextStore()` to avoid a process-local context
|
|
28
|
+
cache that is not keyed by thread.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generate A Slack Manifest
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
import { createSlackAppManifest } from "@cuylabs/channel-slack/setup";
|
|
5
|
+
|
|
6
|
+
const manifest = createSlackAppManifest({
|
|
7
|
+
name: "My Agent",
|
|
8
|
+
description: "Agent-backed Slack assistant",
|
|
9
|
+
preset: "agent-app",
|
|
10
|
+
transport: "socket",
|
|
11
|
+
assistantDescription: "Ask the agent for help with workspace tasks.",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For HTTP transport, pass `baseUrl` so event and interactivity request URLs are
|
|
18
|
+
included:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const manifest = createSlackAppManifest({
|
|
22
|
+
name: "My Agent",
|
|
23
|
+
preset: "agent-app",
|
|
24
|
+
transport: "http",
|
|
25
|
+
baseUrl: "https://agent.example.com",
|
|
26
|
+
eventsPath: "/slack/events",
|
|
27
|
+
});
|
|
28
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# History Visibility Filter
|
|
2
|
+
|
|
3
|
+
Use visibility filters when Slack history is useful but should not expose every
|
|
4
|
+
participant's prior messages to the model.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
import {
|
|
8
|
+
createSlackSupplementalHistoryVisibilityPolicy,
|
|
9
|
+
loadSlackTurnHistoryContext,
|
|
10
|
+
} from "@cuylabs/channel-slack/history";
|
|
11
|
+
|
|
12
|
+
const visibilityPolicy = createSlackSupplementalHistoryVisibilityPolicy({
|
|
13
|
+
mode: "current-user-plus-assistant",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const history = await loadSlackTurnHistoryContext({
|
|
17
|
+
client,
|
|
18
|
+
activity,
|
|
19
|
+
botUserId,
|
|
20
|
+
visibilityPolicy,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const fragments = history.fragment ? [history.fragment] : [];
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For allow-list behavior:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const visibilityPolicy = createSlackSupplementalHistoryVisibilityPolicy({
|
|
30
|
+
mode: "allowed-users-plus-assistant",
|
|
31
|
+
allowedUserIds: ["U123", "U456"],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The result reports omitted counts per source and overall so callers can audit
|
|
36
|
+
what happened without logging message text.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Socket Mode App
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
import {
|
|
5
|
+
createSlackSocketBoltApp,
|
|
6
|
+
createSlackSocketModeRuntime,
|
|
7
|
+
} from "@cuylabs/channel-slack/bolt";
|
|
8
|
+
|
|
9
|
+
const runtime = createSlackSocketModeRuntime({
|
|
10
|
+
appSlug: "my-agent",
|
|
11
|
+
appToken: process.env.SLACK_APP_TOKEN,
|
|
12
|
+
restartGuardEnabled: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const { boltApp } = await createSlackSocketBoltApp({
|
|
16
|
+
appToken: process.env.SLACK_APP_TOKEN,
|
|
17
|
+
botToken: process.env.SLACK_BOT_TOKEN,
|
|
18
|
+
boltAppOptions: runtime.boltAppOptions,
|
|
19
|
+
socketModeReceiverOptions: runtime.socketModeReceiverOptions,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.on("SIGTERM", () => {
|
|
23
|
+
runtime.close();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await boltApp.start();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Socket Mode requires an app-level token with `connections:write`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Channel Slack Boundary
|
|
2
|
+
|
|
3
|
+
`@cuylabs/channel-slack` is the SDK-neutral Slack mechanics package. It parses
|
|
4
|
+
Slack events, formats Slack output, applies reusable admission and history
|
|
5
|
+
policies, and provides Slack setup/runtime helpers. It does not create or run an
|
|
6
|
+
agent.
|
|
7
|
+
|
|
8
|
+
`@cuylabs/channel-slack-agent-core` is the `@cuylabs/agent-core` binding. It
|
|
9
|
+
composes this package with agent-core scopes, event streams, context fragments,
|
|
10
|
+
and approval or human-input contracts.
|
|
11
|
+
|
|
12
|
+
## In This Package
|
|
13
|
+
|
|
14
|
+
- Slack activity parsing and text extraction.
|
|
15
|
+
- Markdown-to-Slack formatting.
|
|
16
|
+
- Thread-aware session helpers.
|
|
17
|
+
- Slack auth and installation store helpers.
|
|
18
|
+
- Socket Mode runtime guard and process lock.
|
|
19
|
+
- Setup requirements and manifest helpers.
|
|
20
|
+
- Diagnostics.
|
|
21
|
+
- User profile and mention helpers.
|
|
22
|
+
- Target parsing and resolution.
|
|
23
|
+
- Feedback blocks and action handler.
|
|
24
|
+
- Slack message policy resolver and state store.
|
|
25
|
+
- Supplemental history reader, context loader, and visibility policy.
|
|
26
|
+
- Assistant message parser and thread-context store.
|
|
27
|
+
|
|
28
|
+
## Outside This Package
|
|
29
|
+
|
|
30
|
+
- Agent runtime execution.
|
|
31
|
+
- Agent-runtime scopes and context-fragment middleware.
|
|
32
|
+
- Agent event stream rendering.
|
|
33
|
+
- Agent-specific approval and human-input request contracts.
|
|
34
|
+
- Product prompts, tools, audit policy, and deployment policy.
|
|
35
|
+
|
|
36
|
+
Those pieces belong in runtime-specific adapters or product applications.
|
|
37
|
+
|
|
38
|
+
## Behavior Notes
|
|
39
|
+
|
|
40
|
+
- The root export is lightweight: `core`, `policy`, and `Logger`.
|
|
41
|
+
- Peer-backed helpers live behind feature subpaths such as `bolt`, `history`,
|
|
42
|
+
`setup`, `diagnostics`, and `users`.
|
|
43
|
+
- Interactive request types are generic rather than derived from any
|
|
44
|
+
agent-runtime event type.
|
|
45
|
+
- Socket Mode logging uses this package's logger bridge rather than an agent
|
|
46
|
+
runtime logger.
|
|
47
|
+
|
|
48
|
+
One migration caveat: the default Socket Mode process-lock directory is
|
|
49
|
+
`${tmpdir}/channel-slack`. Runtime-specific adapters that need continuity with
|
|
50
|
+
an older local lock path should pass an explicit `lockDir`.
|