@jobmatchme/bee-slack 0.1.7 → 0.1.9

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 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
@@ -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
- class SlackSink {
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;
@@ -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
@@ -1,5 +1,7 @@
1
1
  export * from "./config.js";
2
2
  export * from "./gateway.js";
3
3
  export * from "./router.js";
4
+ export * from "./scheduled.js";
5
+ export * from "./slack-sink.js";
4
6
  export * from "./types.js";
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -1,5 +1,7 @@
1
1
  export * from "./config.js";
2
2
  export * from "./gateway.js";
3
3
  export * from "./router.js";
4
+ export * from "./scheduled.js";
5
+ export * from "./slack-sink.js";
4
6
  export * from "./types.js";
5
7
  //# sourceMappingURL=index.js.map
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"]}
@@ -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\nawait startGatewayFromEnv(process.argv[2]);\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
- await startGatewayFromEnv(process.argv[2]);
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;AAEnD,MAAM,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport { startGatewayFromEnv } from \"./gateway.js\";\n\nawait startGatewayFromEnv(process.argv[2]);\n"]}
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,24 @@
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
+ export declare function renderScheduledFinalMessage(requestText: string | undefined, responseText: string): string;
24
+ //# 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;AA+BD,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,CAoEf;AA8LD,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAUzG","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\trequestText?: 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\tlet statusRef: string | undefined;\n\t\tif (!output.threadId) {\n\t\t\tstatusRef = await sink.postMessage({ channelId: output.channelId }, \"_Working..._\");\n\t\t\toutput.threadId = statusRef;\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, statusRef);\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\tstatusRef?: string,\n): Promise<void> {\n\tconst state: RenderState = {\n\t\tstatusRef,\n\t\tlatestText: \"_Working..._\",\n\t\trequestText: input.message.text,\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\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.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 = renderScheduledFinalMessage(state.requestText, text);\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, state.latestText);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\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 = renderScheduledFinalMessage(state.requestText, next);\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, state.latestText);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\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\nexport function renderScheduledFinalMessage(requestText: string | undefined, responseText: string): string {\n\tconst request = requestText?.trim();\n\tif (!request) return responseText;\n\n\tconst quotedRequest = request\n\t\t.split(/\\r?\\n/)\n\t\t.map((line) => `> ${escapeSlackText(line)}`)\n\t\t.join(\"\\n\");\n\n\treturn [\"*Request:*\", quotedRequest, \"*Antwort:*\", responseText].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction escapeSlackText(text: string): string {\n\treturn text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\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,309 @@
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
+ let statusRef;
42
+ if (!output.threadId) {
43
+ statusRef = await sink.postMessage({ channelId: output.channelId }, "_Working..._");
44
+ output.threadId = statusRef;
45
+ }
46
+ const slackConversationId = buildConversationId([
47
+ "slack",
48
+ teamId,
49
+ output.channelId || "unknown-channel",
50
+ output.threadId,
51
+ ]);
52
+ const conversationId = scheduledRun.conversationId || slackConversationId;
53
+ const sessionBase = route.session?.strategy === "channel" ? output.channelId || "unknown-channel" : output.threadId;
54
+ const routeSessionId = buildSessionKey(scheduledRun.sessionPrefix || route.session?.prefix || route.id, buildConversationId(["slack", teamId, output.channelId || "unknown-channel", sessionBase || output.threadId]));
55
+ const sessionId = scheduledRun.sessionId || routeSessionId;
56
+ const input = {
57
+ sessionId,
58
+ threadId: output.threadId,
59
+ worker: route.worker,
60
+ conversation: {
61
+ transport: "slack",
62
+ conversationId,
63
+ },
64
+ actor,
65
+ message: {
66
+ text: scheduledRun.text,
67
+ },
68
+ attachments: [],
69
+ output,
70
+ };
71
+ log.logInfo(`Starting scheduled Slack run ${scheduledRun.id} on route ${route.id}`);
72
+ await streamScheduledTurn(workerClient, sink, input, statusRef);
73
+ log.logInfo(`Completed scheduled Slack run ${scheduledRun.id}`);
74
+ }
75
+ catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ log.logError(`Scheduled Slack run ${scheduledRun.id} failed`, message);
78
+ throw error;
79
+ }
80
+ finally {
81
+ await workerClient.close?.();
82
+ }
83
+ }
84
+ async function resolveScheduledOutputTarget(webClient, scheduledRun) {
85
+ if (scheduledRun.target.channelId) {
86
+ return {
87
+ channelId: scheduledRun.target.channelId,
88
+ threadId: scheduledRun.target.threadTs,
89
+ };
90
+ }
91
+ if (!scheduledRun.target.slackUserId) {
92
+ throw new Error(`Scheduled run ${scheduledRun.id} needs target.slackUserId or target.channelId`);
93
+ }
94
+ const result = await webClient.conversations.open({ users: scheduledRun.target.slackUserId });
95
+ const channel = result.channel;
96
+ if (!channel?.id) {
97
+ throw new Error(`Slack conversations.open did not return a channel for user ${scheduledRun.target.slackUserId}`);
98
+ }
99
+ return {
100
+ channelId: channel.id,
101
+ threadId: scheduledRun.target.threadTs,
102
+ };
103
+ }
104
+ async function resolveScheduledActor(webClient, scheduledRun) {
105
+ if (scheduledRun.actor)
106
+ return scheduledRun.actor;
107
+ if (!scheduledRun.target.slackUserId) {
108
+ return {
109
+ userId: `scheduler:${scheduledRun.id}`,
110
+ displayName: "Bee Scheduler",
111
+ };
112
+ }
113
+ try {
114
+ const info = await webClient.users.info({ user: scheduledRun.target.slackUserId });
115
+ const user = info.user;
116
+ return {
117
+ userId: scheduledRun.target.slackUserId,
118
+ userName: user?.name,
119
+ displayName: user?.profile?.display_name || user?.real_name || user?.name,
120
+ };
121
+ }
122
+ catch (error) {
123
+ log.logWarning(`Could not resolve Slack user ${scheduledRun.target.slackUserId}: ${error instanceof Error ? error.message : String(error)}`);
124
+ return {
125
+ userId: scheduledRun.target.slackUserId,
126
+ displayName: scheduledRun.target.slackUserId,
127
+ };
128
+ }
129
+ }
130
+ async function streamScheduledTurn(workerClient, sink, input, statusRef) {
131
+ const state = {
132
+ statusRef,
133
+ latestText: "_Working..._",
134
+ requestText: input.message.text,
135
+ itemTexts: new Map(),
136
+ };
137
+ const request = {
138
+ sessionId: input.sessionId,
139
+ threadId: input.threadId,
140
+ turnId: newTurnId(),
141
+ conversation: input.conversation,
142
+ actor: input.actor,
143
+ message: input.message,
144
+ attachments: input.attachments,
145
+ };
146
+ try {
147
+ await workerClient.streamTurn(input.worker, request, async (event) => {
148
+ await handleScheduledEvent(sink, input.output, event, state);
149
+ });
150
+ }
151
+ catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error);
153
+ await sink.postMessage(input.output, `_Scheduled gateway error: ${message}_`);
154
+ throw error;
155
+ }
156
+ }
157
+ async function handleScheduledEvent(sink, output, event, state) {
158
+ if (event.name === "run.started") {
159
+ if (!state.statusRef) {
160
+ state.statusRef = await sink.postMessage(output, state.latestText);
161
+ }
162
+ return;
163
+ }
164
+ if (event.name === "run.completed") {
165
+ if (!state.statusRef) {
166
+ state.statusRef = await sink.postMessage(output, state.latestText);
167
+ }
168
+ return;
169
+ }
170
+ if (event.name === "run.failed") {
171
+ const errorText = `_Error: ${asRunFailedPayload(event).error}_`;
172
+ if (state.statusRef) {
173
+ await sink.updateMessage(output, state.statusRef, errorText);
174
+ }
175
+ else {
176
+ state.statusRef = await sink.postMessage(output, errorText);
177
+ }
178
+ return;
179
+ }
180
+ if (event.name === "approval.requested") {
181
+ await sink.postMessage(output, `Approval requested: ${asApprovalRequestedPayload(event).summary}`);
182
+ return;
183
+ }
184
+ if (event.name === "item.appended") {
185
+ const payload = asItemAppendedPayload(event);
186
+ const text = renderParts(payload.item.parts);
187
+ state.itemTexts.set(payload.item.id, text);
188
+ if (payload.item.kind === "artifact") {
189
+ const artifact = firstArtifactRef(payload.item.parts);
190
+ if (artifact && artifactHasPayload(artifact)) {
191
+ try {
192
+ await sink.publishArtifact(output, artifact);
193
+ return;
194
+ }
195
+ catch (error) {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ await sink.postMessage(output, `${text}\n_Artifact upload failed: ${message}_`);
198
+ return;
199
+ }
200
+ }
201
+ await sink.postMessage(output, text);
202
+ return;
203
+ }
204
+ state.latestText = renderScheduledFinalMessage(state.requestText, text);
205
+ if (state.statusRef) {
206
+ await sink.updateMessage(output, state.statusRef, state.latestText);
207
+ }
208
+ else {
209
+ state.statusRef = await sink.postMessage(output, state.latestText);
210
+ }
211
+ return;
212
+ }
213
+ if (event.name === "item.updated") {
214
+ const payload = asItemUpdatedPayload(event);
215
+ const current = state.itemTexts.get(payload.itemId) || "";
216
+ const appended = renderParts(payload.appendParts || []);
217
+ const next = current ? `${current}${appended}` : appended;
218
+ state.itemTexts.set(payload.itemId, next);
219
+ state.latestText = renderScheduledFinalMessage(state.requestText, next);
220
+ if (state.statusRef) {
221
+ await sink.updateMessage(output, state.statusRef, state.latestText);
222
+ }
223
+ else {
224
+ state.statusRef = await sink.postMessage(output, state.latestText);
225
+ }
226
+ }
227
+ }
228
+ function firstArtifactRef(parts) {
229
+ const part = parts.find((entry) => entry.kind === "artifactRef");
230
+ if (!part)
231
+ return undefined;
232
+ return {
233
+ artifactId: String(part.artifactId || "artifact"),
234
+ blobKey: typeof part.blobKey === "string" ? part.blobKey : undefined,
235
+ name: typeof part.name === "string" ? part.name : undefined,
236
+ title: typeof part.title === "string" ? part.title : undefined,
237
+ mimeType: typeof part.mimeType === "string" ? part.mimeType : undefined,
238
+ uri: typeof part.uri === "string" ? part.uri : undefined,
239
+ sizeBytes: typeof part.sizeBytes === "number" ? part.sizeBytes : undefined,
240
+ };
241
+ }
242
+ function artifactHasPayload(artifact) {
243
+ return !!(artifact.uri || artifact.blobKey);
244
+ }
245
+ export function renderScheduledFinalMessage(requestText, responseText) {
246
+ const request = requestText?.trim();
247
+ if (!request)
248
+ return responseText;
249
+ const quotedRequest = request
250
+ .split(/\r?\n/)
251
+ .map((line) => `> ${escapeSlackText(line)}`)
252
+ .join("\n");
253
+ return ["*Request:*", quotedRequest, "*Antwort:*", responseText].filter(Boolean).join("\n\n");
254
+ }
255
+ function escapeSlackText(text) {
256
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
257
+ }
258
+ function renderParts(parts) {
259
+ return parts
260
+ .map((part) => {
261
+ if (part.kind === "text")
262
+ return String(part.text || "");
263
+ if (part.kind === "status")
264
+ return String(part.status || "");
265
+ if (part.kind === "artifactRef") {
266
+ return `Artifact: ${String(part.title || part.name || part.artifactId || "artifact")}`;
267
+ }
268
+ if (part.kind === "approval" || part.kind === "choice") {
269
+ return `${String(part.title || "")}\n${String(part.summary || "")}`.trim();
270
+ }
271
+ if (part.kind === "form")
272
+ return String(part.title || "");
273
+ if (part.kind === "log")
274
+ return String(part.text || "");
275
+ if (part.kind === "patch" || part.kind === "diff") {
276
+ const files = Array.isArray(part.files) ? part.files : [];
277
+ return files.map((entry) => `File: ${String(entry.path || "")}`).join("\n");
278
+ }
279
+ return JSON.stringify(part);
280
+ })
281
+ .filter(Boolean)
282
+ .join("\n");
283
+ }
284
+ function validateScheduledRunConfig(config, source) {
285
+ if (!config.id)
286
+ throw new Error(`Missing id in ${source}`);
287
+ if (!config.routeId)
288
+ throw new Error(`Missing routeId in ${source}`);
289
+ if (!config.text)
290
+ throw new Error(`Missing text in ${source}`);
291
+ if (!config.target)
292
+ throw new Error(`Missing target in ${source}`);
293
+ if (!config.target.slackUserId && !config.target.channelId) {
294
+ throw new Error(`Missing target.slackUserId or target.channelId in ${source}`);
295
+ }
296
+ }
297
+ function asRunFailedPayload(event) {
298
+ return event.payload;
299
+ }
300
+ function asApprovalRequestedPayload(event) {
301
+ return event.payload;
302
+ }
303
+ function asItemAppendedPayload(event) {
304
+ return event.payload;
305
+ }
306
+ function asItemUpdatedPayload(event) {
307
+ return event.payload;
308
+ }
309
+ //# 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\trequestText?: 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\tlet statusRef: string | undefined;\n\t\tif (!output.threadId) {\n\t\t\tstatusRef = await sink.postMessage({ channelId: output.channelId }, \"_Working..._\");\n\t\t\toutput.threadId = statusRef;\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, statusRef);\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\tstatusRef?: string,\n): Promise<void> {\n\tconst state: RenderState = {\n\t\tstatusRef,\n\t\tlatestText: \"_Working..._\",\n\t\trequestText: input.message.text,\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\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.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 = renderScheduledFinalMessage(state.requestText, text);\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, state.latestText);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\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 = renderScheduledFinalMessage(state.requestText, next);\n\t\tif (state.statusRef) {\n\t\t\tawait sink.updateMessage(output, state.statusRef, state.latestText);\n\t\t} else {\n\t\t\tstate.statusRef = await sink.postMessage(output, state.latestText);\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\nexport function renderScheduledFinalMessage(requestText: string | undefined, responseText: string): string {\n\tconst request = requestText?.trim();\n\tif (!request) return responseText;\n\n\tconst quotedRequest = request\n\t\t.split(/\\r?\\n/)\n\t\t.map((line) => `> ${escapeSlackText(line)}`)\n\t\t.join(\"\\n\");\n\n\treturn [\"*Request:*\", quotedRequest, \"*Antwort:*\", responseText].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction escapeSlackText(text: string): string {\n\treturn text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobmatchme/bee-slack",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Slack adapter for Bee Gate and the Bee Dance protocol",
5
5
  "type": "module",
6
6
  "bin": {