@jobmatchme/bee-slack 0.1.7 → 0.1.8
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/README.md +30 -0
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +1 -48
- package/dist/gateway.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -1
- package/dist/main.js.map +1 -1
- package/dist/scheduled.d.ts +23 -0
- package/dist/scheduled.d.ts.map +1 -0
- package/dist/scheduled.js +290 -0
- package/dist/scheduled.js.map +1 -0
- package/dist/slack-sink.d.ts +11 -0
- package/dist/slack-sink.d.ts.map +1 -0
- package/dist/slack-sink.js +49 -0
- package/dist/slack-sink.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,6 +101,36 @@ helm upgrade --install bee-slack ./charts/bee-slack \
|
|
|
101
101
|
The mounted config file must contain the same structure as
|
|
102
102
|
`local.config.example.json`.
|
|
103
103
|
|
|
104
|
+
### Scheduled Slack turns
|
|
105
|
+
|
|
106
|
+
The same container can also run a one-shot scheduled turn. This is intended for
|
|
107
|
+
Kubernetes `CronJob`s that should survive `bee-slack` or `fabee-pi-agent` pod
|
|
108
|
+
restarts while triggering a Bee worker and publishing the result to Slack:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bee-slack run-scheduled /config/config.json /scheduled/job.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Example `/scheduled/job.json`:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"id": "daily-dbt-report",
|
|
119
|
+
"routeId": "dm-test",
|
|
120
|
+
"target": { "slackUserId": "U1234567890" },
|
|
121
|
+
"text": "Erstelle den täglichen dbt Report und schreibe die Ergebnisse kompakt auf Deutsch."
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`routeId` selects one of the configured Slack routes and therefore the Bee
|
|
126
|
+
worker subject, for example `fabee.agent.pi.default`. For DM targets the command
|
|
127
|
+
opens the Slack DM, streams the worker run, updates the Slack status message,
|
|
128
|
+
and uploads inline artifacts when available.
|
|
129
|
+
|
|
130
|
+
The Helm chart supports persistent cluster schedules via `scheduledRuns` values;
|
|
131
|
+
each entry creates a Kubernetes `CronJob` plus a ConfigMap containing its
|
|
132
|
+
one-shot job config.
|
|
133
|
+
|
|
104
134
|
Release order matters for changes in the shared gateway runtime: publish the
|
|
105
135
|
referenced `@jobmatchme/bee-gate` version first, then tag and publish
|
|
106
136
|
`bee-slack` so the npm package, GHCR image, and OCI Helm chart resolve the same
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAmBA,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 { createReadStream } from \"fs\";\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: createReadStream(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"]}
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAiC,kBAAkB,EAAuB,MAAM,YAAY,CAAC;AAazG,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 AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\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 { SlackSink } from \"./slack-sink.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\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
CHANGED
|
@@ -1,58 +1,11 @@
|
|
|
1
1
|
import { BeeGatewayEngine, createNatsBeeClient, LocalFileBlobStore, } from "@jobmatchme/bee-gate";
|
|
2
2
|
import { SocketModeClient } from "@slack/socket-mode";
|
|
3
3
|
import { WebClient } from "@slack/web-api";
|
|
4
|
-
import { createReadStream } from "fs";
|
|
5
4
|
import { join } from "path";
|
|
6
5
|
import { loadConfig } from "./config.js";
|
|
7
6
|
import * as log from "./log.js";
|
|
8
7
|
import { resolveRoute } from "./router.js";
|
|
9
|
-
|
|
10
|
-
webClient;
|
|
11
|
-
blobStore;
|
|
12
|
-
constructor(webClient, blobStore) {
|
|
13
|
-
this.webClient = webClient;
|
|
14
|
-
this.blobStore = blobStore;
|
|
15
|
-
}
|
|
16
|
-
async postMessage(target, text) {
|
|
17
|
-
if (!target.channelId) {
|
|
18
|
-
throw new Error("Missing Slack channel id");
|
|
19
|
-
}
|
|
20
|
-
const result = await this.webClient.chat.postMessage({
|
|
21
|
-
channel: target.channelId,
|
|
22
|
-
text,
|
|
23
|
-
thread_ts: target.threadId,
|
|
24
|
-
});
|
|
25
|
-
return result.ts;
|
|
26
|
-
}
|
|
27
|
-
async updateMessage(target, ref, text) {
|
|
28
|
-
if (!target.channelId) {
|
|
29
|
-
throw new Error("Missing Slack channel id");
|
|
30
|
-
}
|
|
31
|
-
await this.webClient.chat.update({
|
|
32
|
-
channel: target.channelId,
|
|
33
|
-
ts: ref,
|
|
34
|
-
text,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
async publishArtifact(target, artifact) {
|
|
38
|
-
if (!target.channelId) {
|
|
39
|
-
throw new Error("Missing Slack channel id");
|
|
40
|
-
}
|
|
41
|
-
const materialized = await this.blobStore.materialize(artifact);
|
|
42
|
-
try {
|
|
43
|
-
await this.webClient.files.uploadV2({
|
|
44
|
-
channel_id: target.channelId,
|
|
45
|
-
thread_ts: target.threadId,
|
|
46
|
-
file: createReadStream(materialized.path),
|
|
47
|
-
filename: materialized.filename,
|
|
48
|
-
title: artifact.title || artifact.name || materialized.filename,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
finally {
|
|
52
|
-
await materialized.cleanup?.();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
8
|
+
import { SlackSink } from "./slack-sink.js";
|
|
56
9
|
export class SlackGateway {
|
|
57
10
|
config;
|
|
58
11
|
socketClient;
|
package/dist/gateway.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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","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 { createReadStream } from \"fs\";\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: createReadStream(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"]}
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype AttachmentRef,\n\tBeeGatewayEngine,\n\ttype BeeResolvedTurn,\n\ttype BeeWorkerClient,\n\ttype BlobStore,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\ttype TransportOutputTarget,\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 { SlackSink } from \"./slack-sink.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\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
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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"]}
|
|
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,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./router.js\";\nexport * from \"./scheduled.js\";\nexport * from \"./slack-sink.js\";\nexport * from \"./types.js\";\n"]}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./config.js\";\nexport * from \"./gateway.js\";\nexport * from \"./router.js\";\nexport * from \"./scheduled.js\";\nexport * from \"./slack-sink.js\";\nexport * from \"./types.js\";\n"]}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +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\
|
|
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\";\nimport { runScheduledSlackTurnFromFiles } from \"./scheduled.js\";\n\nconst [commandOrConfigPath, maybeConfigPath, maybeJobPath] = process.argv.slice(2);\n\nif (commandOrConfigPath === \"run-scheduled\" || commandOrConfigPath === \"scheduled\") {\n\tawait runScheduledSlackTurnFromFiles(maybeConfigPath, maybeJobPath);\n} else {\n\tawait startGatewayFromEnv(commandOrConfigPath);\n}\n"]}
|
package/dist/main.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { startGatewayFromEnv } from "./gateway.js";
|
|
3
|
-
|
|
3
|
+
import { runScheduledSlackTurnFromFiles } from "./scheduled.js";
|
|
4
|
+
const [commandOrConfigPath, maybeConfigPath, maybeJobPath] = process.argv.slice(2);
|
|
5
|
+
if (commandOrConfigPath === "run-scheduled" || commandOrConfigPath === "scheduled") {
|
|
6
|
+
await runScheduledSlackTurnFromFiles(maybeConfigPath, maybeJobPath);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
await startGatewayFromEnv(commandOrConfigPath);
|
|
10
|
+
}
|
|
4
11
|
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAC;AAEhE,MAAM,CAAC,mBAAmB,EAAE,eAAe,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnF,IAAI,mBAAmB,KAAK,eAAe,IAAI,mBAAmB,KAAK,WAAW,EAAE,CAAC;IACpF,MAAM,8BAA8B,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;AACrE,CAAC;KAAM,CAAC;IACP,MAAM,mBAAmB,CAAC,mBAAmB,CAAC,CAAC;AAChD,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport { startGatewayFromEnv } from \"./gateway.js\";\nimport { runScheduledSlackTurnFromFiles } from \"./scheduled.js\";\n\nconst [commandOrConfigPath, maybeConfigPath, maybeJobPath] = process.argv.slice(2);\n\nif (commandOrConfigPath === \"run-scheduled\" || commandOrConfigPath === \"scheduled\") {\n\tawait runScheduledSlackTurnFromFiles(maybeConfigPath, maybeJobPath);\n} else {\n\tawait startGatewayFromEnv(commandOrConfigPath);\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SlackGatewayConfig } from "./types.js";
|
|
2
|
+
export interface SlackScheduledRunConfig {
|
|
3
|
+
id: string;
|
|
4
|
+
routeId: string;
|
|
5
|
+
text: string;
|
|
6
|
+
target: {
|
|
7
|
+
slackUserId?: string;
|
|
8
|
+
channelId?: string;
|
|
9
|
+
threadTs?: string;
|
|
10
|
+
};
|
|
11
|
+
actor?: {
|
|
12
|
+
userId: string;
|
|
13
|
+
userName?: string;
|
|
14
|
+
displayName?: string;
|
|
15
|
+
};
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
sessionPrefix?: string;
|
|
18
|
+
conversationId?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function loadScheduledRunConfig(jobPath?: string): SlackScheduledRunConfig;
|
|
21
|
+
export declare function runScheduledSlackTurnFromFiles(configPath?: string, jobPath?: string): Promise<void>;
|
|
22
|
+
export declare function runScheduledSlackTurn(gatewayConfig: SlackGatewayConfig, scheduledRun: SlackScheduledRunConfig): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=scheduled.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduled.d.ts","sourceRoot":"","sources":["../src/scheduled.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QACP,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,KAAK,CAAC,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AA8BD,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,uBAAuB,CAYhF;AAED,wBAAsB,8BAA8B,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzG;AAED,wBAAsB,qBAAqB,CAC1C,aAAa,EAAE,kBAAkB,EACjC,YAAY,EAAE,uBAAuB,GACnC,OAAO,CAAC,IAAI,CAAC,CAqEf","sourcesContent":["import {\n\ttype ArtifactRef,\n\ttype BeeResolvedTurn,\n\ttype BeeRunEvent,\n\ttype BeeWorkerClient,\n\tbuildConversationId,\n\tbuildSessionKey,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\tnewTurnId,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { WebClient } from \"@slack/web-api\";\nimport { readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { SlackSink } from \"./slack-sink.js\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport interface SlackScheduledRunConfig {\n\tid: string;\n\trouteId: string;\n\ttext: string;\n\ttarget: {\n\t\tslackUserId?: string;\n\t\tchannelId?: string;\n\t\tthreadTs?: string;\n\t};\n\tactor?: {\n\t\tuserId: string;\n\t\tuserName?: string;\n\t\tdisplayName?: string;\n\t};\n\tsessionId?: string;\n\tsessionPrefix?: string;\n\tconversationId?: string;\n}\n\ninterface RenderState {\n\tstatusRef?: string;\n\tlatestText: string;\n\titemTexts: Map<string, string>;\n}\n\ntype ItemPartLike = { kind: string; [key: string]: unknown };\n\ntype ItemAppendedPayload = {\n\teventType: \"item.appended\";\n\titem: { id: string; kind: string; role: string; parts: ItemPartLike[] };\n};\n\ntype ItemUpdatedPayload = {\n\teventType: \"item.updated\";\n\titemId: string;\n\tappendParts?: ItemPartLike[];\n};\n\ntype RunFailedPayload = {\n\teventType: \"run.failed\";\n\terror: string;\n};\n\ntype ApprovalRequestedPayload = {\n\tsummary: string;\n};\n\nexport function loadScheduledRunConfig(jobPath?: string): SlackScheduledRunConfig {\n\tconst path = jobPath || process.env.BEE_SLACK_SCHEDULED_RUN_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\n\t\t\t\"Missing scheduled run config path; pass it as second argument or set BEE_SLACK_SCHEDULED_RUN_CONFIG\",\n\t\t);\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackScheduledRunConfig;\n\tvalidateScheduledRunConfig(config, fullPath);\n\treturn config;\n}\n\nexport async function runScheduledSlackTurnFromFiles(configPath?: string, jobPath?: string): Promise<void> {\n\tconst gatewayConfig = loadConfig(configPath);\n\tconst scheduledRun = loadScheduledRunConfig(jobPath);\n\tawait runScheduledSlackTurn(gatewayConfig, scheduledRun);\n}\n\nexport async function runScheduledSlackTurn(\n\tgatewayConfig: SlackGatewayConfig,\n\tscheduledRun: SlackScheduledRunConfig,\n): Promise<void> {\n\tvalidateScheduledRunConfig(scheduledRun, \"scheduled run config\");\n\tconst route = gatewayConfig.routes.find((candidate) => candidate.id === scheduledRun.routeId);\n\tif (!route) {\n\t\tthrow new Error(`Scheduled run ${scheduledRun.id} references unknown routeId ${scheduledRun.routeId}`);\n\t}\n\n\tconst webClient = new WebClient(gatewayConfig.botToken);\n\tconst blobStore = new LocalFileBlobStore(\n\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t);\n\tconst sink = new SlackSink(webClient, blobStore);\n\tconst workerClient = await createNatsBeeClient(gatewayConfig.nats);\n\n\ttry {\n\t\tconst auth = await webClient.auth.test();\n\t\tconst teamId = String(auth.team_id || \"unknown-team\");\n\t\tconst output = await resolveScheduledOutputTarget(webClient, scheduledRun);\n\t\tconst actor = await resolveScheduledActor(webClient, scheduledRun);\n\t\tif (!output.threadId) {\n\t\t\toutput.threadId = await sink.postMessage(\n\t\t\t\t{ channelId: output.channelId },\n\t\t\t\t`:alarm_clock: Scheduled Bee run \\`${scheduledRun.id}\\` started. Reply in this thread to continue the session.`,\n\t\t\t);\n\t\t}\n\t\tconst slackConversationId = buildConversationId([\n\t\t\t\"slack\",\n\t\t\tteamId,\n\t\t\toutput.channelId || \"unknown-channel\",\n\t\t\toutput.threadId,\n\t\t]);\n\t\tconst conversationId = scheduledRun.conversationId || slackConversationId;\n\t\tconst sessionBase =\n\t\t\troute.session?.strategy === \"channel\" ? output.channelId || \"unknown-channel\" : output.threadId;\n\t\tconst routeSessionId = buildSessionKey(\n\t\t\tscheduledRun.sessionPrefix || route.session?.prefix || route.id,\n\t\t\tbuildConversationId([\"slack\", teamId, output.channelId || \"unknown-channel\", sessionBase || output.threadId]),\n\t\t);\n\t\tconst sessionId = scheduledRun.sessionId || routeSessionId;\n\n\t\tconst input: BeeResolvedTurn = {\n\t\t\tsessionId,\n\t\t\tthreadId: output.threadId,\n\t\t\tworker: route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId,\n\t\t\t},\n\t\t\tactor,\n\t\t\tmessage: {\n\t\t\t\ttext: scheduledRun.text,\n\t\t\t},\n\t\t\tattachments: [],\n\t\t\toutput,\n\t\t};\n\n\t\tlog.logInfo(`Starting scheduled Slack run ${scheduledRun.id} on route ${route.id}`);\n\t\tawait streamScheduledTurn(workerClient, sink, input);\n\t\tlog.logInfo(`Completed scheduled Slack run ${scheduledRun.id}`);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tlog.logError(`Scheduled Slack run ${scheduledRun.id} failed`, message);\n\t\tthrow error;\n\t} finally {\n\t\tawait workerClient.close?.();\n\t}\n}\n\nasync function resolveScheduledOutputTarget(\n\twebClient: WebClient,\n\tscheduledRun: SlackScheduledRunConfig,\n): Promise<TransportOutputTarget> {\n\tif (scheduledRun.target.channelId) {\n\t\treturn {\n\t\t\tchannelId: scheduledRun.target.channelId,\n\t\t\tthreadId: scheduledRun.target.threadTs,\n\t\t};\n\t}\n\n\tif (!scheduledRun.target.slackUserId) {\n\t\tthrow new Error(`Scheduled run ${scheduledRun.id} needs target.slackUserId or target.channelId`);\n\t}\n\n\tconst result = await webClient.conversations.open({ users: scheduledRun.target.slackUserId });\n\tconst channel = result.channel as { id?: string } | undefined;\n\tif (!channel?.id) {\n\t\tthrow new Error(`Slack conversations.open did not return a channel for user ${scheduledRun.target.slackUserId}`);\n\t}\n\treturn {\n\t\tchannelId: channel.id,\n\t\tthreadId: scheduledRun.target.threadTs,\n\t};\n}\n\nasync function resolveScheduledActor(\n\twebClient: WebClient,\n\tscheduledRun: SlackScheduledRunConfig,\n): Promise<BeeResolvedTurn[\"actor\"]> {\n\tif (scheduledRun.actor) return scheduledRun.actor;\n\tif (!scheduledRun.target.slackUserId) {\n\t\treturn {\n\t\t\tuserId: `scheduler:${scheduledRun.id}`,\n\t\t\tdisplayName: \"Bee Scheduler\",\n\t\t};\n\t}\n\n\ttry {\n\t\tconst info = await webClient.users.info({ user: scheduledRun.target.slackUserId });\n\t\tconst user = info.user as\n\t\t\t| { id?: string; name?: string; real_name?: string; profile?: { display_name?: string } }\n\t\t\t| undefined;\n\t\treturn {\n\t\t\tuserId: scheduledRun.target.slackUserId,\n\t\t\tuserName: user?.name,\n\t\t\tdisplayName: user?.profile?.display_name || user?.real_name || user?.name,\n\t\t};\n\t} catch (error) {\n\t\tlog.logWarning(\n\t\t\t`Could not resolve Slack user ${scheduledRun.target.slackUserId}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t\treturn {\n\t\t\tuserId: scheduledRun.target.slackUserId,\n\t\t\tdisplayName: scheduledRun.target.slackUserId,\n\t\t};\n\t}\n}\n\nasync function streamScheduledTurn(\n\tworkerClient: BeeWorkerClient,\n\tsink: SlackSink,\n\tinput: BeeResolvedTurn,\n): Promise<void> {\n\tconst state: RenderState = {\n\t\tlatestText: \"_Working..._\",\n\t\titemTexts: new Map<string, string>(),\n\t};\n\tconst request = {\n\t\tsessionId: input.sessionId,\n\t\tthreadId: input.threadId,\n\t\tturnId: newTurnId(),\n\t\tconversation: input.conversation,\n\t\tactor: input.actor,\n\t\tmessage: input.message,\n\t\tattachments: input.attachments,\n\t};\n\n\ttry {\n\t\tawait workerClient.streamTurn(input.worker, request, async (event) => {\n\t\t\tawait handleScheduledEvent(sink, input.output, event, state);\n\t\t});\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tawait sink.postMessage(input.output, `_Scheduled gateway error: ${message}_`);\n\t\tthrow error;\n\t}\n}\n\nasync function handleScheduledEvent(\n\tsink: SlackSink,\n\toutput: TransportOutputTarget,\n\tevent: BeeRunEvent,\n\tstate: RenderState,\n): Promise<void> {\n\tif (event.name === \"run.started\") {\n\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\n\t\treturn;\n\t}\n\n\tif (event.name === \"run.completed\") {\n\t\tif (!state.statusRef) {\n\t\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (event.name === \"run.failed\") {\n\t\tconst errorText = `_Error: ${asRunFailedPayload(event).error}_`;\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, errorText);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, errorText);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (event.name === \"approval.requested\") {\n\t\tawait sink.postMessage(output, `Approval requested: ${asApprovalRequestedPayload(event).summary}`);\n\t\treturn;\n\t}\n\n\tif (event.name === \"item.appended\") {\n\t\tconst payload = asItemAppendedPayload(event);\n\t\tconst text = renderParts(payload.item.parts);\n\t\tstate.itemTexts.set(payload.item.id, text);\n\t\tif (payload.item.kind === \"artifact\") {\n\t\t\tconst artifact = firstArtifactRef(payload.item.parts);\n\t\t\tif (artifact && artifactHasPayload(artifact)) {\n\t\t\t\ttry {\n\t\t\t\t\tawait sink.publishArtifact(output, artifact);\n\t\t\t\t\treturn;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tawait sink.postMessage(output, `${text}\\n_Artifact upload failed: ${message}_`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait sink.postMessage(output, text);\n\t\t\treturn;\n\t\t}\n\t\tstate.latestText = text;\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, text);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, text);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (event.name === \"item.updated\") {\n\t\tconst payload = asItemUpdatedPayload(event);\n\t\tconst current = state.itemTexts.get(payload.itemId) || \"\";\n\t\tconst appended = renderParts(payload.appendParts || []);\n\t\tconst next = current ? `${current}${appended}` : appended;\n\t\tstate.itemTexts.set(payload.itemId, next);\n\t\tstate.latestText = next;\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, next);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, next);\n\t\t}\n\t}\n}\n\nfunction firstArtifactRef(parts: ItemPartLike[]): ArtifactRef | undefined {\n\tconst part = parts.find((entry) => entry.kind === \"artifactRef\");\n\tif (!part) return undefined;\n\treturn {\n\t\tartifactId: String(part.artifactId || \"artifact\"),\n\t\tblobKey: typeof part.blobKey === \"string\" ? part.blobKey : undefined,\n\t\tname: typeof part.name === \"string\" ? part.name : undefined,\n\t\ttitle: typeof part.title === \"string\" ? part.title : undefined,\n\t\tmimeType: typeof part.mimeType === \"string\" ? part.mimeType : undefined,\n\t\turi: typeof part.uri === \"string\" ? part.uri : undefined,\n\t\tsizeBytes: typeof part.sizeBytes === \"number\" ? part.sizeBytes : undefined,\n\t};\n}\n\nfunction artifactHasPayload(artifact: ArtifactRef): boolean {\n\treturn !!(artifact.uri || artifact.blobKey);\n}\n\nfunction renderParts(parts: ItemPartLike[]): string {\n\treturn parts\n\t\t.map((part) => {\n\t\t\tif (part.kind === \"text\") return String(part.text || \"\");\n\t\t\tif (part.kind === \"status\") return String(part.status || \"\");\n\t\t\tif (part.kind === \"artifactRef\") {\n\t\t\t\treturn `Artifact: ${String(part.title || part.name || part.artifactId || \"artifact\")}`;\n\t\t\t}\n\t\t\tif (part.kind === \"approval\" || part.kind === \"choice\") {\n\t\t\t\treturn `${String(part.title || \"\")}\\n${String(part.summary || \"\")}`.trim();\n\t\t\t}\n\t\t\tif (part.kind === \"form\") return String(part.title || \"\");\n\t\t\tif (part.kind === \"log\") return String(part.text || \"\");\n\t\t\tif (part.kind === \"patch\" || part.kind === \"diff\") {\n\t\t\t\tconst files = Array.isArray(part.files) ? (part.files as Array<{ path?: unknown }>) : [];\n\t\t\t\treturn files.map((entry) => `File: ${String(entry.path || \"\")}`).join(\"\\n\");\n\t\t\t}\n\t\t\treturn JSON.stringify(part);\n\t\t})\n\t\t.filter(Boolean)\n\t\t.join(\"\\n\");\n}\n\nfunction validateScheduledRunConfig(config: SlackScheduledRunConfig, source: string): void {\n\tif (!config.id) throw new Error(`Missing id in ${source}`);\n\tif (!config.routeId) throw new Error(`Missing routeId in ${source}`);\n\tif (!config.text) throw new Error(`Missing text in ${source}`);\n\tif (!config.target) throw new Error(`Missing target in ${source}`);\n\tif (!config.target.slackUserId && !config.target.channelId) {\n\t\tthrow new Error(`Missing target.slackUserId or target.channelId in ${source}`);\n\t}\n}\n\nfunction asRunFailedPayload(event: BeeRunEvent): RunFailedPayload {\n\treturn event.payload as RunFailedPayload;\n}\n\nfunction asApprovalRequestedPayload(event: BeeRunEvent): ApprovalRequestedPayload {\n\treturn event.payload as ApprovalRequestedPayload;\n}\n\nfunction asItemAppendedPayload(event: BeeRunEvent): ItemAppendedPayload {\n\treturn event.payload as ItemAppendedPayload;\n}\n\nfunction asItemUpdatedPayload(event: BeeRunEvent): ItemUpdatedPayload {\n\treturn event.payload as ItemUpdatedPayload;\n}\n"]}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { buildConversationId, buildSessionKey, createNatsBeeClient, LocalFileBlobStore, newTurnId, } from "@jobmatchme/bee-gate";
|
|
2
|
+
import { WebClient } from "@slack/web-api";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import * as log from "./log.js";
|
|
7
|
+
import { SlackSink } from "./slack-sink.js";
|
|
8
|
+
export function loadScheduledRunConfig(jobPath) {
|
|
9
|
+
const path = jobPath || process.env.BEE_SLACK_SCHEDULED_RUN_CONFIG;
|
|
10
|
+
if (!path) {
|
|
11
|
+
throw new Error("Missing scheduled run config path; pass it as second argument or set BEE_SLACK_SCHEDULED_RUN_CONFIG");
|
|
12
|
+
}
|
|
13
|
+
const fullPath = resolve(path);
|
|
14
|
+
const config = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
15
|
+
validateScheduledRunConfig(config, fullPath);
|
|
16
|
+
return config;
|
|
17
|
+
}
|
|
18
|
+
export async function runScheduledSlackTurnFromFiles(configPath, jobPath) {
|
|
19
|
+
const gatewayConfig = loadConfig(configPath);
|
|
20
|
+
const scheduledRun = loadScheduledRunConfig(jobPath);
|
|
21
|
+
await runScheduledSlackTurn(gatewayConfig, scheduledRun);
|
|
22
|
+
}
|
|
23
|
+
export async function runScheduledSlackTurn(gatewayConfig, scheduledRun) {
|
|
24
|
+
validateScheduledRunConfig(scheduledRun, "scheduled run config");
|
|
25
|
+
const route = gatewayConfig.routes.find((candidate) => candidate.id === scheduledRun.routeId);
|
|
26
|
+
if (!route) {
|
|
27
|
+
throw new Error(`Scheduled run ${scheduledRun.id} references unknown routeId ${scheduledRun.routeId}`);
|
|
28
|
+
}
|
|
29
|
+
const webClient = new WebClient(gatewayConfig.botToken);
|
|
30
|
+
const blobStore = new LocalFileBlobStore(process.env.BEE_SLACK_BLOB_STORE_ROOT ||
|
|
31
|
+
process.env.BEE_BLOB_STORE_ROOT ||
|
|
32
|
+
process.env.HUDAI_BLOB_STORE_ROOT ||
|
|
33
|
+
join(process.cwd(), ".bee-blob-store"));
|
|
34
|
+
const sink = new SlackSink(webClient, blobStore);
|
|
35
|
+
const workerClient = await createNatsBeeClient(gatewayConfig.nats);
|
|
36
|
+
try {
|
|
37
|
+
const auth = await webClient.auth.test();
|
|
38
|
+
const teamId = String(auth.team_id || "unknown-team");
|
|
39
|
+
const output = await resolveScheduledOutputTarget(webClient, scheduledRun);
|
|
40
|
+
const actor = await resolveScheduledActor(webClient, scheduledRun);
|
|
41
|
+
if (!output.threadId) {
|
|
42
|
+
output.threadId = await sink.postMessage({ channelId: output.channelId }, `:alarm_clock: Scheduled Bee run \`${scheduledRun.id}\` started. Reply in this thread to continue the session.`);
|
|
43
|
+
}
|
|
44
|
+
const slackConversationId = buildConversationId([
|
|
45
|
+
"slack",
|
|
46
|
+
teamId,
|
|
47
|
+
output.channelId || "unknown-channel",
|
|
48
|
+
output.threadId,
|
|
49
|
+
]);
|
|
50
|
+
const conversationId = scheduledRun.conversationId || slackConversationId;
|
|
51
|
+
const sessionBase = route.session?.strategy === "channel" ? output.channelId || "unknown-channel" : output.threadId;
|
|
52
|
+
const routeSessionId = buildSessionKey(scheduledRun.sessionPrefix || route.session?.prefix || route.id, buildConversationId(["slack", teamId, output.channelId || "unknown-channel", sessionBase || output.threadId]));
|
|
53
|
+
const sessionId = scheduledRun.sessionId || routeSessionId;
|
|
54
|
+
const input = {
|
|
55
|
+
sessionId,
|
|
56
|
+
threadId: output.threadId,
|
|
57
|
+
worker: route.worker,
|
|
58
|
+
conversation: {
|
|
59
|
+
transport: "slack",
|
|
60
|
+
conversationId,
|
|
61
|
+
},
|
|
62
|
+
actor,
|
|
63
|
+
message: {
|
|
64
|
+
text: scheduledRun.text,
|
|
65
|
+
},
|
|
66
|
+
attachments: [],
|
|
67
|
+
output,
|
|
68
|
+
};
|
|
69
|
+
log.logInfo(`Starting scheduled Slack run ${scheduledRun.id} on route ${route.id}`);
|
|
70
|
+
await streamScheduledTurn(workerClient, sink, input);
|
|
71
|
+
log.logInfo(`Completed scheduled Slack run ${scheduledRun.id}`);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
log.logError(`Scheduled Slack run ${scheduledRun.id} failed`, message);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await workerClient.close?.();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function resolveScheduledOutputTarget(webClient, scheduledRun) {
|
|
83
|
+
if (scheduledRun.target.channelId) {
|
|
84
|
+
return {
|
|
85
|
+
channelId: scheduledRun.target.channelId,
|
|
86
|
+
threadId: scheduledRun.target.threadTs,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!scheduledRun.target.slackUserId) {
|
|
90
|
+
throw new Error(`Scheduled run ${scheduledRun.id} needs target.slackUserId or target.channelId`);
|
|
91
|
+
}
|
|
92
|
+
const result = await webClient.conversations.open({ users: scheduledRun.target.slackUserId });
|
|
93
|
+
const channel = result.channel;
|
|
94
|
+
if (!channel?.id) {
|
|
95
|
+
throw new Error(`Slack conversations.open did not return a channel for user ${scheduledRun.target.slackUserId}`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
channelId: channel.id,
|
|
99
|
+
threadId: scheduledRun.target.threadTs,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function resolveScheduledActor(webClient, scheduledRun) {
|
|
103
|
+
if (scheduledRun.actor)
|
|
104
|
+
return scheduledRun.actor;
|
|
105
|
+
if (!scheduledRun.target.slackUserId) {
|
|
106
|
+
return {
|
|
107
|
+
userId: `scheduler:${scheduledRun.id}`,
|
|
108
|
+
displayName: "Bee Scheduler",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const info = await webClient.users.info({ user: scheduledRun.target.slackUserId });
|
|
113
|
+
const user = info.user;
|
|
114
|
+
return {
|
|
115
|
+
userId: scheduledRun.target.slackUserId,
|
|
116
|
+
userName: user?.name,
|
|
117
|
+
displayName: user?.profile?.display_name || user?.real_name || user?.name,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
log.logWarning(`Could not resolve Slack user ${scheduledRun.target.slackUserId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
122
|
+
return {
|
|
123
|
+
userId: scheduledRun.target.slackUserId,
|
|
124
|
+
displayName: scheduledRun.target.slackUserId,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function streamScheduledTurn(workerClient, sink, input) {
|
|
129
|
+
const state = {
|
|
130
|
+
latestText: "_Working..._",
|
|
131
|
+
itemTexts: new Map(),
|
|
132
|
+
};
|
|
133
|
+
const request = {
|
|
134
|
+
sessionId: input.sessionId,
|
|
135
|
+
threadId: input.threadId,
|
|
136
|
+
turnId: newTurnId(),
|
|
137
|
+
conversation: input.conversation,
|
|
138
|
+
actor: input.actor,
|
|
139
|
+
message: input.message,
|
|
140
|
+
attachments: input.attachments,
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
await workerClient.streamTurn(input.worker, request, async (event) => {
|
|
144
|
+
await handleScheduledEvent(sink, input.output, event, state);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
+
await sink.postMessage(input.output, `_Scheduled gateway error: ${message}_`);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function handleScheduledEvent(sink, output, event, state) {
|
|
154
|
+
if (event.name === "run.started") {
|
|
155
|
+
state.statusRef = await sink.postMessage(output, state.latestText);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (event.name === "run.completed") {
|
|
159
|
+
if (!state.statusRef) {
|
|
160
|
+
state.statusRef = await sink.postMessage(output, state.latestText);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (event.name === "run.failed") {
|
|
165
|
+
const errorText = `_Error: ${asRunFailedPayload(event).error}_`;
|
|
166
|
+
if (state.statusRef) {
|
|
167
|
+
await sink.updateMessage(output, state.statusRef, errorText);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
state.statusRef = await sink.postMessage(output, errorText);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (event.name === "approval.requested") {
|
|
175
|
+
await sink.postMessage(output, `Approval requested: ${asApprovalRequestedPayload(event).summary}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (event.name === "item.appended") {
|
|
179
|
+
const payload = asItemAppendedPayload(event);
|
|
180
|
+
const text = renderParts(payload.item.parts);
|
|
181
|
+
state.itemTexts.set(payload.item.id, text);
|
|
182
|
+
if (payload.item.kind === "artifact") {
|
|
183
|
+
const artifact = firstArtifactRef(payload.item.parts);
|
|
184
|
+
if (artifact && artifactHasPayload(artifact)) {
|
|
185
|
+
try {
|
|
186
|
+
await sink.publishArtifact(output, artifact);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
+
await sink.postMessage(output, `${text}\n_Artifact upload failed: ${message}_`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await sink.postMessage(output, text);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
state.latestText = text;
|
|
199
|
+
if (state.statusRef) {
|
|
200
|
+
await sink.updateMessage(output, state.statusRef, text);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
state.statusRef = await sink.postMessage(output, text);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (event.name === "item.updated") {
|
|
208
|
+
const payload = asItemUpdatedPayload(event);
|
|
209
|
+
const current = state.itemTexts.get(payload.itemId) || "";
|
|
210
|
+
const appended = renderParts(payload.appendParts || []);
|
|
211
|
+
const next = current ? `${current}${appended}` : appended;
|
|
212
|
+
state.itemTexts.set(payload.itemId, next);
|
|
213
|
+
state.latestText = next;
|
|
214
|
+
if (state.statusRef) {
|
|
215
|
+
await sink.updateMessage(output, state.statusRef, next);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
state.statusRef = await sink.postMessage(output, next);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function firstArtifactRef(parts) {
|
|
223
|
+
const part = parts.find((entry) => entry.kind === "artifactRef");
|
|
224
|
+
if (!part)
|
|
225
|
+
return undefined;
|
|
226
|
+
return {
|
|
227
|
+
artifactId: String(part.artifactId || "artifact"),
|
|
228
|
+
blobKey: typeof part.blobKey === "string" ? part.blobKey : undefined,
|
|
229
|
+
name: typeof part.name === "string" ? part.name : undefined,
|
|
230
|
+
title: typeof part.title === "string" ? part.title : undefined,
|
|
231
|
+
mimeType: typeof part.mimeType === "string" ? part.mimeType : undefined,
|
|
232
|
+
uri: typeof part.uri === "string" ? part.uri : undefined,
|
|
233
|
+
sizeBytes: typeof part.sizeBytes === "number" ? part.sizeBytes : undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function artifactHasPayload(artifact) {
|
|
237
|
+
return !!(artifact.uri || artifact.blobKey);
|
|
238
|
+
}
|
|
239
|
+
function renderParts(parts) {
|
|
240
|
+
return parts
|
|
241
|
+
.map((part) => {
|
|
242
|
+
if (part.kind === "text")
|
|
243
|
+
return String(part.text || "");
|
|
244
|
+
if (part.kind === "status")
|
|
245
|
+
return String(part.status || "");
|
|
246
|
+
if (part.kind === "artifactRef") {
|
|
247
|
+
return `Artifact: ${String(part.title || part.name || part.artifactId || "artifact")}`;
|
|
248
|
+
}
|
|
249
|
+
if (part.kind === "approval" || part.kind === "choice") {
|
|
250
|
+
return `${String(part.title || "")}\n${String(part.summary || "")}`.trim();
|
|
251
|
+
}
|
|
252
|
+
if (part.kind === "form")
|
|
253
|
+
return String(part.title || "");
|
|
254
|
+
if (part.kind === "log")
|
|
255
|
+
return String(part.text || "");
|
|
256
|
+
if (part.kind === "patch" || part.kind === "diff") {
|
|
257
|
+
const files = Array.isArray(part.files) ? part.files : [];
|
|
258
|
+
return files.map((entry) => `File: ${String(entry.path || "")}`).join("\n");
|
|
259
|
+
}
|
|
260
|
+
return JSON.stringify(part);
|
|
261
|
+
})
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join("\n");
|
|
264
|
+
}
|
|
265
|
+
function validateScheduledRunConfig(config, source) {
|
|
266
|
+
if (!config.id)
|
|
267
|
+
throw new Error(`Missing id in ${source}`);
|
|
268
|
+
if (!config.routeId)
|
|
269
|
+
throw new Error(`Missing routeId in ${source}`);
|
|
270
|
+
if (!config.text)
|
|
271
|
+
throw new Error(`Missing text in ${source}`);
|
|
272
|
+
if (!config.target)
|
|
273
|
+
throw new Error(`Missing target in ${source}`);
|
|
274
|
+
if (!config.target.slackUserId && !config.target.channelId) {
|
|
275
|
+
throw new Error(`Missing target.slackUserId or target.channelId in ${source}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function asRunFailedPayload(event) {
|
|
279
|
+
return event.payload;
|
|
280
|
+
}
|
|
281
|
+
function asApprovalRequestedPayload(event) {
|
|
282
|
+
return event.payload;
|
|
283
|
+
}
|
|
284
|
+
function asItemAppendedPayload(event) {
|
|
285
|
+
return event.payload;
|
|
286
|
+
}
|
|
287
|
+
function asItemUpdatedPayload(event) {
|
|
288
|
+
return event.payload;
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=scheduled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduled.js","sourceRoot":"","sources":["../src/scheduled.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype ArtifactRef,\n\ttype BeeResolvedTurn,\n\ttype BeeRunEvent,\n\ttype BeeWorkerClient,\n\tbuildConversationId,\n\tbuildSessionKey,\n\tcreateNatsBeeClient,\n\tLocalFileBlobStore,\n\tnewTurnId,\n\ttype TransportOutputTarget,\n} from \"@jobmatchme/bee-gate\";\nimport { WebClient } from \"@slack/web-api\";\nimport { readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { loadConfig } from \"./config.js\";\nimport * as log from \"./log.js\";\nimport { SlackSink } from \"./slack-sink.js\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport interface SlackScheduledRunConfig {\n\tid: string;\n\trouteId: string;\n\ttext: string;\n\ttarget: {\n\t\tslackUserId?: string;\n\t\tchannelId?: string;\n\t\tthreadTs?: string;\n\t};\n\tactor?: {\n\t\tuserId: string;\n\t\tuserName?: string;\n\t\tdisplayName?: string;\n\t};\n\tsessionId?: string;\n\tsessionPrefix?: string;\n\tconversationId?: string;\n}\n\ninterface RenderState {\n\tstatusRef?: string;\n\tlatestText: string;\n\titemTexts: Map<string, string>;\n}\n\ntype ItemPartLike = { kind: string; [key: string]: unknown };\n\ntype ItemAppendedPayload = {\n\teventType: \"item.appended\";\n\titem: { id: string; kind: string; role: string; parts: ItemPartLike[] };\n};\n\ntype ItemUpdatedPayload = {\n\teventType: \"item.updated\";\n\titemId: string;\n\tappendParts?: ItemPartLike[];\n};\n\ntype RunFailedPayload = {\n\teventType: \"run.failed\";\n\terror: string;\n};\n\ntype ApprovalRequestedPayload = {\n\tsummary: string;\n};\n\nexport function loadScheduledRunConfig(jobPath?: string): SlackScheduledRunConfig {\n\tconst path = jobPath || process.env.BEE_SLACK_SCHEDULED_RUN_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\n\t\t\t\"Missing scheduled run config path; pass it as second argument or set BEE_SLACK_SCHEDULED_RUN_CONFIG\",\n\t\t);\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackScheduledRunConfig;\n\tvalidateScheduledRunConfig(config, fullPath);\n\treturn config;\n}\n\nexport async function runScheduledSlackTurnFromFiles(configPath?: string, jobPath?: string): Promise<void> {\n\tconst gatewayConfig = loadConfig(configPath);\n\tconst scheduledRun = loadScheduledRunConfig(jobPath);\n\tawait runScheduledSlackTurn(gatewayConfig, scheduledRun);\n}\n\nexport async function runScheduledSlackTurn(\n\tgatewayConfig: SlackGatewayConfig,\n\tscheduledRun: SlackScheduledRunConfig,\n): Promise<void> {\n\tvalidateScheduledRunConfig(scheduledRun, \"scheduled run config\");\n\tconst route = gatewayConfig.routes.find((candidate) => candidate.id === scheduledRun.routeId);\n\tif (!route) {\n\t\tthrow new Error(`Scheduled run ${scheduledRun.id} references unknown routeId ${scheduledRun.routeId}`);\n\t}\n\n\tconst webClient = new WebClient(gatewayConfig.botToken);\n\tconst blobStore = new LocalFileBlobStore(\n\t\tprocess.env.BEE_SLACK_BLOB_STORE_ROOT ||\n\t\t\tprocess.env.BEE_BLOB_STORE_ROOT ||\n\t\t\tprocess.env.HUDAI_BLOB_STORE_ROOT ||\n\t\t\tjoin(process.cwd(), \".bee-blob-store\"),\n\t);\n\tconst sink = new SlackSink(webClient, blobStore);\n\tconst workerClient = await createNatsBeeClient(gatewayConfig.nats);\n\n\ttry {\n\t\tconst auth = await webClient.auth.test();\n\t\tconst teamId = String(auth.team_id || \"unknown-team\");\n\t\tconst output = await resolveScheduledOutputTarget(webClient, scheduledRun);\n\t\tconst actor = await resolveScheduledActor(webClient, scheduledRun);\n\t\tif (!output.threadId) {\n\t\t\toutput.threadId = await sink.postMessage(\n\t\t\t\t{ channelId: output.channelId },\n\t\t\t\t`:alarm_clock: Scheduled Bee run \\`${scheduledRun.id}\\` started. Reply in this thread to continue the session.`,\n\t\t\t);\n\t\t}\n\t\tconst slackConversationId = buildConversationId([\n\t\t\t\"slack\",\n\t\t\tteamId,\n\t\t\toutput.channelId || \"unknown-channel\",\n\t\t\toutput.threadId,\n\t\t]);\n\t\tconst conversationId = scheduledRun.conversationId || slackConversationId;\n\t\tconst sessionBase =\n\t\t\troute.session?.strategy === \"channel\" ? output.channelId || \"unknown-channel\" : output.threadId;\n\t\tconst routeSessionId = buildSessionKey(\n\t\t\tscheduledRun.sessionPrefix || route.session?.prefix || route.id,\n\t\t\tbuildConversationId([\"slack\", teamId, output.channelId || \"unknown-channel\", sessionBase || output.threadId]),\n\t\t);\n\t\tconst sessionId = scheduledRun.sessionId || routeSessionId;\n\n\t\tconst input: BeeResolvedTurn = {\n\t\t\tsessionId,\n\t\t\tthreadId: output.threadId,\n\t\t\tworker: route.worker,\n\t\t\tconversation: {\n\t\t\t\ttransport: \"slack\",\n\t\t\t\tconversationId,\n\t\t\t},\n\t\t\tactor,\n\t\t\tmessage: {\n\t\t\t\ttext: scheduledRun.text,\n\t\t\t},\n\t\t\tattachments: [],\n\t\t\toutput,\n\t\t};\n\n\t\tlog.logInfo(`Starting scheduled Slack run ${scheduledRun.id} on route ${route.id}`);\n\t\tawait streamScheduledTurn(workerClient, sink, input);\n\t\tlog.logInfo(`Completed scheduled Slack run ${scheduledRun.id}`);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tlog.logError(`Scheduled Slack run ${scheduledRun.id} failed`, message);\n\t\tthrow error;\n\t} finally {\n\t\tawait workerClient.close?.();\n\t}\n}\n\nasync function resolveScheduledOutputTarget(\n\twebClient: WebClient,\n\tscheduledRun: SlackScheduledRunConfig,\n): Promise<TransportOutputTarget> {\n\tif (scheduledRun.target.channelId) {\n\t\treturn {\n\t\t\tchannelId: scheduledRun.target.channelId,\n\t\t\tthreadId: scheduledRun.target.threadTs,\n\t\t};\n\t}\n\n\tif (!scheduledRun.target.slackUserId) {\n\t\tthrow new Error(`Scheduled run ${scheduledRun.id} needs target.slackUserId or target.channelId`);\n\t}\n\n\tconst result = await webClient.conversations.open({ users: scheduledRun.target.slackUserId });\n\tconst channel = result.channel as { id?: string } | undefined;\n\tif (!channel?.id) {\n\t\tthrow new Error(`Slack conversations.open did not return a channel for user ${scheduledRun.target.slackUserId}`);\n\t}\n\treturn {\n\t\tchannelId: channel.id,\n\t\tthreadId: scheduledRun.target.threadTs,\n\t};\n}\n\nasync function resolveScheduledActor(\n\twebClient: WebClient,\n\tscheduledRun: SlackScheduledRunConfig,\n): Promise<BeeResolvedTurn[\"actor\"]> {\n\tif (scheduledRun.actor) return scheduledRun.actor;\n\tif (!scheduledRun.target.slackUserId) {\n\t\treturn {\n\t\t\tuserId: `scheduler:${scheduledRun.id}`,\n\t\t\tdisplayName: \"Bee Scheduler\",\n\t\t};\n\t}\n\n\ttry {\n\t\tconst info = await webClient.users.info({ user: scheduledRun.target.slackUserId });\n\t\tconst user = info.user as\n\t\t\t| { id?: string; name?: string; real_name?: string; profile?: { display_name?: string } }\n\t\t\t| undefined;\n\t\treturn {\n\t\t\tuserId: scheduledRun.target.slackUserId,\n\t\t\tuserName: user?.name,\n\t\t\tdisplayName: user?.profile?.display_name || user?.real_name || user?.name,\n\t\t};\n\t} catch (error) {\n\t\tlog.logWarning(\n\t\t\t`Could not resolve Slack user ${scheduledRun.target.slackUserId}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t\treturn {\n\t\t\tuserId: scheduledRun.target.slackUserId,\n\t\t\tdisplayName: scheduledRun.target.slackUserId,\n\t\t};\n\t}\n}\n\nasync function streamScheduledTurn(\n\tworkerClient: BeeWorkerClient,\n\tsink: SlackSink,\n\tinput: BeeResolvedTurn,\n): Promise<void> {\n\tconst state: RenderState = {\n\t\tlatestText: \"_Working..._\",\n\t\titemTexts: new Map<string, string>(),\n\t};\n\tconst request = {\n\t\tsessionId: input.sessionId,\n\t\tthreadId: input.threadId,\n\t\tturnId: newTurnId(),\n\t\tconversation: input.conversation,\n\t\tactor: input.actor,\n\t\tmessage: input.message,\n\t\tattachments: input.attachments,\n\t};\n\n\ttry {\n\t\tawait workerClient.streamTurn(input.worker, request, async (event) => {\n\t\t\tawait handleScheduledEvent(sink, input.output, event, state);\n\t\t});\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tawait sink.postMessage(input.output, `_Scheduled gateway error: ${message}_`);\n\t\tthrow error;\n\t}\n}\n\nasync function handleScheduledEvent(\n\tsink: SlackSink,\n\toutput: TransportOutputTarget,\n\tevent: BeeRunEvent,\n\tstate: RenderState,\n): Promise<void> {\n\tif (event.name === \"run.started\") {\n\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\n\t\treturn;\n\t}\n\n\tif (event.name === \"run.completed\") {\n\t\tif (!state.statusRef) {\n\t\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (event.name === \"run.failed\") {\n\t\tconst errorText = `_Error: ${asRunFailedPayload(event).error}_`;\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, errorText);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, errorText);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (event.name === \"approval.requested\") {\n\t\tawait sink.postMessage(output, `Approval requested: ${asApprovalRequestedPayload(event).summary}`);\n\t\treturn;\n\t}\n\n\tif (event.name === \"item.appended\") {\n\t\tconst payload = asItemAppendedPayload(event);\n\t\tconst text = renderParts(payload.item.parts);\n\t\tstate.itemTexts.set(payload.item.id, text);\n\t\tif (payload.item.kind === \"artifact\") {\n\t\t\tconst artifact = firstArtifactRef(payload.item.parts);\n\t\t\tif (artifact && artifactHasPayload(artifact)) {\n\t\t\t\ttry {\n\t\t\t\t\tawait sink.publishArtifact(output, artifact);\n\t\t\t\t\treturn;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tawait sink.postMessage(output, `${text}\\n_Artifact upload failed: ${message}_`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait sink.postMessage(output, text);\n\t\t\treturn;\n\t\t}\n\t\tstate.latestText = text;\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, text);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, text);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (event.name === \"item.updated\") {\n\t\tconst payload = asItemUpdatedPayload(event);\n\t\tconst current = state.itemTexts.get(payload.itemId) || \"\";\n\t\tconst appended = renderParts(payload.appendParts || []);\n\t\tconst next = current ? `${current}${appended}` : appended;\n\t\tstate.itemTexts.set(payload.itemId, next);\n\t\tstate.latestText = next;\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, next);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, next);\n\t\t}\n\t}\n}\n\nfunction firstArtifactRef(parts: ItemPartLike[]): ArtifactRef | undefined {\n\tconst part = parts.find((entry) => entry.kind === \"artifactRef\");\n\tif (!part) return undefined;\n\treturn {\n\t\tartifactId: String(part.artifactId || \"artifact\"),\n\t\tblobKey: typeof part.blobKey === \"string\" ? part.blobKey : undefined,\n\t\tname: typeof part.name === \"string\" ? part.name : undefined,\n\t\ttitle: typeof part.title === \"string\" ? part.title : undefined,\n\t\tmimeType: typeof part.mimeType === \"string\" ? part.mimeType : undefined,\n\t\turi: typeof part.uri === \"string\" ? part.uri : undefined,\n\t\tsizeBytes: typeof part.sizeBytes === \"number\" ? part.sizeBytes : undefined,\n\t};\n}\n\nfunction artifactHasPayload(artifact: ArtifactRef): boolean {\n\treturn !!(artifact.uri || artifact.blobKey);\n}\n\nfunction renderParts(parts: ItemPartLike[]): string {\n\treturn parts\n\t\t.map((part) => {\n\t\t\tif (part.kind === \"text\") return String(part.text || \"\");\n\t\t\tif (part.kind === \"status\") return String(part.status || \"\");\n\t\t\tif (part.kind === \"artifactRef\") {\n\t\t\t\treturn `Artifact: ${String(part.title || part.name || part.artifactId || \"artifact\")}`;\n\t\t\t}\n\t\t\tif (part.kind === \"approval\" || part.kind === \"choice\") {\n\t\t\t\treturn `${String(part.title || \"\")}\\n${String(part.summary || \"\")}`.trim();\n\t\t\t}\n\t\t\tif (part.kind === \"form\") return String(part.title || \"\");\n\t\t\tif (part.kind === \"log\") return String(part.text || \"\");\n\t\t\tif (part.kind === \"patch\" || part.kind === \"diff\") {\n\t\t\t\tconst files = Array.isArray(part.files) ? (part.files as Array<{ path?: unknown }>) : [];\n\t\t\t\treturn files.map((entry) => `File: ${String(entry.path || \"\")}`).join(\"\\n\");\n\t\t\t}\n\t\t\treturn JSON.stringify(part);\n\t\t})\n\t\t.filter(Boolean)\n\t\t.join(\"\\n\");\n}\n\nfunction validateScheduledRunConfig(config: SlackScheduledRunConfig, source: string): void {\n\tif (!config.id) throw new Error(`Missing id in ${source}`);\n\tif (!config.routeId) throw new Error(`Missing routeId in ${source}`);\n\tif (!config.text) throw new Error(`Missing text in ${source}`);\n\tif (!config.target) throw new Error(`Missing target in ${source}`);\n\tif (!config.target.slackUserId && !config.target.channelId) {\n\t\tthrow new Error(`Missing target.slackUserId or target.channelId in ${source}`);\n\t}\n}\n\nfunction asRunFailedPayload(event: BeeRunEvent): RunFailedPayload {\n\treturn event.payload as RunFailedPayload;\n}\n\nfunction asApprovalRequestedPayload(event: BeeRunEvent): ApprovalRequestedPayload {\n\treturn event.payload as ApprovalRequestedPayload;\n}\n\nfunction asItemAppendedPayload(event: BeeRunEvent): ItemAppendedPayload {\n\treturn event.payload as ItemAppendedPayload;\n}\n\nfunction asItemUpdatedPayload(event: BeeRunEvent): ItemUpdatedPayload {\n\treturn event.payload as ItemUpdatedPayload;\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArtifactRef, BlobStore, TransportOutputTarget, TransportSink } from "@jobmatchme/bee-gate";
|
|
2
|
+
import type { WebClient } from "@slack/web-api";
|
|
3
|
+
export declare class SlackSink implements TransportSink<string> {
|
|
4
|
+
private webClient;
|
|
5
|
+
private blobStore;
|
|
6
|
+
constructor(webClient: WebClient, blobStore: BlobStore);
|
|
7
|
+
postMessage(target: TransportOutputTarget, text: string): Promise<string>;
|
|
8
|
+
updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void>;
|
|
9
|
+
publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=slack-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,qBAAa,SAAU,YAAW,aAAa,CAAC,MAAM,CAAC;IAErD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,SAAS;IAFlB,YACS,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACzB;IAEE,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAW9E;IAEK,aAAa,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3F;IAEK,eAAe,CAAC,MAAM,EAAE,qBAAqB,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzF;CACD","sourcesContent":["import type { ArtifactRef, BlobStore, TransportOutputTarget, TransportSink } from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport class 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: createReadStream(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"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createReadStream } from "fs";
|
|
2
|
+
export class SlackSink {
|
|
3
|
+
webClient;
|
|
4
|
+
blobStore;
|
|
5
|
+
constructor(webClient, blobStore) {
|
|
6
|
+
this.webClient = webClient;
|
|
7
|
+
this.blobStore = blobStore;
|
|
8
|
+
}
|
|
9
|
+
async postMessage(target, text) {
|
|
10
|
+
if (!target.channelId) {
|
|
11
|
+
throw new Error("Missing Slack channel id");
|
|
12
|
+
}
|
|
13
|
+
const result = await this.webClient.chat.postMessage({
|
|
14
|
+
channel: target.channelId,
|
|
15
|
+
text,
|
|
16
|
+
thread_ts: target.threadId,
|
|
17
|
+
});
|
|
18
|
+
return result.ts;
|
|
19
|
+
}
|
|
20
|
+
async updateMessage(target, ref, text) {
|
|
21
|
+
if (!target.channelId) {
|
|
22
|
+
throw new Error("Missing Slack channel id");
|
|
23
|
+
}
|
|
24
|
+
await this.webClient.chat.update({
|
|
25
|
+
channel: target.channelId,
|
|
26
|
+
ts: ref,
|
|
27
|
+
text,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async publishArtifact(target, artifact) {
|
|
31
|
+
if (!target.channelId) {
|
|
32
|
+
throw new Error("Missing Slack channel id");
|
|
33
|
+
}
|
|
34
|
+
const materialized = await this.blobStore.materialize(artifact);
|
|
35
|
+
try {
|
|
36
|
+
await this.webClient.files.uploadV2({
|
|
37
|
+
channel_id: target.channelId,
|
|
38
|
+
thread_ts: target.threadId,
|
|
39
|
+
file: createReadStream(materialized.path),
|
|
40
|
+
filename: materialized.filename,
|
|
41
|
+
title: artifact.title || artifact.name || materialized.filename,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await materialized.cleanup?.();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=slack-sink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAEtC,MAAM,OAAO,SAAS;IAEZ,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,gBAAgB,CAAC,YAAY,CAAC,IAAI,CAAC;gBACzC,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","sourcesContent":["import type { ArtifactRef, BlobStore, TransportOutputTarget, TransportSink } from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport class 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: createReadStream(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"]}
|