@jobmatchme/bee-slack 0.1.13 → 0.1.14

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.
@@ -1,6 +1,11 @@
1
1
  import type { ActionUpdate, ArtifactRef, BlobStore, TransportOutputTarget, TransportSink, TransportStreamResult, TransportStreamStart } from "@jobmatchme/bee-gate";
2
2
  import type { WebClient } from "@slack/web-api";
3
3
  export declare const SLACK_STREAM_HEADER = "Ich bearbeite jetzt deine Anfrage...";
4
+ export declare const SLACK_PLAN_ACTIVE_TITLE = "Biene Willy arbeitet \uD83D\uDC1D";
5
+ export declare const SLACK_PLAN_COMPLETE_TITLE = "Biene Willy ist fertig \uD83C\uDF6F";
6
+ export declare const SLACK_PLAN_ERROR_TITLE = "Biene Willy braucht Hilfe \uD83D\uDC1D";
7
+ export declare const SLACK_RUN_TASK_ID = "bee-run";
8
+ export declare const SLACK_RUN_TASK_TITLE = "Anfrage bearbeiten";
4
9
  export declare const SLACK_TASK_TEXT_LIMIT = 256;
5
10
  export declare const SLACK_MARKDOWN_LIMIT = 12000;
6
11
  export declare const SLACK_MARKDOWN_TRUNCATION_SUFFIX = "\n\n_[Antwort auf 12.000 Zeichen gek\u00FCrzt]_";
@@ -16,6 +21,7 @@ export declare class SlackSink implements TransportSink<string> {
16
21
  private observer?;
17
22
  constructor(webClient: WebClient, blobStore: BlobStore, observer?: SlackSinkObserver | undefined);
18
23
  private readonly taskDetailsSent;
24
+ private readonly planStreams;
19
25
  postMessage(target: TransportOutputTarget, text: string): Promise<string>;
20
26
  updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void>;
21
27
  prepareStreamText(text: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,YAAY,EACZ,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,oBAAoB,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,mBAAmB,yCAAyC,CAAC;AAC1E,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,gCAAgC,oDAA+C,CAAC;AAS7F,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,MAAM,WAAW,iBAAiB;IACjC,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,OAAO,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACxE;AAED,qBAAa,SAAU,YAAW,aAAa,CAAC,MAAM,CAAC;IAErD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ,CAAC;IAHlB,YACS,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,QAAQ,CAAC,+BAAmB,EACjC;IACJ,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkC;IAE5D,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;IAEK,aAAa,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3F;IAED,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtC;IAEK,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB7F;IAEK,YAAY,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlG;IAEK,UAAU,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBzG;IAEK,eAAe,CAAC,MAAM,EAAE,qBAAqB,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzF;CACD","sourcesContent":["import type {\n\tActionUpdate,\n\tArtifactRef,\n\tBlobStore,\n\tTransportOutputTarget,\n\tTransportSink,\n\tTransportStreamResult,\n\tTransportStreamStart,\n} from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport const SLACK_STREAM_HEADER = \"Ich bearbeite jetzt deine Anfrage...\";\nexport const SLACK_TASK_TEXT_LIMIT = 256;\nexport const SLACK_MARKDOWN_LIMIT = 12_000;\nexport const SLACK_MARKDOWN_TRUNCATION_SUFFIX = \"\\n\\n_[Antwort auf 12.000 Zeichen gekürzt]_\";\n\nfunction truncateCodePoints(text: string, limit: number, suffix: string): string {\n\tconst codePoints = Array.from(text);\n\tif (codePoints.length <= limit) return text;\n\tconst suffixCodePoints = Array.from(suffix);\n\treturn `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join(\"\")}${suffix}`;\n}\n\nexport function truncateSlackTaskText(text: string): string {\n\treturn truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, \"…\");\n}\n\nexport function truncateSlackMarkdown(text: string): string {\n\treturn truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);\n}\n\nfunction requiredContextString(start: TransportStreamStart, key: string): string {\n\tconst value = start.context?.[key];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Missing Slack stream context ${key}`);\n\t}\n\treturn value;\n}\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\tprivate readonly taskDetailsSent = new Map<string, Set<string>>();\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\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\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\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tprepareStreamText(text: string): string {\n\t\treturn truncateSlackMarkdown(text);\n\t}\n\n\tasync startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tif (!target.threadId) throw new Error(\"Missing Slack stream thread timestamp\");\n\n\t\tconst headerChunk = {\n\t\t\ttype: \"blocks\",\n\t\t\tblocks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"header\",\n\t\t\t\t\ttext: { type: \"plain_text\", text: SLACK_STREAM_HEADER },\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst result = await this.webClient.chat.startStream({\n\t\t\tchannel: target.channelId,\n\t\t\tthread_ts: target.threadId,\n\t\t\trecipient_user_id: requiredContextString(start, \"recipientUserId\"),\n\t\t\trecipient_team_id: requiredContextString(start, \"recipientTeamId\"),\n\t\t\ttask_display_mode: start.presentation || \"timeline\",\n\t\t\t// Slack supports block chunks although the current SDK's chunk union omits them.\n\t\t\tchunks: [headerChunk] as never,\n\t\t});\n\t\tif (!result.ts) throw new Error(\"Slack chat.startStream did not return a message timestamp\");\n\t\treturn result.ts;\n\t}\n\n\tasync updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\n\t\tconst sentTaskIds = this.taskDetailsSent.get(ref) ?? new Set<string>();\n\t\tconst details = action.details === undefined || sentTaskIds.has(action.id) ? undefined : action.details;\n\t\tawait this.webClient.chat.appendStream({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tchunks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\tid: action.id,\n\t\t\t\t\ttitle: truncateSlackTaskText(action.title),\n\t\t\t\t\t...(details === undefined ? {} : { details: truncateSlackTaskText(details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tif (details !== undefined) {\n\t\t\tsentTaskIds.add(action.id);\n\t\t\tthis.taskDetailsSent.set(ref, sentTaskIds);\n\t\t}\n\t}\n\n\tasync stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tconst payload = {\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\t// The stream starts in chunk mode for header/task updates, so final text\n\t\t\t// must use the same mode. Top-level markdown_text is rejected by Slack\n\t\t\t// with streaming_mode_mismatch after chunk-based starts.\n\t\t\tchunks: [{ type: \"markdown_text\" as const, text: truncateSlackMarkdown(result.text) }],\n\t\t};\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t} catch {\n\t\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.taskDetailsSent.delete(ref);\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"]}
1
+ {"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,YAAY,EACZ,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,oBAAoB,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,mBAAmB,yCAAyC,CAAC;AAC1E,eAAO,MAAM,uBAAuB,sCAA2B,CAAC;AAChE,eAAO,MAAM,yBAAyB,wCAA6B,CAAC;AACpE,eAAO,MAAM,sBAAsB,2CAAgC,CAAC;AACpE,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAC3C,eAAO,MAAM,oBAAoB,uBAAuB,CAAC;AACzD,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,gCAAgC,oDAA+C,CAAC;AAS7F,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,MAAM,WAAW,iBAAiB;IACjC,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,OAAO,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACxE;AAED,qBAAa,SAAU,YAAW,aAAa,CAAC,MAAM,CAAC;IAErD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ,CAAC;IAHlB,YACS,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,QAAQ,CAAC,+BAAmB,EACjC;IACJ,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkC;IAClE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IAE3C,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;IAEK,aAAa,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3F;IAED,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtC;IAEK,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqC7F;IAEK,YAAY,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlG;IAEK,UAAU,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCzG;IAEK,eAAe,CAAC,MAAM,EAAE,qBAAqB,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzF;CACD","sourcesContent":["import type {\n\tActionUpdate,\n\tArtifactRef,\n\tBlobStore,\n\tTransportOutputTarget,\n\tTransportSink,\n\tTransportStreamResult,\n\tTransportStreamStart,\n} from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport const SLACK_STREAM_HEADER = \"Ich bearbeite jetzt deine Anfrage...\";\nexport const SLACK_PLAN_ACTIVE_TITLE = \"Biene Willy arbeitet 🐝\";\nexport const SLACK_PLAN_COMPLETE_TITLE = \"Biene Willy ist fertig 🍯\";\nexport const SLACK_PLAN_ERROR_TITLE = \"Biene Willy braucht Hilfe 🐝\";\nexport const SLACK_RUN_TASK_ID = \"bee-run\";\nexport const SLACK_RUN_TASK_TITLE = \"Anfrage bearbeiten\";\nexport const SLACK_TASK_TEXT_LIMIT = 256;\nexport const SLACK_MARKDOWN_LIMIT = 12_000;\nexport const SLACK_MARKDOWN_TRUNCATION_SUFFIX = \"\\n\\n_[Antwort auf 12.000 Zeichen gekürzt]_\";\n\nfunction truncateCodePoints(text: string, limit: number, suffix: string): string {\n\tconst codePoints = Array.from(text);\n\tif (codePoints.length <= limit) return text;\n\tconst suffixCodePoints = Array.from(suffix);\n\treturn `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join(\"\")}${suffix}`;\n}\n\nexport function truncateSlackTaskText(text: string): string {\n\treturn truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, \"…\");\n}\n\nexport function truncateSlackMarkdown(text: string): string {\n\treturn truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);\n}\n\nfunction requiredContextString(start: TransportStreamStart, key: string): string {\n\tconst value = start.context?.[key];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Missing Slack stream context ${key}`);\n\t}\n\treturn value;\n}\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\tprivate readonly taskDetailsSent = new Map<string, Set<string>>();\n\tprivate readonly planStreams = new Set<string>();\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\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\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\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tprepareStreamText(text: string): string {\n\t\treturn truncateSlackMarkdown(text);\n\t}\n\n\tasync startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tif (!target.threadId) throw new Error(\"Missing Slack stream thread timestamp\");\n\n\t\tconst headerChunk = {\n\t\t\ttype: \"blocks\",\n\t\t\tblocks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"header\",\n\t\t\t\t\ttext: { type: \"plain_text\", text: SLACK_STREAM_HEADER },\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst planChunks =\n\t\t\tstart.presentation === \"plan\"\n\t\t\t\t? [\n\t\t\t\t\t\t{ type: \"plan_update\", title: SLACK_PLAN_ACTIVE_TITLE },\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\t\tstatus: \"in_progress\",\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\tconst result = await this.webClient.chat.startStream({\n\t\t\tchannel: target.channelId,\n\t\t\tthread_ts: target.threadId,\n\t\t\trecipient_user_id: requiredContextString(start, \"recipientUserId\"),\n\t\t\trecipient_team_id: requiredContextString(start, \"recipientTeamId\"),\n\t\t\ttask_display_mode: start.presentation || \"timeline\",\n\t\t\t// Slack supports block chunks although the current SDK's chunk union omits them.\n\t\t\tchunks: [headerChunk, ...planChunks] as never,\n\t\t});\n\t\tif (!result.ts) throw new Error(\"Slack chat.startStream did not return a message timestamp\");\n\t\tif (start.presentation === \"plan\") this.planStreams.add(result.ts);\n\t\treturn result.ts;\n\t}\n\n\tasync updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\n\t\tconst sentTaskIds = this.taskDetailsSent.get(ref) ?? new Set<string>();\n\t\tconst details = action.details === undefined || sentTaskIds.has(action.id) ? undefined : action.details;\n\t\tawait this.webClient.chat.appendStream({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tchunks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\tid: action.id,\n\t\t\t\t\ttitle: truncateSlackTaskText(action.title),\n\t\t\t\t\t...(details === undefined ? {} : { details: truncateSlackTaskText(details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tif (details !== undefined) {\n\t\t\tsentTaskIds.add(action.id);\n\t\t\tthis.taskDetailsSent.set(ref, sentTaskIds);\n\t\t}\n\t}\n\n\tasync stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tconst planChunks = this.planStreams.has(ref)\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"task_update\" as const,\n\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\tstatus: result.outcome,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"plan_update\" as const,\n\t\t\t\t\t\ttitle: result.outcome === \"complete\" ? SLACK_PLAN_COMPLETE_TITLE : SLACK_PLAN_ERROR_TITLE,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst payload = {\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\t// The stream starts in chunk mode for header/task updates, so final text\n\t\t\t// must use the same mode. Top-level markdown_text is rejected by Slack\n\t\t\t// with streaming_mode_mismatch after chunk-based starts.\n\t\t\tchunks: [...planChunks, { type: \"markdown_text\" as const, text: truncateSlackMarkdown(result.text) }],\n\t\t};\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t} catch {\n\t\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.taskDetailsSent.delete(ref);\n\t\t\tthis.planStreams.delete(ref);\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"]}
@@ -1,5 +1,10 @@
1
1
  import { createReadStream } from "fs";
2
2
  export const SLACK_STREAM_HEADER = "Ich bearbeite jetzt deine Anfrage...";
3
+ export const SLACK_PLAN_ACTIVE_TITLE = "Biene Willy arbeitet 🐝";
4
+ export const SLACK_PLAN_COMPLETE_TITLE = "Biene Willy ist fertig 🍯";
5
+ export const SLACK_PLAN_ERROR_TITLE = "Biene Willy braucht Hilfe 🐝";
6
+ export const SLACK_RUN_TASK_ID = "bee-run";
7
+ export const SLACK_RUN_TASK_TITLE = "Anfrage bearbeiten";
3
8
  export const SLACK_TASK_TEXT_LIMIT = 256;
4
9
  export const SLACK_MARKDOWN_LIMIT = 12_000;
5
10
  export const SLACK_MARKDOWN_TRUNCATION_SUFFIX = "\n\n_[Antwort auf 12.000 Zeichen gekürzt]_";
@@ -33,6 +38,7 @@ export class SlackSink {
33
38
  this.observer = observer;
34
39
  }
35
40
  taskDetailsSent = new Map();
41
+ planStreams = new Set();
36
42
  async postMessage(target, text) {
37
43
  if (!target.channelId) {
38
44
  throw new Error("Missing Slack channel id");
@@ -74,6 +80,17 @@ export class SlackSink {
74
80
  },
75
81
  ],
76
82
  };
83
+ const planChunks = start.presentation === "plan"
84
+ ? [
85
+ { type: "plan_update", title: SLACK_PLAN_ACTIVE_TITLE },
86
+ {
87
+ type: "task_update",
88
+ id: SLACK_RUN_TASK_ID,
89
+ title: SLACK_RUN_TASK_TITLE,
90
+ status: "in_progress",
91
+ },
92
+ ]
93
+ : [];
77
94
  const result = await this.webClient.chat.startStream({
78
95
  channel: target.channelId,
79
96
  thread_ts: target.threadId,
@@ -81,10 +98,12 @@ export class SlackSink {
81
98
  recipient_team_id: requiredContextString(start, "recipientTeamId"),
82
99
  task_display_mode: start.presentation || "timeline",
83
100
  // Slack supports block chunks although the current SDK's chunk union omits them.
84
- chunks: [headerChunk],
101
+ chunks: [headerChunk, ...planChunks],
85
102
  });
86
103
  if (!result.ts)
87
104
  throw new Error("Slack chat.startStream did not return a message timestamp");
105
+ if (start.presentation === "plan")
106
+ this.planStreams.add(result.ts);
88
107
  return result.ts;
89
108
  }
90
109
  async updateStream(target, ref, action) {
@@ -113,13 +132,27 @@ export class SlackSink {
113
132
  async stopStream(target, ref, result) {
114
133
  if (!target.channelId)
115
134
  throw new Error("Missing Slack channel id");
135
+ const planChunks = this.planStreams.has(ref)
136
+ ? [
137
+ {
138
+ type: "task_update",
139
+ id: SLACK_RUN_TASK_ID,
140
+ title: SLACK_RUN_TASK_TITLE,
141
+ status: result.outcome,
142
+ },
143
+ {
144
+ type: "plan_update",
145
+ title: result.outcome === "complete" ? SLACK_PLAN_COMPLETE_TITLE : SLACK_PLAN_ERROR_TITLE,
146
+ },
147
+ ]
148
+ : [];
116
149
  const payload = {
117
150
  channel: target.channelId,
118
151
  ts: ref,
119
152
  // The stream starts in chunk mode for header/task updates, so final text
120
153
  // must use the same mode. Top-level markdown_text is rejected by Slack
121
154
  // with streaming_mode_mismatch after chunk-based starts.
122
- chunks: [{ type: "markdown_text", text: truncateSlackMarkdown(result.text) }],
155
+ chunks: [...planChunks, { type: "markdown_text", text: truncateSlackMarkdown(result.text) }],
123
156
  };
124
157
  try {
125
158
  try {
@@ -132,6 +165,7 @@ export class SlackSink {
132
165
  }
133
166
  finally {
134
167
  this.taskDetailsSent.delete(ref);
168
+ this.planStreams.delete(ref);
135
169
  }
136
170
  }
137
171
  async publishArtifact(target, artifact) {
@@ -1 +1 @@
1
- {"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tActionUpdate,\n\tArtifactRef,\n\tBlobStore,\n\tTransportOutputTarget,\n\tTransportSink,\n\tTransportStreamResult,\n\tTransportStreamStart,\n} from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport const SLACK_STREAM_HEADER = \"Ich bearbeite jetzt deine Anfrage...\";\nexport const SLACK_TASK_TEXT_LIMIT = 256;\nexport const SLACK_MARKDOWN_LIMIT = 12_000;\nexport const SLACK_MARKDOWN_TRUNCATION_SUFFIX = \"\\n\\n_[Antwort auf 12.000 Zeichen gekürzt]_\";\n\nfunction truncateCodePoints(text: string, limit: number, suffix: string): string {\n\tconst codePoints = Array.from(text);\n\tif (codePoints.length <= limit) return text;\n\tconst suffixCodePoints = Array.from(suffix);\n\treturn `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join(\"\")}${suffix}`;\n}\n\nexport function truncateSlackTaskText(text: string): string {\n\treturn truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, \"…\");\n}\n\nexport function truncateSlackMarkdown(text: string): string {\n\treturn truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);\n}\n\nfunction requiredContextString(start: TransportStreamStart, key: string): string {\n\tconst value = start.context?.[key];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Missing Slack stream context ${key}`);\n\t}\n\treturn value;\n}\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\tprivate readonly taskDetailsSent = new Map<string, Set<string>>();\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\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\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\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tprepareStreamText(text: string): string {\n\t\treturn truncateSlackMarkdown(text);\n\t}\n\n\tasync startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tif (!target.threadId) throw new Error(\"Missing Slack stream thread timestamp\");\n\n\t\tconst headerChunk = {\n\t\t\ttype: \"blocks\",\n\t\t\tblocks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"header\",\n\t\t\t\t\ttext: { type: \"plain_text\", text: SLACK_STREAM_HEADER },\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst result = await this.webClient.chat.startStream({\n\t\t\tchannel: target.channelId,\n\t\t\tthread_ts: target.threadId,\n\t\t\trecipient_user_id: requiredContextString(start, \"recipientUserId\"),\n\t\t\trecipient_team_id: requiredContextString(start, \"recipientTeamId\"),\n\t\t\ttask_display_mode: start.presentation || \"timeline\",\n\t\t\t// Slack supports block chunks although the current SDK's chunk union omits them.\n\t\t\tchunks: [headerChunk] as never,\n\t\t});\n\t\tif (!result.ts) throw new Error(\"Slack chat.startStream did not return a message timestamp\");\n\t\treturn result.ts;\n\t}\n\n\tasync updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\n\t\tconst sentTaskIds = this.taskDetailsSent.get(ref) ?? new Set<string>();\n\t\tconst details = action.details === undefined || sentTaskIds.has(action.id) ? undefined : action.details;\n\t\tawait this.webClient.chat.appendStream({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tchunks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\tid: action.id,\n\t\t\t\t\ttitle: truncateSlackTaskText(action.title),\n\t\t\t\t\t...(details === undefined ? {} : { details: truncateSlackTaskText(details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tif (details !== undefined) {\n\t\t\tsentTaskIds.add(action.id);\n\t\t\tthis.taskDetailsSent.set(ref, sentTaskIds);\n\t\t}\n\t}\n\n\tasync stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tconst payload = {\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\t// The stream starts in chunk mode for header/task updates, so final text\n\t\t\t// must use the same mode. Top-level markdown_text is rejected by Slack\n\t\t\t// with streaming_mode_mismatch after chunk-based starts.\n\t\t\tchunks: [{ type: \"markdown_text\" as const, text: truncateSlackMarkdown(result.text) }],\n\t\t};\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t} catch {\n\t\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.taskDetailsSent.delete(ref);\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"]}
1
+ {"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tActionUpdate,\n\tArtifactRef,\n\tBlobStore,\n\tTransportOutputTarget,\n\tTransportSink,\n\tTransportStreamResult,\n\tTransportStreamStart,\n} from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport const SLACK_STREAM_HEADER = \"Ich bearbeite jetzt deine Anfrage...\";\nexport const SLACK_PLAN_ACTIVE_TITLE = \"Biene Willy arbeitet 🐝\";\nexport const SLACK_PLAN_COMPLETE_TITLE = \"Biene Willy ist fertig 🍯\";\nexport const SLACK_PLAN_ERROR_TITLE = \"Biene Willy braucht Hilfe 🐝\";\nexport const SLACK_RUN_TASK_ID = \"bee-run\";\nexport const SLACK_RUN_TASK_TITLE = \"Anfrage bearbeiten\";\nexport const SLACK_TASK_TEXT_LIMIT = 256;\nexport const SLACK_MARKDOWN_LIMIT = 12_000;\nexport const SLACK_MARKDOWN_TRUNCATION_SUFFIX = \"\\n\\n_[Antwort auf 12.000 Zeichen gekürzt]_\";\n\nfunction truncateCodePoints(text: string, limit: number, suffix: string): string {\n\tconst codePoints = Array.from(text);\n\tif (codePoints.length <= limit) return text;\n\tconst suffixCodePoints = Array.from(suffix);\n\treturn `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join(\"\")}${suffix}`;\n}\n\nexport function truncateSlackTaskText(text: string): string {\n\treturn truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, \"…\");\n}\n\nexport function truncateSlackMarkdown(text: string): string {\n\treturn truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);\n}\n\nfunction requiredContextString(start: TransportStreamStart, key: string): string {\n\tconst value = start.context?.[key];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Missing Slack stream context ${key}`);\n\t}\n\treturn value;\n}\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\tprivate readonly taskDetailsSent = new Map<string, Set<string>>();\n\tprivate readonly planStreams = new Set<string>();\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\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\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\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tprepareStreamText(text: string): string {\n\t\treturn truncateSlackMarkdown(text);\n\t}\n\n\tasync startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tif (!target.threadId) throw new Error(\"Missing Slack stream thread timestamp\");\n\n\t\tconst headerChunk = {\n\t\t\ttype: \"blocks\",\n\t\t\tblocks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"header\",\n\t\t\t\t\ttext: { type: \"plain_text\", text: SLACK_STREAM_HEADER },\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst planChunks =\n\t\t\tstart.presentation === \"plan\"\n\t\t\t\t? [\n\t\t\t\t\t\t{ type: \"plan_update\", title: SLACK_PLAN_ACTIVE_TITLE },\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\t\tstatus: \"in_progress\",\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\tconst result = await this.webClient.chat.startStream({\n\t\t\tchannel: target.channelId,\n\t\t\tthread_ts: target.threadId,\n\t\t\trecipient_user_id: requiredContextString(start, \"recipientUserId\"),\n\t\t\trecipient_team_id: requiredContextString(start, \"recipientTeamId\"),\n\t\t\ttask_display_mode: start.presentation || \"timeline\",\n\t\t\t// Slack supports block chunks although the current SDK's chunk union omits them.\n\t\t\tchunks: [headerChunk, ...planChunks] as never,\n\t\t});\n\t\tif (!result.ts) throw new Error(\"Slack chat.startStream did not return a message timestamp\");\n\t\tif (start.presentation === \"plan\") this.planStreams.add(result.ts);\n\t\treturn result.ts;\n\t}\n\n\tasync updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\n\t\tconst sentTaskIds = this.taskDetailsSent.get(ref) ?? new Set<string>();\n\t\tconst details = action.details === undefined || sentTaskIds.has(action.id) ? undefined : action.details;\n\t\tawait this.webClient.chat.appendStream({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tchunks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\tid: action.id,\n\t\t\t\t\ttitle: truncateSlackTaskText(action.title),\n\t\t\t\t\t...(details === undefined ? {} : { details: truncateSlackTaskText(details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tif (details !== undefined) {\n\t\t\tsentTaskIds.add(action.id);\n\t\t\tthis.taskDetailsSent.set(ref, sentTaskIds);\n\t\t}\n\t}\n\n\tasync stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tconst planChunks = this.planStreams.has(ref)\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"task_update\" as const,\n\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\tstatus: result.outcome,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"plan_update\" as const,\n\t\t\t\t\t\ttitle: result.outcome === \"complete\" ? SLACK_PLAN_COMPLETE_TITLE : SLACK_PLAN_ERROR_TITLE,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst payload = {\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\t// The stream starts in chunk mode for header/task updates, so final text\n\t\t\t// must use the same mode. Top-level markdown_text is rejected by Slack\n\t\t\t// with streaming_mode_mismatch after chunk-based starts.\n\t\t\tchunks: [...planChunks, { type: \"markdown_text\" as const, text: truncateSlackMarkdown(result.text) }],\n\t\t};\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t} catch {\n\t\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.taskDetailsSent.delete(ref);\n\t\t\tthis.planStreams.delete(ref);\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.13",
3
+ "version": "0.1.14",
4
4
  "description": "Slack adapter for Bee Gate and the Bee Dance protocol",
5
5
  "type": "module",
6
6
  "bin": {