@jobmatchme/bee-slack 0.1.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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -0
- package/dist/gateway.d.ts +26 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +291 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +4 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +14 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +38 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jobmatchme
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `@jobmatchme/bee-slack`
|
|
2
|
+
|
|
3
|
+
`bee-slack` is the Slack-facing adapter for the Bee Dance stack.
|
|
4
|
+
|
|
5
|
+
It accepts inbound Slack events, normalizes them into gateway turns, forwards
|
|
6
|
+
them through `@jobmatchme/bee-gate`, and renders the resulting Bee Dance event
|
|
7
|
+
stream back into Slack messages and thread updates.
|
|
8
|
+
|
|
9
|
+
## What this package does
|
|
10
|
+
|
|
11
|
+
- connects to Slack via Socket Mode
|
|
12
|
+
- resolves inbound DMs and app mentions against a route configuration
|
|
13
|
+
- downloads Slack file attachments into the gateway blob store
|
|
14
|
+
- maps inbound Slack messages into the Bee Gate input model
|
|
15
|
+
- renders streamed Bee Dance output back into Slack messages and artifacts
|
|
16
|
+
|
|
17
|
+
## Design intent
|
|
18
|
+
|
|
19
|
+
The package is intentionally thin. It owns Slack-specific concerns:
|
|
20
|
+
|
|
21
|
+
- Slack tokens and Socket Mode lifecycle
|
|
22
|
+
- user and channel lookups
|
|
23
|
+
- route matching
|
|
24
|
+
- posting and updating Slack messages
|
|
25
|
+
- uploading artifacts to Slack
|
|
26
|
+
|
|
27
|
+
It does not own protocol orchestration itself. That responsibility stays in
|
|
28
|
+
`@jobmatchme/bee-gate`, which keeps the Slack adapter replaceable and easier to
|
|
29
|
+
compare against other frontends.
|
|
30
|
+
|
|
31
|
+
## Local development
|
|
32
|
+
|
|
33
|
+
For local manual testing, copy `local.config.example.json` to
|
|
34
|
+
`local.config.json`, fill in your Slack tokens, and run:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run start:local
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This starts a local NATS broker via Docker, starts the local fake backend, and
|
|
41
|
+
then launches `bee-slack` against that local stack.
|
|
42
|
+
|
|
43
|
+
## Publishing
|
|
44
|
+
|
|
45
|
+
The package is intended for public npm publication from GitHub Actions using npm
|
|
46
|
+
Trusted Publishing via GitHub OIDC.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAiBlE","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\treturn config;\n}\n"]}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
export function loadConfig(configPath) {
|
|
4
|
+
const path = configPath || process.env.BEE_SLACK_CONFIG;
|
|
5
|
+
if (!path) {
|
|
6
|
+
throw new Error("Missing BEE_SLACK_CONFIG");
|
|
7
|
+
}
|
|
8
|
+
const fullPath = resolve(path);
|
|
9
|
+
const config = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
10
|
+
if (!config.appToken)
|
|
11
|
+
throw new Error(`Missing appToken in ${fullPath}`);
|
|
12
|
+
if (!config.botToken)
|
|
13
|
+
throw new Error(`Missing botToken in ${fullPath}`);
|
|
14
|
+
if (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {
|
|
15
|
+
throw new Error(`Missing nats.servers in ${fullPath}`);
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(config.routes) || config.routes.length === 0) {
|
|
18
|
+
throw new Error(`Missing routes in ${fullPath}`);
|
|
19
|
+
}
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,MAAM,UAAU,UAAU,CAAC,UAAmB,EAAsB;IACnE,MAAM,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAuB,CAAC;IACjF,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACvG,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\treturn config;\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SlackGatewayConfig } from "./types.js";
|
|
2
|
+
export declare class SlackGateway {
|
|
3
|
+
private config;
|
|
4
|
+
private socketClient;
|
|
5
|
+
private webClient;
|
|
6
|
+
private users;
|
|
7
|
+
private channels;
|
|
8
|
+
private botUserId;
|
|
9
|
+
private teamId;
|
|
10
|
+
private teamName;
|
|
11
|
+
private blobStore;
|
|
12
|
+
private sink;
|
|
13
|
+
private engine;
|
|
14
|
+
private workerClient;
|
|
15
|
+
constructor(config: SlackGatewayConfig);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
private setupEventHandlers;
|
|
18
|
+
private enqueueInbound;
|
|
19
|
+
private buildResolvedTurn;
|
|
20
|
+
private buildOutputTarget;
|
|
21
|
+
private downloadAttachments;
|
|
22
|
+
private fetchUsers;
|
|
23
|
+
private fetchChannels;
|
|
24
|
+
}
|
|
25
|
+
export declare function startGatewayFromEnv(configPath?: string): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAiC,kBAAkB,EAAuB,MAAM,YAAY,CAAC;AAgEzG,qBAAa,YAAY;IAaZ,OAAO,CAAC,MAAM;IAZ1B,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,YAAY,CAAgC;IAEpD,YAAoB,MAAM,EAAE,kBAAkB,EAU7C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB3B;IAED,OAAO,CAAC,kBAAkB;YAkEZ,cAAc;IA0C5B,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,iBAAiB;YAOX,mBAAmB;YA+BnB,UAAU;YAsBV,aAAa;CA0B3B;AAED,wBAAsB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5E","sourcesContent":["import {\n\ttype ArtifactRef,\n\ttype AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\n\ttype TransportSink,\n} from \"@jobmatchme/bee-gate\";\nimport { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { join } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute } from \"./router.js\";\nimport type { ResolvedSlackRoute, SlackFile, SlackGatewayConfig, SlackInboundMessage } from \"./types.js\";\n\ninterface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\ninterface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\nclass SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t) {}\n\n\tasync postMessage(target: TransportOutputTarget, text: string): Promise<string> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\tchannel: target.channelId,\n\t\t\ttext,\n\t\t\tthread_ts: target.threadId,\n\t\t});\n\t\treturn result.ts as string;\n\t}\n\n\tasync updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tawait this.webClient.chat.update({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\ttext,\n\t\t});\n\t}\n\n\tasync publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst materialized = await this.blobStore.materialize(artifact);\n\t\ttry {\n\t\t\tawait (this.webClient.files as any).uploadV2({\n\t\t\t\tchannel_id: target.channelId,\n\t\t\t\tthread_ts: target.threadId,\n\t\t\t\tfile: materialized.path,\n\t\t\t\tfilename: materialized.filename,\n\t\t\t\ttitle: artifact.title || artifact.name || materialized.filename,\n\t\t\t});\n\t\t} finally {\n\t\t\tawait materialized.cleanup?.();\n\t\t}\n\t}\n}\n\nexport class SlackGateway {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate botUserId: string | null = null;\n\tprivate teamId: string | null = null;\n\tprivate teamName: string | null = null;\n\tprivate blobStore: BlobStore;\n\tprivate sink: SlackSink;\n\tprivate engine: BeeGatewayEngine<string> | null = null;\n\tprivate workerClient: BeeWorkerClient | null = null;\n\n\tconstructor(private config: SlackGatewayConfig) {\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.blobStore = new LocalFileBlobStore(\n\t\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t\t);\n\t\tthis.sink = new SlackSink(this.webClient, this.blobStore);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.workerClient = await createNatsBeeClient(this.config.nats);\n\t\tthis.engine = new BeeGatewayEngine({\n\t\t\tsink: this.sink,\n\t\t\tworkerClient: this.workerClient,\n\t\t\tlogger: {\n\t\t\t\tinfo: log.logInfo,\n\t\t\t\twarn: log.logWarning,\n\t\t\t\terror: log.logError,\n\t\t\t},\n\t\t});\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tthis.teamId = auth.team_id as string;\n\t\tthis.teamName = (auth.team as string | undefined) || null;\n\t\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\t\tlog.logInfo(\n\t\t\t`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`,\n\t\t);\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.channel.startsWith(\"D\")) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) return;\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") return;\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) return;\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = !!this.botUserId && e.text?.includes(`<@${this.botUserId}>`);\n\t\t\tif (!isDM && isBotMention) return;\n\t\t\tif (!isDM) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"dm\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate async enqueueInbound(message: SlackInboundMessage): Promise<void> {\n\t\tif (!this.botUserId || !this.teamId) {\n\t\t\tthrow new Error(\"Gateway has not been initialized\");\n\t\t}\n\t\tif (!this.engine) {\n\t\t\tthrow new Error(\"Gateway engine has not been initialized\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst resolved = resolveRoute(this.config.routes, message, {\n\t\t\t\tbotUserId: this.botUserId,\n\t\t\t\tteamId: this.teamId,\n\t\t\t\tteamName: this.teamName || undefined,\n\t\t\t});\n\t\t\tif (!resolved) {\n\t\t\t\tlog.logWarning(`No route for channel ${message.channelId}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outputTarget = this.buildOutputTarget(message, resolved);\n\t\t\tif (message.text.trim().toLowerCase() === \"stop\") {\n\t\t\t\tconst stopped = await this.engine.stopActiveRun(resolved.sessionId);\n\t\t\t\tawait this.sink.postMessage(outputTarget, stopped ? \"Stopping active run.\" : \"Nothing running.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst attachments = await this.downloadAttachments(resolved, message);\n\t\t\tconst input = this.buildResolvedTurn(message, resolved, attachments, outputTarget);\n\t\t\tthis.engine.dispatch(input);\n\t\t} catch (error) {\n\t\t\tconst messageText = error instanceof Error ? error.message : String(error);\n\t\t\tlog.logError(`Failed to normalize Slack inbound message ${message.ts}`, messageText);\n\t\t\tawait this.sink.postMessage(\n\t\t\t\t{\n\t\t\t\t\tchannelId: message.channelId,\n\t\t\t\t\tthreadId: message.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t\t\t},\n\t\t\t\t`_Gateway error: ${messageText}_`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate buildResolvedTurn(\n\t\tmessage: SlackInboundMessage,\n\t\tresolved: ResolvedSlackRoute,\n\t\tattachments: AttachmentRef[],\n\t\toutput: TransportOutputTarget,\n\t): BeeResolvedTurn {\n\t\tif (!this.teamId || !this.botUserId) {\n\t\t\tthrow new Error(\"Missing Slack gateway identity\");\n\t\t}\n\n\t\treturn {\n\t\t\tsessionId: resolved.sessionId,\n\t\t\tthreadId: resolved.threadTs,\n\t\t\tworker: resolved.route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId: resolved.conversationId,\n\t\t\t},\n\t\t\tactor: {\n\t\t\t\tuserId: message.userId,\n\t\t\t\tuserName: message.userName,\n\t\t\t\tdisplayName: message.displayName,\n\t\t\t},\n\t\t\tmessage: {\n\t\t\t\ttext: message.text,\n\t\t\t},\n\t\t\tattachments,\n\t\t\toutput,\n\t\t};\n\t}\n\n\tprivate buildOutputTarget(message: SlackInboundMessage, resolved: ResolvedSlackRoute): TransportOutputTarget {\n\t\treturn {\n\t\t\tchannelId: message.channelId,\n\t\t\tthreadId: resolved.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t};\n\t}\n\n\tprivate async downloadAttachments(\n\t\tresolved: ResolvedSlackRoute,\n\t\tmessage: SlackInboundMessage,\n\t): Promise<AttachmentRef[]> {\n\t\tconst files = message.files || [];\n\t\tif (files.length === 0) return [];\n\n\t\tconst attachments: AttachmentRef[] = [];\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url || !file.name) continue;\n\t\t\tconst response = await fetch(url, {\n\t\t\t\theaders: { authorization: `Bearer ${this.config.botToken}` },\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to download Slack attachment ${file.name}: ${response.status}`);\n\t\t\t}\n\t\t\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\t\t\tattachments.push(\n\t\t\t\tawait this.blobStore.put({\n\t\t\t\t\tnamespace: `incoming/slack/${resolved.sessionId}`,\n\t\t\t\t\tname: file.name,\n\t\t\t\t\ttitle: file.name,\n\t\t\t\t\tmimeType: file.mimetype,\n\t\t\t\t\tdata: bytes,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn attachments;\n\t}\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const user of members) {\n\t\t\t\t\tif (user.id && user.name && !user.deleted) {\n\t\t\t\t\t\tthis.users.set(user.id, {\n\t\t\t\t\t\t\tid: user.id,\n\t\t\t\t\t\t\tuserName: user.name,\n\t\t\t\t\t\t\tdisplayName: user.real_name || user.name,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel,im\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; user?: string }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const channel of channels) {\n\t\t\t\t\tif (!channel.id) continue;\n\t\t\t\t\tif (channel.name) {\n\t\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name });\n\t\t\t\t\t} else if (channel.user && this.users.has(channel.user)) {\n\t\t\t\t\t\tthis.channels.set(channel.id, {\n\t\t\t\t\t\t\tid: channel.id,\n\t\t\t\t\t\t\tname: `DM:${this.users.get(channel.user)!.userName}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n\nexport async function startGatewayFromEnv(configPath?: string): Promise<void> {\n\tconst config = loadConfig(configPath);\n\tconst gateway = new SlackGateway(config);\n\tawait gateway.start();\n}\n"]}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { BeeGatewayEngine, createNatsBeeClient, LocalFileBlobStore, } from "@jobmatchme/bee-gate";
|
|
2
|
+
import { SocketModeClient } from "@slack/socket-mode";
|
|
3
|
+
import { WebClient } from "@slack/web-api";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import * as log from "./log.js";
|
|
7
|
+
import { resolveRoute } from "./router.js";
|
|
8
|
+
class SlackSink {
|
|
9
|
+
webClient;
|
|
10
|
+
blobStore;
|
|
11
|
+
constructor(webClient, blobStore) {
|
|
12
|
+
this.webClient = webClient;
|
|
13
|
+
this.blobStore = blobStore;
|
|
14
|
+
}
|
|
15
|
+
async postMessage(target, text) {
|
|
16
|
+
if (!target.channelId) {
|
|
17
|
+
throw new Error("Missing Slack channel id");
|
|
18
|
+
}
|
|
19
|
+
const result = await this.webClient.chat.postMessage({
|
|
20
|
+
channel: target.channelId,
|
|
21
|
+
text,
|
|
22
|
+
thread_ts: target.threadId,
|
|
23
|
+
});
|
|
24
|
+
return result.ts;
|
|
25
|
+
}
|
|
26
|
+
async updateMessage(target, ref, text) {
|
|
27
|
+
if (!target.channelId) {
|
|
28
|
+
throw new Error("Missing Slack channel id");
|
|
29
|
+
}
|
|
30
|
+
await this.webClient.chat.update({
|
|
31
|
+
channel: target.channelId,
|
|
32
|
+
ts: ref,
|
|
33
|
+
text,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async publishArtifact(target, artifact) {
|
|
37
|
+
if (!target.channelId) {
|
|
38
|
+
throw new Error("Missing Slack channel id");
|
|
39
|
+
}
|
|
40
|
+
const materialized = await this.blobStore.materialize(artifact);
|
|
41
|
+
try {
|
|
42
|
+
await this.webClient.files.uploadV2({
|
|
43
|
+
channel_id: target.channelId,
|
|
44
|
+
thread_ts: target.threadId,
|
|
45
|
+
file: materialized.path,
|
|
46
|
+
filename: materialized.filename,
|
|
47
|
+
title: artifact.title || artifact.name || materialized.filename,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await materialized.cleanup?.();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class SlackGateway {
|
|
56
|
+
config;
|
|
57
|
+
socketClient;
|
|
58
|
+
webClient;
|
|
59
|
+
users = new Map();
|
|
60
|
+
channels = new Map();
|
|
61
|
+
botUserId = null;
|
|
62
|
+
teamId = null;
|
|
63
|
+
teamName = null;
|
|
64
|
+
blobStore;
|
|
65
|
+
sink;
|
|
66
|
+
engine = null;
|
|
67
|
+
workerClient = null;
|
|
68
|
+
constructor(config) {
|
|
69
|
+
this.config = config;
|
|
70
|
+
this.socketClient = new SocketModeClient({ appToken: config.appToken });
|
|
71
|
+
this.webClient = new WebClient(config.botToken);
|
|
72
|
+
this.blobStore = new LocalFileBlobStore(process.env.BEE_SLACK_BLOB_STORE_ROOT ||
|
|
73
|
+
process.env.BEE_BLOB_STORE_ROOT ||
|
|
74
|
+
process.env.HUDAI_BLOB_STORE_ROOT ||
|
|
75
|
+
join(process.cwd(), ".bee-blob-store"));
|
|
76
|
+
this.sink = new SlackSink(this.webClient, this.blobStore);
|
|
77
|
+
}
|
|
78
|
+
async start() {
|
|
79
|
+
this.workerClient = await createNatsBeeClient(this.config.nats);
|
|
80
|
+
this.engine = new BeeGatewayEngine({
|
|
81
|
+
sink: this.sink,
|
|
82
|
+
workerClient: this.workerClient,
|
|
83
|
+
logger: {
|
|
84
|
+
info: log.logInfo,
|
|
85
|
+
warn: log.logWarning,
|
|
86
|
+
error: log.logError,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const auth = await this.webClient.auth.test();
|
|
90
|
+
this.botUserId = auth.user_id;
|
|
91
|
+
this.teamId = auth.team_id;
|
|
92
|
+
this.teamName = auth.team || null;
|
|
93
|
+
await Promise.all([this.fetchUsers(), this.fetchChannels()]);
|
|
94
|
+
this.setupEventHandlers();
|
|
95
|
+
await this.socketClient.start();
|
|
96
|
+
log.logInfo(`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`);
|
|
97
|
+
}
|
|
98
|
+
setupEventHandlers() {
|
|
99
|
+
this.socketClient.on("app_mention", async ({ event, ack }) => {
|
|
100
|
+
await ack();
|
|
101
|
+
const e = event;
|
|
102
|
+
if (e.channel.startsWith("D"))
|
|
103
|
+
return;
|
|
104
|
+
await this.enqueueInbound({
|
|
105
|
+
type: "mention",
|
|
106
|
+
channelId: e.channel,
|
|
107
|
+
channelName: this.channels.get(e.channel)?.name,
|
|
108
|
+
threadTs: e.thread_ts,
|
|
109
|
+
ts: e.ts,
|
|
110
|
+
userId: e.user,
|
|
111
|
+
userName: this.users.get(e.user)?.userName,
|
|
112
|
+
displayName: this.users.get(e.user)?.displayName,
|
|
113
|
+
text: e.text.replace(/<@[A-Z0-9]+>/gi, "").trim(),
|
|
114
|
+
files: e.files,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
this.socketClient.on("message", async ({ event, ack }) => {
|
|
118
|
+
await ack();
|
|
119
|
+
const e = event;
|
|
120
|
+
if (e.bot_id || !e.user || e.user === this.botUserId)
|
|
121
|
+
return;
|
|
122
|
+
if (e.subtype !== undefined && e.subtype !== "file_share")
|
|
123
|
+
return;
|
|
124
|
+
if (!e.text && (!e.files || e.files.length === 0))
|
|
125
|
+
return;
|
|
126
|
+
const isDM = e.channel_type === "im";
|
|
127
|
+
const isBotMention = !!this.botUserId && e.text?.includes(`<@${this.botUserId}>`);
|
|
128
|
+
if (!isDM && isBotMention)
|
|
129
|
+
return;
|
|
130
|
+
if (!isDM)
|
|
131
|
+
return;
|
|
132
|
+
await this.enqueueInbound({
|
|
133
|
+
type: "dm",
|
|
134
|
+
channelId: e.channel,
|
|
135
|
+
channelName: this.channels.get(e.channel)?.name,
|
|
136
|
+
threadTs: e.thread_ts,
|
|
137
|
+
ts: e.ts,
|
|
138
|
+
userId: e.user,
|
|
139
|
+
userName: this.users.get(e.user)?.userName,
|
|
140
|
+
displayName: this.users.get(e.user)?.displayName,
|
|
141
|
+
text: (e.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim(),
|
|
142
|
+
files: e.files,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async enqueueInbound(message) {
|
|
147
|
+
if (!this.botUserId || !this.teamId) {
|
|
148
|
+
throw new Error("Gateway has not been initialized");
|
|
149
|
+
}
|
|
150
|
+
if (!this.engine) {
|
|
151
|
+
throw new Error("Gateway engine has not been initialized");
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const resolved = resolveRoute(this.config.routes, message, {
|
|
155
|
+
botUserId: this.botUserId,
|
|
156
|
+
teamId: this.teamId,
|
|
157
|
+
teamName: this.teamName || undefined,
|
|
158
|
+
});
|
|
159
|
+
if (!resolved) {
|
|
160
|
+
log.logWarning(`No route for channel ${message.channelId}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const outputTarget = this.buildOutputTarget(message, resolved);
|
|
164
|
+
if (message.text.trim().toLowerCase() === "stop") {
|
|
165
|
+
const stopped = await this.engine.stopActiveRun(resolved.sessionId);
|
|
166
|
+
await this.sink.postMessage(outputTarget, stopped ? "Stopping active run." : "Nothing running.");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const attachments = await this.downloadAttachments(resolved, message);
|
|
170
|
+
const input = this.buildResolvedTurn(message, resolved, attachments, outputTarget);
|
|
171
|
+
this.engine.dispatch(input);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
175
|
+
log.logError(`Failed to normalize Slack inbound message ${message.ts}`, messageText);
|
|
176
|
+
await this.sink.postMessage({
|
|
177
|
+
channelId: message.channelId,
|
|
178
|
+
threadId: message.threadTs || (message.type === "mention" ? message.ts : undefined),
|
|
179
|
+
}, `_Gateway error: ${messageText}_`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
buildResolvedTurn(message, resolved, attachments, output) {
|
|
183
|
+
if (!this.teamId || !this.botUserId) {
|
|
184
|
+
throw new Error("Missing Slack gateway identity");
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
sessionId: resolved.sessionId,
|
|
188
|
+
threadId: resolved.threadTs,
|
|
189
|
+
worker: resolved.route.worker,
|
|
190
|
+
conversation: {
|
|
191
|
+
transport: "slack",
|
|
192
|
+
conversationId: resolved.conversationId,
|
|
193
|
+
},
|
|
194
|
+
actor: {
|
|
195
|
+
userId: message.userId,
|
|
196
|
+
userName: message.userName,
|
|
197
|
+
displayName: message.displayName,
|
|
198
|
+
},
|
|
199
|
+
message: {
|
|
200
|
+
text: message.text,
|
|
201
|
+
},
|
|
202
|
+
attachments,
|
|
203
|
+
output,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
buildOutputTarget(message, resolved) {
|
|
207
|
+
return {
|
|
208
|
+
channelId: message.channelId,
|
|
209
|
+
threadId: resolved.threadTs || (message.type === "mention" ? message.ts : undefined),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async downloadAttachments(resolved, message) {
|
|
213
|
+
const files = message.files || [];
|
|
214
|
+
if (files.length === 0)
|
|
215
|
+
return [];
|
|
216
|
+
const attachments = [];
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
const url = file.url_private_download || file.url_private;
|
|
219
|
+
if (!url || !file.name)
|
|
220
|
+
continue;
|
|
221
|
+
const response = await fetch(url, {
|
|
222
|
+
headers: { authorization: `Bearer ${this.config.botToken}` },
|
|
223
|
+
});
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`Failed to download Slack attachment ${file.name}: ${response.status}`);
|
|
226
|
+
}
|
|
227
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
228
|
+
attachments.push(await this.blobStore.put({
|
|
229
|
+
namespace: `incoming/slack/${resolved.sessionId}`,
|
|
230
|
+
name: file.name,
|
|
231
|
+
title: file.name,
|
|
232
|
+
mimeType: file.mimetype,
|
|
233
|
+
data: bytes,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
return attachments;
|
|
237
|
+
}
|
|
238
|
+
async fetchUsers() {
|
|
239
|
+
let cursor;
|
|
240
|
+
do {
|
|
241
|
+
const result = await this.webClient.users.list({ limit: 200, cursor });
|
|
242
|
+
const members = result.members;
|
|
243
|
+
if (members) {
|
|
244
|
+
for (const user of members) {
|
|
245
|
+
if (user.id && user.name && !user.deleted) {
|
|
246
|
+
this.users.set(user.id, {
|
|
247
|
+
id: user.id,
|
|
248
|
+
userName: user.name,
|
|
249
|
+
displayName: user.real_name || user.name,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
cursor = result.response_metadata?.next_cursor;
|
|
255
|
+
} while (cursor);
|
|
256
|
+
}
|
|
257
|
+
async fetchChannels() {
|
|
258
|
+
let cursor;
|
|
259
|
+
do {
|
|
260
|
+
const result = await this.webClient.conversations.list({
|
|
261
|
+
types: "public_channel,private_channel,im",
|
|
262
|
+
exclude_archived: true,
|
|
263
|
+
limit: 200,
|
|
264
|
+
cursor,
|
|
265
|
+
});
|
|
266
|
+
const channels = result.channels;
|
|
267
|
+
if (channels) {
|
|
268
|
+
for (const channel of channels) {
|
|
269
|
+
if (!channel.id)
|
|
270
|
+
continue;
|
|
271
|
+
if (channel.name) {
|
|
272
|
+
this.channels.set(channel.id, { id: channel.id, name: channel.name });
|
|
273
|
+
}
|
|
274
|
+
else if (channel.user && this.users.has(channel.user)) {
|
|
275
|
+
this.channels.set(channel.id, {
|
|
276
|
+
id: channel.id,
|
|
277
|
+
name: `DM:${this.users.get(channel.user).userName}`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
cursor = result.response_metadata?.next_cursor;
|
|
283
|
+
} while (cursor);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export async function startGatewayFromEnv(configPath) {
|
|
287
|
+
const config = loadConfig(configPath);
|
|
288
|
+
const gateway = new SlackGateway(config);
|
|
289
|
+
await gateway.start();
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,gBAAgB,EAIhB,mBAAmB,EACnB,kBAAkB,GAGlB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C,MAAM,SAAS;IAEL,SAAS;IACT,SAAS;IAFlB,YACS,SAAoB,EACpB,SAAoB,EAC3B;yBAFO,SAAS;yBACT,SAAS;IACf,CAAC;IAEJ,KAAK,CAAC,WAAW,CAAC,MAA6B,EAAE,IAAY,EAAmB;QAC/E,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;YACpD,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,IAAI;YACJ,SAAS,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,EAAY,CAAC;IAAA,CAC3B;IAED,KAAK,CAAC,aAAa,CAAC,MAA6B,EAAE,GAAW,EAAE,IAAY,EAAiB;QAC5F,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;YAChC,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,EAAE,EAAE,GAAG;YACP,IAAI;SACJ,CAAC,CAAC;IAAA,CACH;IAED,KAAK,CAAC,eAAe,CAAC,MAA6B,EAAE,QAAqB,EAAiB;QAC1F,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAChE,IAAI,CAAC;YACJ,MAAO,IAAI,CAAC,SAAS,CAAC,KAAa,CAAC,QAAQ,CAAC;gBAC5C,UAAU,EAAE,MAAM,CAAC,SAAS;gBAC5B,SAAS,EAAE,MAAM,CAAC,QAAQ;gBAC1B,IAAI,EAAE,YAAY,CAAC,IAAI;gBACvB,QAAQ,EAAE,YAAY,CAAC,QAAQ;gBAC/B,KAAK,EAAE,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC,QAAQ;aAC/D,CAAC,CAAC;QACJ,CAAC;gBAAS,CAAC;YACV,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;QAChC,CAAC;IAAA,CACD;CACD;AAED,MAAM,OAAO,YAAY;IAaJ,MAAM;IAZlB,YAAY,CAAmB;IAC/B,SAAS,CAAY;IACrB,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IACrC,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC3C,SAAS,GAAkB,IAAI,CAAC;IAChC,MAAM,GAAkB,IAAI,CAAC;IAC7B,QAAQ,GAAkB,IAAI,CAAC;IAC/B,SAAS,CAAY;IACrB,IAAI,CAAY;IAChB,MAAM,GAAoC,IAAI,CAAC;IAC/C,YAAY,GAA2B,IAAI,CAAC;IAEpD,YAAoB,MAA0B,EAAE;sBAA5B,MAAM;QACzB,IAAI,CAAC,YAAY,GAAG,IAAI,gBAAgB,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,GAAG,IAAI,kBAAkB,CACtC,OAAO,CAAC,GAAG,CAAC,yBAAyB;YACpC,OAAO,CAAC,GAAG,CAAC,mBAAmB;YAC/B,OAAO,CAAC,GAAG,CAAC,qBAAqB;YACjC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,iBAAiB,CAAC,CACvC,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAAA,CAC1D;IAED,KAAK,CAAC,KAAK,GAAkB;QAC5B,IAAI,CAAC,YAAY,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAgB,CAAC;YAClC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,MAAM,EAAE;gBACP,IAAI,EAAE,GAAG,CAAC,OAAO;gBACjB,IAAI,EAAE,GAAG,CAAC,UAAU;gBACpB,KAAK,EAAE,GAAG,CAAC,QAAQ;aACnB;SACD,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAiB,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAiB,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAI,IAAI,CAAC,IAA2B,IAAI,IAAI,CAAC;QAC1D,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC1B,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAChC,GAAG,CAAC,OAAO,CACV,gBAAgB,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,QAAQ,CAAC,IAAI,iBAAiB,IAAI,CAAC,KAAK,CAAC,IAAI,QAAQ,CACtH,CAAC;IAAA,CACF;IAEO,kBAAkB,GAAS;QAClC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,aAAa,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;YAC7D,MAAM,GAAG,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,KAOT,CAAC;YAEF,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,OAAO;YAEtC,MAAM,IAAI,CAAC,cAAc,CAAC;gBACzB,IAAI,EAAE,SAAS;gBACf,SAAS,EAAE,CAAC,CAAC,OAAO;gBACpB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI;gBAC/C,QAAQ,EAAE,CAAC,CAAC,SAAS;gBACrB,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,MAAM,EAAE,CAAC,CAAC,IAAI;gBACd,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ;gBAC1C,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,WAAW;gBAChD,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;gBACjD,KAAK,EAAE,CAAC,CAAC,KAAK;aACd,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;YACzD,MAAM,GAAG,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,KAUT,CAAC;YAEF,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS;gBAAE,OAAO;YAC7D,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,YAAY;gBAAE,OAAO;YAClE,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;gBAAE,OAAO;YAE1D,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,KAAK,IAAI,CAAC;YACrC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;YAClF,IAAI,CAAC,IAAI,IAAI,YAAY;gBAAE,OAAO;YAClC,IAAI,CAAC,IAAI;gBAAE,OAAO;YAElB,MAAM,IAAI,CAAC,cAAc,CAAC;gBACzB,IAAI,EAAE,IAAI;gBACV,SAAS,EAAE,CAAC,CAAC,OAAO;gBACpB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI;gBAC/C,QAAQ,EAAE,CAAC,CAAC,SAAS;gBACrB,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,MAAM,EAAE,CAAC,CAAC,IAAI;gBACd,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ;gBAC1C,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,WAAW;gBAChD,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;gBACzD,KAAK,EAAE,CAAC,CAAC,KAAK;aACd,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;IAAA,CACH;IAEO,KAAK,CAAC,cAAc,CAAC,OAA4B,EAAiB;QACzE,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC5D,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE;gBAC1D,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,SAAS;aACpC,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,GAAG,CAAC,UAAU,CAAC,wBAAwB,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;gBAC5D,OAAO;YACR,CAAC;YAED,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC/D,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;gBAClD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;gBACpE,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;gBACjG,OAAO;YACR,CAAC;YAED,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACtE,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;YACnF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,WAAW,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC3E,GAAG,CAAC,QAAQ,CAAC,6CAA6C,OAAO,CAAC,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC;YACrF,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAC1B;gBACC,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;aACnF,EACD,mBAAmB,WAAW,GAAG,CACjC,CAAC;QACH,CAAC;IAAA,CACD;IAEO,iBAAiB,CACxB,OAA4B,EAC5B,QAA4B,EAC5B,WAA4B,EAC5B,MAA6B,EACX;QAClB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACnD,CAAC;QAED,OAAO;YACN,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM;YAC7B,YAAY,EAAE;gBACb,SAAS,EAAE,OAAO;gBAClB,cAAc,EAAE,QAAQ,CAAC,cAAc;aACvC;YACD,KAAK,EAAE;gBACN,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;aAChC;YACD,OAAO,EAAE;gBACR,IAAI,EAAE,OAAO,CAAC,IAAI;aAClB;YACD,WAAW;YACX,MAAM;SACN,CAAC;IAAA,CACF;IAEO,iBAAiB,CAAC,OAA4B,EAAE,QAA4B,EAAyB;QAC5G,OAAO;YACN,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;SACpF,CAAC;IAAA,CACF;IAEO,KAAK,CAAC,mBAAmB,CAChC,QAA4B,EAC5B,OAA4B,EACD;QAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAElC,MAAM,WAAW,GAAoB,EAAE,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,WAAW,CAAC;YAC1D,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACjC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBACjC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE;aAC5D,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACzF,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;YAC3D,WAAW,CAAC,IAAI,CACf,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;gBACxB,SAAS,EAAE,kBAAkB,QAAQ,CAAC,SAAS,EAAE;gBACjD,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,KAAK,EAAE,IAAI,CAAC,IAAI;gBAChB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,IAAI,EAAE,KAAK;aACX,CAAC,CACF,CAAC;QACH,CAAC;QACD,OAAO,WAAW,CAAC;IAAA,CACnB;IAEO,KAAK,CAAC,UAAU,GAAkB;QACzC,IAAI,MAA0B,CAAC;QAC/B,GAAG,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;YACvE,MAAM,OAAO,GAAG,MAAM,CAAC,OAEX,CAAC;YACb,IAAI,OAAO,EAAE,CAAC;gBACb,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;oBAC5B,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;wBAC3C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE;4BACvB,EAAE,EAAE,IAAI,CAAC,EAAE;4BACX,QAAQ,EAAE,IAAI,CAAC,IAAI;4BACnB,WAAW,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI;yBACxC,CAAC,CAAC;oBACJ,CAAC;gBACF,CAAC;YACF,CAAC;YACD,MAAM,GAAG,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAAC;QAChD,CAAC,QAAQ,MAAM,EAAE;IAAA,CACjB;IAEO,KAAK,CAAC,aAAa,GAAkB;QAC5C,IAAI,MAA0B,CAAC;QAC/B,GAAG,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC;gBACtD,KAAK,EAAE,mCAAmC;gBAC1C,gBAAgB,EAAE,IAAI;gBACtB,KAAK,EAAE,GAAG;gBACV,MAAM;aACN,CAAC,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,CAAC,QAA4E,CAAC;YACrG,IAAI,QAAQ,EAAE,CAAC;gBACd,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAChC,IAAI,CAAC,OAAO,CAAC,EAAE;wBAAE,SAAS;oBAC1B,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;wBAClB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;oBACvE,CAAC;yBAAM,IAAI,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;wBACzD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE;4BAC7B,EAAE,EAAE,OAAO,CAAC,EAAE;4BACd,IAAI,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAE,CAAC,QAAQ,EAAE;yBACpD,CAAC,CAAC;oBACJ,CAAC;gBACF,CAAC;YACF,CAAC;YACD,MAAM,GAAG,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAAC;QAChD,CAAC,QAAQ,MAAM,EAAE;IAAA,CACjB;CACD;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,UAAmB,EAAiB;IAC7E,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;AAAA,CACtB","sourcesContent":["import {\n\ttype ArtifactRef,\n\ttype AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\n\ttype TransportSink,\n} from \"@jobmatchme/bee-gate\";\nimport { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { join } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute } from \"./router.js\";\nimport type { ResolvedSlackRoute, SlackFile, SlackGatewayConfig, SlackInboundMessage } from \"./types.js\";\n\ninterface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\ninterface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\nclass SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t) {}\n\n\tasync postMessage(target: TransportOutputTarget, text: string): Promise<string> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\tchannel: target.channelId,\n\t\t\ttext,\n\t\t\tthread_ts: target.threadId,\n\t\t});\n\t\treturn result.ts as string;\n\t}\n\n\tasync updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tawait this.webClient.chat.update({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\ttext,\n\t\t});\n\t}\n\n\tasync publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst materialized = await this.blobStore.materialize(artifact);\n\t\ttry {\n\t\t\tawait (this.webClient.files as any).uploadV2({\n\t\t\t\tchannel_id: target.channelId,\n\t\t\t\tthread_ts: target.threadId,\n\t\t\t\tfile: materialized.path,\n\t\t\t\tfilename: materialized.filename,\n\t\t\t\ttitle: artifact.title || artifact.name || materialized.filename,\n\t\t\t});\n\t\t} finally {\n\t\t\tawait materialized.cleanup?.();\n\t\t}\n\t}\n}\n\nexport class SlackGateway {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate botUserId: string | null = null;\n\tprivate teamId: string | null = null;\n\tprivate teamName: string | null = null;\n\tprivate blobStore: BlobStore;\n\tprivate sink: SlackSink;\n\tprivate engine: BeeGatewayEngine<string> | null = null;\n\tprivate workerClient: BeeWorkerClient | null = null;\n\n\tconstructor(private config: SlackGatewayConfig) {\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.blobStore = new LocalFileBlobStore(\n\t\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t\t);\n\t\tthis.sink = new SlackSink(this.webClient, this.blobStore);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.workerClient = await createNatsBeeClient(this.config.nats);\n\t\tthis.engine = new BeeGatewayEngine({\n\t\t\tsink: this.sink,\n\t\t\tworkerClient: this.workerClient,\n\t\t\tlogger: {\n\t\t\t\tinfo: log.logInfo,\n\t\t\t\twarn: log.logWarning,\n\t\t\t\terror: log.logError,\n\t\t\t},\n\t\t});\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tthis.teamId = auth.team_id as string;\n\t\tthis.teamName = (auth.team as string | undefined) || null;\n\t\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\t\tlog.logInfo(\n\t\t\t`Connected as ${this.botUserId} in ${this.teamId}; loaded ${this.channels.size} channels and ${this.users.size} users`,\n\t\t);\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.channel.startsWith(\"D\")) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\t\t\tconst e = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: SlackFile[];\n\t\t\t};\n\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) return;\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") return;\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) return;\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = !!this.botUserId && e.text?.includes(`<@${this.botUserId}>`);\n\t\t\tif (!isDM && isBotMention) return;\n\t\t\tif (!isDM) return;\n\n\t\t\tawait this.enqueueInbound({\n\t\t\t\ttype: \"dm\",\n\t\t\t\tchannelId: e.channel,\n\t\t\t\tchannelName: this.channels.get(e.channel)?.name,\n\t\t\t\tthreadTs: e.thread_ts,\n\t\t\t\tts: e.ts,\n\t\t\t\tuserId: e.user,\n\t\t\t\tuserName: this.users.get(e.user)?.userName,\n\t\t\t\tdisplayName: this.users.get(e.user)?.displayName,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate async enqueueInbound(message: SlackInboundMessage): Promise<void> {\n\t\tif (!this.botUserId || !this.teamId) {\n\t\t\tthrow new Error(\"Gateway has not been initialized\");\n\t\t}\n\t\tif (!this.engine) {\n\t\t\tthrow new Error(\"Gateway engine has not been initialized\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst resolved = resolveRoute(this.config.routes, message, {\n\t\t\t\tbotUserId: this.botUserId,\n\t\t\t\tteamId: this.teamId,\n\t\t\t\tteamName: this.teamName || undefined,\n\t\t\t});\n\t\t\tif (!resolved) {\n\t\t\t\tlog.logWarning(`No route for channel ${message.channelId}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outputTarget = this.buildOutputTarget(message, resolved);\n\t\t\tif (message.text.trim().toLowerCase() === \"stop\") {\n\t\t\t\tconst stopped = await this.engine.stopActiveRun(resolved.sessionId);\n\t\t\t\tawait this.sink.postMessage(outputTarget, stopped ? \"Stopping active run.\" : \"Nothing running.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst attachments = await this.downloadAttachments(resolved, message);\n\t\t\tconst input = this.buildResolvedTurn(message, resolved, attachments, outputTarget);\n\t\t\tthis.engine.dispatch(input);\n\t\t} catch (error) {\n\t\t\tconst messageText = error instanceof Error ? error.message : String(error);\n\t\t\tlog.logError(`Failed to normalize Slack inbound message ${message.ts}`, messageText);\n\t\t\tawait this.sink.postMessage(\n\t\t\t\t{\n\t\t\t\t\tchannelId: message.channelId,\n\t\t\t\t\tthreadId: message.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t\t\t},\n\t\t\t\t`_Gateway error: ${messageText}_`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate buildResolvedTurn(\n\t\tmessage: SlackInboundMessage,\n\t\tresolved: ResolvedSlackRoute,\n\t\tattachments: AttachmentRef[],\n\t\toutput: TransportOutputTarget,\n\t): BeeResolvedTurn {\n\t\tif (!this.teamId || !this.botUserId) {\n\t\t\tthrow new Error(\"Missing Slack gateway identity\");\n\t\t}\n\n\t\treturn {\n\t\t\tsessionId: resolved.sessionId,\n\t\t\tthreadId: resolved.threadTs,\n\t\t\tworker: resolved.route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId: resolved.conversationId,\n\t\t\t},\n\t\t\tactor: {\n\t\t\t\tuserId: message.userId,\n\t\t\t\tuserName: message.userName,\n\t\t\t\tdisplayName: message.displayName,\n\t\t\t},\n\t\t\tmessage: {\n\t\t\t\ttext: message.text,\n\t\t\t},\n\t\t\tattachments,\n\t\t\toutput,\n\t\t};\n\t}\n\n\tprivate buildOutputTarget(message: SlackInboundMessage, resolved: ResolvedSlackRoute): TransportOutputTarget {\n\t\treturn {\n\t\t\tchannelId: message.channelId,\n\t\t\tthreadId: resolved.threadTs || (message.type === \"mention\" ? message.ts : undefined),\n\t\t};\n\t}\n\n\tprivate async downloadAttachments(\n\t\tresolved: ResolvedSlackRoute,\n\t\tmessage: SlackInboundMessage,\n\t): Promise<AttachmentRef[]> {\n\t\tconst files = message.files || [];\n\t\tif (files.length === 0) return [];\n\n\t\tconst attachments: AttachmentRef[] = [];\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url || !file.name) continue;\n\t\t\tconst response = await fetch(url, {\n\t\t\t\theaders: { authorization: `Bearer ${this.config.botToken}` },\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Failed to download Slack attachment ${file.name}: ${response.status}`);\n\t\t\t}\n\t\t\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\t\t\tattachments.push(\n\t\t\t\tawait this.blobStore.put({\n\t\t\t\t\tnamespace: `incoming/slack/${resolved.sessionId}`,\n\t\t\t\t\tname: file.name,\n\t\t\t\t\ttitle: file.name,\n\t\t\t\t\tmimeType: file.mimetype,\n\t\t\t\t\tdata: bytes,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn attachments;\n\t}\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const user of members) {\n\t\t\t\t\tif (user.id && user.name && !user.deleted) {\n\t\t\t\t\t\tthis.users.set(user.id, {\n\t\t\t\t\t\t\tid: user.id,\n\t\t\t\t\t\t\tuserName: user.name,\n\t\t\t\t\t\t\tdisplayName: user.real_name || user.name,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel,im\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; user?: string }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const channel of channels) {\n\t\t\t\t\tif (!channel.id) continue;\n\t\t\t\t\tif (channel.name) {\n\t\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name });\n\t\t\t\t\t} else if (channel.user && this.users.has(channel.user)) {\n\t\t\t\t\t\tthis.channels.set(channel.id, {\n\t\t\t\t\t\t\tid: channel.id,\n\t\t\t\t\t\t\tname: `DM:${this.users.get(channel.user)!.userName}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n\nexport async function startGatewayFromEnv(configPath?: string): Promise<void> {\n\tconst config = loadConfig(configPath);\n\tconst gateway = new SlackGateway(config);\n\tawait gateway.start();\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./router.js\";\nexport * from \"./types.js\";\n"]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./router.js\";\nexport * from \"./types.js\";\n"]}
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAMA,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAE9D","sourcesContent":["import chalk from \"chalk\";\n\nfunction timestamp(): string {\n\treturn new Date().toISOString();\n}\n\nexport function logInfo(message: string): void {\n\tconsole.log(`${chalk.gray(timestamp())} ${chalk.cyan(\"INFO\")} ${message}`);\n}\n\nexport function logWarning(message: string): void {\n\tconsole.warn(`${chalk.gray(timestamp())} ${chalk.yellow(\"WARN\")} ${message}`);\n}\n\nexport function logError(message: string, error?: string): void {\n\tconsole.error(`${chalk.gray(timestamp())} ${chalk.red(\"ERROR\")} ${message}${error ? `: ${error}` : \"\"}`);\n}\n"]}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
function timestamp() {
|
|
3
|
+
return new Date().toISOString();
|
|
4
|
+
}
|
|
5
|
+
export function logInfo(message) {
|
|
6
|
+
console.log(`${chalk.gray(timestamp())} ${chalk.cyan("INFO")} ${message}`);
|
|
7
|
+
}
|
|
8
|
+
export function logWarning(message) {
|
|
9
|
+
console.warn(`${chalk.gray(timestamp())} ${chalk.yellow("WARN")} ${message}`);
|
|
10
|
+
}
|
|
11
|
+
export function logError(message, error) {
|
|
12
|
+
console.error(`${chalk.gray(timestamp())} ${chalk.red("ERROR")} ${message}${error ? `: ${error}` : ""}`);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,SAAS,SAAS,GAAW;IAC5B,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAAA,CAChC;AAED,MAAM,UAAU,OAAO,CAAC,OAAe,EAAQ;IAC9C,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC;AAAA,CAC3E;AAED,MAAM,UAAU,UAAU,CAAC,OAAe,EAAQ;IACjD,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC;AAAA,CAC9E;AAED,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,KAAc,EAAQ;IAC/D,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAAA,CACzG","sourcesContent":["import chalk from \"chalk\";\n\nfunction timestamp(): string {\n\treturn new Date().toISOString();\n}\n\nexport function logInfo(message: string): void {\n\tconsole.log(`${chalk.gray(timestamp())} ${chalk.cyan(\"INFO\")} ${message}`);\n}\n\nexport function logWarning(message: string): void {\n\tconsole.warn(`${chalk.gray(timestamp())} ${chalk.yellow(\"WARN\")} ${message}`);\n}\n\nexport function logError(message: string, error?: string): void {\n\tconsole.error(`${chalk.gray(timestamp())} ${chalk.red(\"ERROR\")} ${message}${error ? `: ${error}` : \"\"}`);\n}\n"]}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { startGatewayFromEnv } from \"./gateway.js\";\n\nawait startGatewayFromEnv(process.argv[2]);\n"]}
|
package/dist/main.js
ADDED
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport { startGatewayFromEnv } from \"./gateway.js\";\n\nawait startGatewayFromEnv(process.argv[2]);\n"]}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ResolvedSlackRoute, SlackGatewayContext, SlackInboundMessage, SlackRouteConfig } from "./types.js";
|
|
2
|
+
export declare function resolveRoute(routes: SlackRouteConfig[], message: SlackInboundMessage, context: SlackGatewayContext): ResolvedSlackRoute | null;
|
|
3
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA0BjH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,gBAAgB,EAAE,EAC1B,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,mBAAmB,GAC1B,kBAAkB,GAAG,IAAI,CAkB3B","sourcesContent":["import { buildConversationId, buildSessionKey } from \"@jobmatchme/bee-gate\";\nimport type { ResolvedSlackRoute, SlackGatewayContext, SlackInboundMessage, SlackRouteConfig } from \"./types.js\";\n\nfunction matchesRoute(route: SlackRouteConfig, message: SlackInboundMessage): boolean {\n\tconst match = route.match;\n\n\tif (message.type === \"dm\") {\n\t\tif (match.dm !== true) return false;\n\t} else if (match.dm === true) {\n\t\treturn false;\n\t}\n\n\tif (match.channelIds && !match.channelIds.includes(message.channelId)) {\n\t\treturn false;\n\t}\n\n\tif (match.channelNames && (!message.channelName || !match.channelNames.includes(message.channelName))) {\n\t\treturn false;\n\t}\n\n\tif (match.textPrefix && !message.text.trim().startsWith(match.textPrefix)) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nexport function resolveRoute(\n\troutes: SlackRouteConfig[],\n\tmessage: SlackInboundMessage,\n\tcontext: SlackGatewayContext,\n): ResolvedSlackRoute | null {\n\tconst route = routes.find((candidate) => matchesRoute(candidate, message));\n\tif (!route) return null;\n\n\tconst threadId = message.threadTs || (message.type === \"mention\" ? message.ts : undefined);\n\tconst strategy = route.session?.strategy || \"thread\";\n\tconst sessionBase = strategy === \"channel\" ? message.channelId : threadId || message.ts;\n\tconst conversationId = buildConversationId([\"slack\", context.teamId, message.channelId, threadId || message.ts]);\n\tconst prefix = route.session?.prefix || route.id;\n\treturn {\n\t\troute,\n\t\tconversationId,\n\t\tsessionId: buildSessionKey(\n\t\t\tprefix,\n\t\t\tbuildConversationId([\"slack\", context.teamId, message.channelId, sessionBase]),\n\t\t),\n\t\tthreadTs: threadId,\n\t};\n}\n"]}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { buildConversationId, buildSessionKey } from "@jobmatchme/bee-gate";
|
|
2
|
+
function matchesRoute(route, message) {
|
|
3
|
+
const match = route.match;
|
|
4
|
+
if (message.type === "dm") {
|
|
5
|
+
if (match.dm !== true)
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
else if (match.dm === true) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (match.channelIds && !match.channelIds.includes(message.channelId)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (match.channelNames && (!message.channelName || !match.channelNames.includes(message.channelName))) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (match.textPrefix && !message.text.trim().startsWith(match.textPrefix)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
export function resolveRoute(routes, message, context) {
|
|
23
|
+
const route = routes.find((candidate) => matchesRoute(candidate, message));
|
|
24
|
+
if (!route)
|
|
25
|
+
return null;
|
|
26
|
+
const threadId = message.threadTs || (message.type === "mention" ? message.ts : undefined);
|
|
27
|
+
const strategy = route.session?.strategy || "thread";
|
|
28
|
+
const sessionBase = strategy === "channel" ? message.channelId : threadId || message.ts;
|
|
29
|
+
const conversationId = buildConversationId(["slack", context.teamId, message.channelId, threadId || message.ts]);
|
|
30
|
+
const prefix = route.session?.prefix || route.id;
|
|
31
|
+
return {
|
|
32
|
+
route,
|
|
33
|
+
conversationId,
|
|
34
|
+
sessionId: buildSessionKey(prefix, buildConversationId(["slack", context.teamId, message.channelId, sessionBase])),
|
|
35
|
+
threadTs: threadId,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5E,SAAS,YAAY,CAAC,KAAuB,EAAE,OAA4B,EAAW;IACrF,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAE1B,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,EAAE,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACrC,CAAC;SAAM,IAAI,KAAK,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACd,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACvE,OAAO,KAAK,CAAC;IACd,CAAC;IAED,IAAI,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC,OAAO,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;QACvG,OAAO,KAAK,CAAC;IACd,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3E,OAAO,KAAK,CAAC;IACd,CAAC;IAED,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,YAAY,CAC3B,MAA0B,EAC1B,OAA4B,EAC5B,OAA4B,EACA;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3E,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC3F,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,EAAE,QAAQ,IAAI,QAAQ,CAAC;IACrD,MAAM,WAAW,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,CAAC;IACxF,MAAM,cAAc,GAAG,mBAAmB,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,QAAQ,IAAI,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;IACjH,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,MAAM,IAAI,KAAK,CAAC,EAAE,CAAC;IACjD,OAAO;QACN,KAAK;QACL,cAAc;QACd,SAAS,EAAE,eAAe,CACzB,MAAM,EACN,mBAAmB,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAC9E;QACD,QAAQ,EAAE,QAAQ;KAClB,CAAC;AAAA,CACF","sourcesContent":["import { buildConversationId, buildSessionKey } from \"@jobmatchme/bee-gate\";\nimport type { ResolvedSlackRoute, SlackGatewayContext, SlackInboundMessage, SlackRouteConfig } from \"./types.js\";\n\nfunction matchesRoute(route: SlackRouteConfig, message: SlackInboundMessage): boolean {\n\tconst match = route.match;\n\n\tif (message.type === \"dm\") {\n\t\tif (match.dm !== true) return false;\n\t} else if (match.dm === true) {\n\t\treturn false;\n\t}\n\n\tif (match.channelIds && !match.channelIds.includes(message.channelId)) {\n\t\treturn false;\n\t}\n\n\tif (match.channelNames && (!message.channelName || !match.channelNames.includes(message.channelName))) {\n\t\treturn false;\n\t}\n\n\tif (match.textPrefix && !message.text.trim().startsWith(match.textPrefix)) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nexport function resolveRoute(\n\troutes: SlackRouteConfig[],\n\tmessage: SlackInboundMessage,\n\tcontext: SlackGatewayContext,\n): ResolvedSlackRoute | null {\n\tconst route = routes.find((candidate) => matchesRoute(candidate, message));\n\tif (!route) return null;\n\n\tconst threadId = message.threadTs || (message.type === \"mention\" ? message.ts : undefined);\n\tconst strategy = route.session?.strategy || \"thread\";\n\tconst sessionBase = strategy === \"channel\" ? message.channelId : threadId || message.ts;\n\tconst conversationId = buildConversationId([\"slack\", context.teamId, message.channelId, threadId || message.ts]);\n\tconst prefix = route.session?.prefix || route.id;\n\treturn {\n\t\troute,\n\t\tconversationId,\n\t\tsessionId: buildSessionKey(\n\t\t\tprefix,\n\t\t\tbuildConversationId([\"slack\", context.teamId, message.channelId, sessionBase]),\n\t\t),\n\t\tthreadTs: threadId,\n\t};\n}\n"]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { BeeWorkerTargetConfig } from "@jobmatchme/bee-gate";
|
|
2
|
+
export interface SlackRouteMatch {
|
|
3
|
+
channelIds?: string[];
|
|
4
|
+
channelNames?: string[];
|
|
5
|
+
dm?: boolean;
|
|
6
|
+
textPrefix?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SlackSessionConfig {
|
|
9
|
+
strategy?: "channel" | "thread";
|
|
10
|
+
prefix?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SlackRouteConfig {
|
|
13
|
+
id: string;
|
|
14
|
+
match: SlackRouteMatch;
|
|
15
|
+
worker: BeeWorkerTargetConfig;
|
|
16
|
+
session?: SlackSessionConfig;
|
|
17
|
+
}
|
|
18
|
+
export interface SlackGatewayConfig {
|
|
19
|
+
appToken: string;
|
|
20
|
+
botToken: string;
|
|
21
|
+
nats: {
|
|
22
|
+
servers: string | string[];
|
|
23
|
+
name?: string;
|
|
24
|
+
};
|
|
25
|
+
routes: SlackRouteConfig[];
|
|
26
|
+
}
|
|
27
|
+
export interface SlackFile {
|
|
28
|
+
id?: string;
|
|
29
|
+
mimetype?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
size?: number;
|
|
32
|
+
url_private_download?: string;
|
|
33
|
+
url_private?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SlackInboundMessage {
|
|
36
|
+
type: "mention" | "dm";
|
|
37
|
+
channelId: string;
|
|
38
|
+
channelName?: string;
|
|
39
|
+
threadTs?: string;
|
|
40
|
+
ts: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
userName?: string;
|
|
43
|
+
displayName?: string;
|
|
44
|
+
text: string;
|
|
45
|
+
files?: SlackFile[];
|
|
46
|
+
}
|
|
47
|
+
export interface ResolvedSlackRoute {
|
|
48
|
+
route: SlackRouteConfig;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
threadTs?: string;
|
|
51
|
+
conversationId: string;
|
|
52
|
+
}
|
|
53
|
+
export interface SlackGatewayContext {
|
|
54
|
+
teamId: string;
|
|
55
|
+
teamName?: string;
|
|
56
|
+
botUserId: string;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QACL,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,gBAAgB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jobmatchme/bee-slack",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Slack adapter for Bee Gate and the Bee Dance protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bee-slack": "dist/main.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"homepage": "https://github.com/jobmatchme/bee-slack",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/jobmatchme/bee-slack.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/jobmatchme/bee-slack/issues"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"slack",
|
|
25
|
+
"agents",
|
|
26
|
+
"gateway",
|
|
27
|
+
"protocol",
|
|
28
|
+
"nats"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"clean": "shx rm -rf coverage dist",
|
|
37
|
+
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/main.js",
|
|
38
|
+
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
39
|
+
"start:local": "zsh ./scripts/start-local-stack.sh",
|
|
40
|
+
"lint": "biome check --error-on-warnings .",
|
|
41
|
+
"lint:fix": "biome check --write --error-on-warnings .",
|
|
42
|
+
"format": "biome format --write .",
|
|
43
|
+
"typecheck": "tsgo --noEmit -p tsconfig.json",
|
|
44
|
+
"test": "vitest run --coverage",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"check": "biome check --write --error-on-warnings . && tsgo --noEmit -p tsconfig.json && vitest run --coverage && npm run build",
|
|
47
|
+
"prepare": "husky",
|
|
48
|
+
"prepublishOnly": "npm run clean && npm run check"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@jobmatchme/bee-gate": "0.1.7",
|
|
52
|
+
"@slack/socket-mode": "^2.0.0",
|
|
53
|
+
"@slack/web-api": "^7.11.0",
|
|
54
|
+
"chalk": "^5.6.2"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@biomejs/biome": "2.3.5",
|
|
58
|
+
"@typescript/native-preview": "7.0.0-dev.20260120.1",
|
|
59
|
+
"@types/node": "^24.3.0",
|
|
60
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
61
|
+
"husky": "^9.1.7",
|
|
62
|
+
"shx": "^0.4.0",
|
|
63
|
+
"typescript": "^5.7.3",
|
|
64
|
+
"vitest": "^3.2.4"
|
|
65
|
+
},
|
|
66
|
+
"optionalDependencies": {
|
|
67
|
+
"@typescript/native-preview-linux-arm64": "7.0.0-dev.20260120.1"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=20.0.0"
|
|
71
|
+
},
|
|
72
|
+
"license": "MIT"
|
|
73
|
+
}
|