@jobmatchme/bee-slack 0.1.11 → 0.1.12
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 +34 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +8 -2
- package/dist/gateway.js.map +1 -1
- package/dist/router.d.ts +2 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +13 -0
- package/dist/router.js.map +1 -1
- package/dist/slack-sink.d.ts +11 -1
- package/dist/slack-sink.d.ts.map +1 -1
- package/dist/slack-sink.js +87 -0
- package/dist/slack-sink.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,6 +49,40 @@ POST /api/handoffs
|
|
|
49
49
|
GET /api/handoffs/:routeId/:threadTs/replies
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Route-opt-in native streaming
|
|
53
|
+
|
|
54
|
+
A normal channel mention route can opt into Slack native streaming. The stream
|
|
55
|
+
starts in the mention's thread with the header
|
|
56
|
+
`Ich bearbeite jetzt deine Anfrage...`, renders transport-neutral Bee action
|
|
57
|
+
updates as Slack task cards, and stops with the complete assistant markdown.
|
|
58
|
+
Routes without `streaming.enabled: true` retain the existing
|
|
59
|
+
`chat.postMessage`/`chat.update` delivery.
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"id": "mention-pilot",
|
|
64
|
+
"match": { "channelIds": ["C0123456789"] },
|
|
65
|
+
"streaming": {
|
|
66
|
+
"enabled": true,
|
|
67
|
+
"taskDisplayMode": "timeline"
|
|
68
|
+
},
|
|
69
|
+
"worker": { "subject": "fabee.agent.pi.default" },
|
|
70
|
+
"session": { "strategy": "thread", "prefix": "bee-pilot" }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`taskDisplayMode` accepts `timeline`, `plan`, or `dense` and defaults to
|
|
75
|
+
`timeline`. Streaming is deliberately limited to interactive app mentions;
|
|
76
|
+
DMs, scheduled runs, and authenticated handoffs remain on legacy delivery even
|
|
77
|
+
if their selected route contains this option. Final markdown is passed through
|
|
78
|
+
unchanged up to Slack's 12,000-character limit and visibly truncated above it.
|
|
79
|
+
Task titles and details are capped conservatively for Slack chunk limits.
|
|
80
|
+
|
|
81
|
+
Open stream state is process-local and is not recovered after a pod restart.
|
|
82
|
+
Slack API failures are surfaced to Bee Gate, which owns the per-run fallback
|
|
83
|
+
from streaming to legacy/degraded delivery. Generated artifacts remain separate
|
|
84
|
+
`files.uploadV2` uploads after the final response.
|
|
85
|
+
|
|
52
86
|
## Local development
|
|
53
87
|
|
|
54
88
|
For local manual testing, copy `local.config.example.json` to
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,kBAAkB,CA+
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,kBAAkB,CA+ClE","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tconst handoffConfig = process.env.BEE_SLACK_HANDOFF_CONFIG;\n\tif (handoffConfig) {\n\t\tconfig.handoff = JSON.parse(handoffConfig) as SlackGatewayConfig[\"handoff\"];\n\t}\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\tconst taskDisplayModes = new Set([\"timeline\", \"plan\", \"dense\"]);\n\tfor (const route of config.routes) {\n\t\tif (route.streaming === undefined) continue;\n\t\tif (!route.streaming || typeof route.streaming !== \"object\" || Array.isArray(route.streaming)) {\n\t\t\tthrow new Error(`Route ${route.id || \"<unknown>\"} streaming must be an object in ${fullPath}`);\n\t\t}\n\t\tif (typeof route.streaming.enabled !== \"boolean\") {\n\t\t\tthrow new Error(`Route ${route.id || \"<unknown>\"} streaming.enabled must be a boolean in ${fullPath}`);\n\t\t}\n\t\tif (route.streaming.taskDisplayMode !== undefined && !taskDisplayModes.has(route.streaming.taskDisplayMode)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Route ${route.id || \"<unknown>\"} streaming.taskDisplayMode must be timeline, plan or dense in ${fullPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (config.handoff?.enabled) {\n\t\tif (!Array.isArray(config.handoff.routes) || config.handoff.routes.length === 0) {\n\t\t\tthrow new Error(`handoff.routes must be a non-empty array in ${fullPath}`);\n\t\t}\n\t\tfor (const route of config.handoff.routes) {\n\t\t\tif (!route.id || !route.channelId || !route.worker?.subject) {\n\t\t\t\tthrow new Error(`Every handoff route needs id, channelId and worker.subject in ${fullPath}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn config;\n}\n"]}
|
package/dist/config.js
CHANGED
|
@@ -21,6 +21,20 @@ export function loadConfig(configPath) {
|
|
|
21
21
|
if (!Array.isArray(config.routes) || config.routes.length === 0) {
|
|
22
22
|
throw new Error(`Missing routes in ${fullPath}`);
|
|
23
23
|
}
|
|
24
|
+
const taskDisplayModes = new Set(["timeline", "plan", "dense"]);
|
|
25
|
+
for (const route of config.routes) {
|
|
26
|
+
if (route.streaming === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
if (!route.streaming || typeof route.streaming !== "object" || Array.isArray(route.streaming)) {
|
|
29
|
+
throw new Error(`Route ${route.id || "<unknown>"} streaming must be an object in ${fullPath}`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof route.streaming.enabled !== "boolean") {
|
|
32
|
+
throw new Error(`Route ${route.id || "<unknown>"} streaming.enabled must be a boolean in ${fullPath}`);
|
|
33
|
+
}
|
|
34
|
+
if (route.streaming.taskDisplayMode !== undefined && !taskDisplayModes.has(route.streaming.taskDisplayMode)) {
|
|
35
|
+
throw new Error(`Route ${route.id || "<unknown>"} streaming.taskDisplayMode must be timeline, plan or dense in ${fullPath}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
24
38
|
if (config.handoff?.enabled) {
|
|
25
39
|
if (!Array.isArray(config.handoff.routes) || config.handoff.routes.length === 0) {
|
|
26
40
|
throw new Error(`handoff.routes must be a non-empty array in ${fullPath}`);
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,MAAM,UAAU,UAAU,CAAC,UAAmB,EAAsB;IACnE,MAAM,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAuB,CAAC;IACjF,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC;IAC3D,IAAI,aAAa,EAAE,CAAC;QACnB,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAkC,CAAC;IAC7E,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACvG,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,KAAK,CAAC,+CAA+C,QAAQ,EAAE,CAAC,CAAC;QAC5E,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;gBAC7D,MAAM,IAAI,KAAK,CAAC,iEAAiE,QAAQ,EAAE,CAAC,CAAC;YAC9F,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tconst handoffConfig = process.env.BEE_SLACK_HANDOFF_CONFIG;\n\tif (handoffConfig) {\n\t\tconfig.handoff = JSON.parse(handoffConfig) as SlackGatewayConfig[\"handoff\"];\n\t}\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\tif (config.handoff?.enabled) {\n\t\tif (!Array.isArray(config.handoff.routes) || config.handoff.routes.length === 0) {\n\t\t\tthrow new Error(`handoff.routes must be a non-empty array in ${fullPath}`);\n\t\t}\n\t\tfor (const route of config.handoff.routes) {\n\t\t\tif (!route.id || !route.channelId || !route.worker?.subject) {\n\t\t\t\tthrow new Error(`Every handoff route needs id, channelId and worker.subject in ${fullPath}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn config;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,MAAM,UAAU,UAAU,CAAC,UAAmB,EAAsB;IACnE,MAAM,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAuB,CAAC;IACjF,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC;IAC3D,IAAI,aAAa,EAAE,CAAC;QACnB,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAkC,CAAC;IAC7E,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACvG,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChE,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;YAAE,SAAS;QAC5C,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/F,MAAM,IAAI,KAAK,CAAC,SAAS,KAAK,CAAC,EAAE,IAAI,WAAW,mCAAmC,QAAQ,EAAE,CAAC,CAAC;QAChG,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,SAAS,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,SAAS,KAAK,CAAC,EAAE,IAAI,WAAW,2CAA2C,QAAQ,EAAE,CAAC,CAAC;QACxG,CAAC;QACD,IAAI,KAAK,CAAC,SAAS,CAAC,eAAe,KAAK,SAAS,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,CAAC;YAC7G,MAAM,IAAI,KAAK,CACd,SAAS,KAAK,CAAC,EAAE,IAAI,WAAW,iEAAiE,QAAQ,EAAE,CAC3G,CAAC;QACH,CAAC;IACF,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,KAAK,CAAC,+CAA+C,QAAQ,EAAE,CAAC,CAAC;QAC5E,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;gBAC7D,MAAM,IAAI,KAAK,CAAC,iEAAiE,QAAQ,EAAE,CAAC,CAAC;YAC9F,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import { readFileSync } from \"fs\";\nimport { resolve } from \"path\";\nimport type { SlackGatewayConfig } from \"./types.js\";\n\nexport function loadConfig(configPath?: string): SlackGatewayConfig {\n\tconst path = configPath || process.env.BEE_SLACK_CONFIG;\n\tif (!path) {\n\t\tthrow new Error(\"Missing BEE_SLACK_CONFIG\");\n\t}\n\n\tconst fullPath = resolve(path);\n\tconst config = JSON.parse(readFileSync(fullPath, \"utf-8\")) as SlackGatewayConfig;\n\tconst handoffConfig = process.env.BEE_SLACK_HANDOFF_CONFIG;\n\tif (handoffConfig) {\n\t\tconfig.handoff = JSON.parse(handoffConfig) as SlackGatewayConfig[\"handoff\"];\n\t}\n\tif (!config.appToken) throw new Error(`Missing appToken in ${fullPath}`);\n\tif (!config.botToken) throw new Error(`Missing botToken in ${fullPath}`);\n\tif (!config.nats?.servers || (Array.isArray(config.nats.servers) && config.nats.servers.length === 0)) {\n\t\tthrow new Error(`Missing nats.servers in ${fullPath}`);\n\t}\n\tif (!Array.isArray(config.routes) || config.routes.length === 0) {\n\t\tthrow new Error(`Missing routes in ${fullPath}`);\n\t}\n\tconst taskDisplayModes = new Set([\"timeline\", \"plan\", \"dense\"]);\n\tfor (const route of config.routes) {\n\t\tif (route.streaming === undefined) continue;\n\t\tif (!route.streaming || typeof route.streaming !== \"object\" || Array.isArray(route.streaming)) {\n\t\t\tthrow new Error(`Route ${route.id || \"<unknown>\"} streaming must be an object in ${fullPath}`);\n\t\t}\n\t\tif (typeof route.streaming.enabled !== \"boolean\") {\n\t\t\tthrow new Error(`Route ${route.id || \"<unknown>\"} streaming.enabled must be a boolean in ${fullPath}`);\n\t\t}\n\t\tif (route.streaming.taskDisplayMode !== undefined && !taskDisplayModes.has(route.streaming.taskDisplayMode)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Route ${route.id || \"<unknown>\"} streaming.taskDisplayMode must be timeline, plan or dense in ${fullPath}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (config.handoff?.enabled) {\n\t\tif (!Array.isArray(config.handoff.routes) || config.handoff.routes.length === 0) {\n\t\t\tthrow new Error(`handoff.routes must be a non-empty array in ${fullPath}`);\n\t\t}\n\t\tfor (const route of config.handoff.routes) {\n\t\t\tif (!route.id || !route.channelId || !route.worker?.subject) {\n\t\t\t\tthrow new Error(`Every handoff route needs id, channelId and worker.subject in ${fullPath}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn config;\n}\n"]}
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAiC,kBAAkB,EAAuB,MAAM,YAAY,CAAC;AAazG,qBAAa,YAAY;IAeZ,OAAO,CAAC,MAAM;IAd1B,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;IACpD,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,6BAA6B,CAAS;IAE9C,YAAoB,MAAM,EAAE,kBAAkB,EAa7C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB3B;IAED,OAAO,CAAC,kBAAkB;IAqD1B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,kBAAkB;YAkEZ,cAAc;IA0C5B,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,iBAAiB;YAOX,mBAAmB;YA+BnB,UAAU;YAsBV,aAAa;CA0B3B;AAaD,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 { createHandoffServer, SlackHandoffController } from \"./handoff.js\";\nimport { HandoffReplyCache } from \"./handoff-reply-cache.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\tprivate handoffReplyCache = new HandoffReplyCache();\n\tprivate slackThreadHistoryUnavailable = false;\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\t\tposted: (target, ref, text) => this.handoffReplyCache.recordPosted(target, ref, text, this.botDisplayName()),\n\t\t\tupdated: (target, ref, text) => this.handoffReplyCache.recordUpdated(target, ref, text, this.botDisplayName()),\n\t\t});\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.startHandoffServer();\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 startHandoffServer(): void {\n\t\tconst handoffConfig = this.config.handoff;\n\t\tif (!handoffConfig?.enabled) return;\n\t\tif (!this.teamId || !this.engine) throw new Error(\"Gateway must be initialized before handoff server startup\");\n\t\tconst controller = new SlackHandoffController(handoffConfig, {\n\t\t\tteamId: this.teamId,\n\t\t\tpostRootMessage: async (channelId, text) => this.sink.postMessage({ channelId }, text),\n\t\t\tgetPermalink: async (channelId, messageTs) => {\n\t\t\t\tconst result = await this.webClient.chat.getPermalink({ channel: channelId, message_ts: messageTs });\n\t\t\t\tif (!result.permalink) throw new Error(\"Slack did not return a permalink\");\n\t\t\t\treturn result.permalink;\n\t\t\t},\n\t\t\tdispatch: (input) => this.engine!.dispatch(input),\n\t\t\tgetReplies: async (channelId, threadTs) => {\n\t\t\t\tif (this.slackThreadHistoryUnavailable) return this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.webClient.conversations.replies({\n\t\t\t\t\t\tchannel: channelId,\n\t\t\t\t\t\tts: threadTs,\n\t\t\t\t\t\tlimit: 100,\n\t\t\t\t\t});\n\t\t\t\t\treturn (result.messages || []).map((message) => {\n\t\t\t\t\t\tconst typed = message as typeof message & {\n\t\t\t\t\t\t\tbot_id?: string;\n\t\t\t\t\t\t\tbot_profile?: { name?: string };\n\t\t\t\t\t\t\tusername?: string;\n\t\t\t\t\t\t\tthread_ts?: string;\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tts: String(typed.ts || \"\"),\n\t\t\t\t\t\t\tthreadTs: typed.thread_ts,\n\t\t\t\t\t\t\ttext: typed.text || \"\",\n\t\t\t\t\t\t\tauthor: typed.user\n\t\t\t\t\t\t\t\t? this.users.get(typed.user)?.displayName || this.users.get(typed.user)?.userName || typed.user\n\t\t\t\t\t\t\t\t: typed.bot_profile?.name || typed.username || \"Bot\",\n\t\t\t\t\t\t\tisBot: Boolean(typed.bot_id),\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst errorCode = slackErrorCode(error);\n\t\t\t\t\tif (!isSlackHistoryPermissionError(errorCode)) throw error;\n\t\t\t\t\tthis.slackThreadHistoryUnavailable = true;\n\t\t\t\t\tlog.logWarning(`Slack thread history unavailable (${errorCode}); using outbound Bee reply cache`);\n\t\t\t\t\treturn this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst server = createHandoffServer(controller);\n\t\tconst host = handoffConfig.host || \"0.0.0.0\";\n\t\tconst port = handoffConfig.port || 8080;\n\t\tserver.listen(port, host, () => log.logInfo(`bee-slack handoff listening on http://${host}:${port}`));\n\t}\n\n\tprivate botDisplayName(): string {\n\t\tif (!this.botUserId) return \"Bee\";\n\t\tconst bot = this.users.get(this.botUserId);\n\t\treturn bot?.displayName || bot?.userName || \"Bee\";\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\nfunction slackErrorCode(error: unknown): string | undefined {\n\tif (!error || typeof error !== \"object\" || !(\"data\" in error)) return undefined;\n\tconst data = error.data;\n\tif (!data || typeof data !== \"object\" || !(\"error\" in data)) return undefined;\n\treturn typeof data.error === \"string\" ? data.error : undefined;\n}\n\nfunction isSlackHistoryPermissionError(errorCode: string | undefined): boolean {\n\treturn errorCode === \"missing_scope\" || errorCode === \"no_permission\" || errorCode === \"not_allowed_token_type\";\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":"AAmBA,OAAO,KAAK,EAAiC,kBAAkB,EAAuB,MAAM,YAAY,CAAC;AAazG,qBAAa,YAAY;IAeZ,OAAO,CAAC,MAAM;IAd1B,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;IACpD,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,6BAA6B,CAAS;IAE9C,YAAoB,MAAM,EAAE,kBAAkB,EAa7C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB3B;IAED,OAAO,CAAC,kBAAkB;IAqD1B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,kBAAkB;YAmEZ,cAAc;IA0C5B,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,iBAAiB;YAOX,mBAAmB;YA+BnB,UAAU;YAsBV,aAAa;CA0B3B;AAaD,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 { createHandoffServer, SlackHandoffController } from \"./handoff.js\";\nimport { HandoffReplyCache } from \"./handoff-reply-cache.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute, resolveStreamingPreference } 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\tprivate handoffReplyCache = new HandoffReplyCache();\n\tprivate slackThreadHistoryUnavailable = false;\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\t\tposted: (target, ref, text) => this.handoffReplyCache.recordPosted(target, ref, text, this.botDisplayName()),\n\t\t\tupdated: (target, ref, text) => this.handoffReplyCache.recordUpdated(target, ref, text, this.botDisplayName()),\n\t\t});\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.startHandoffServer();\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 startHandoffServer(): void {\n\t\tconst handoffConfig = this.config.handoff;\n\t\tif (!handoffConfig?.enabled) return;\n\t\tif (!this.teamId || !this.engine) throw new Error(\"Gateway must be initialized before handoff server startup\");\n\t\tconst controller = new SlackHandoffController(handoffConfig, {\n\t\t\tteamId: this.teamId,\n\t\t\tpostRootMessage: async (channelId, text) => this.sink.postMessage({ channelId }, text),\n\t\t\tgetPermalink: async (channelId, messageTs) => {\n\t\t\t\tconst result = await this.webClient.chat.getPermalink({ channel: channelId, message_ts: messageTs });\n\t\t\t\tif (!result.permalink) throw new Error(\"Slack did not return a permalink\");\n\t\t\t\treturn result.permalink;\n\t\t\t},\n\t\t\tdispatch: (input) => this.engine!.dispatch(input),\n\t\t\tgetReplies: async (channelId, threadTs) => {\n\t\t\t\tif (this.slackThreadHistoryUnavailable) return this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.webClient.conversations.replies({\n\t\t\t\t\t\tchannel: channelId,\n\t\t\t\t\t\tts: threadTs,\n\t\t\t\t\t\tlimit: 100,\n\t\t\t\t\t});\n\t\t\t\t\treturn (result.messages || []).map((message) => {\n\t\t\t\t\t\tconst typed = message as typeof message & {\n\t\t\t\t\t\t\tbot_id?: string;\n\t\t\t\t\t\t\tbot_profile?: { name?: string };\n\t\t\t\t\t\t\tusername?: string;\n\t\t\t\t\t\t\tthread_ts?: string;\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tts: String(typed.ts || \"\"),\n\t\t\t\t\t\t\tthreadTs: typed.thread_ts,\n\t\t\t\t\t\t\ttext: typed.text || \"\",\n\t\t\t\t\t\t\tauthor: typed.user\n\t\t\t\t\t\t\t\t? this.users.get(typed.user)?.displayName || this.users.get(typed.user)?.userName || typed.user\n\t\t\t\t\t\t\t\t: typed.bot_profile?.name || typed.username || \"Bot\",\n\t\t\t\t\t\t\tisBot: Boolean(typed.bot_id),\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst errorCode = slackErrorCode(error);\n\t\t\t\t\tif (!isSlackHistoryPermissionError(errorCode)) throw error;\n\t\t\t\t\tthis.slackThreadHistoryUnavailable = true;\n\t\t\t\t\tlog.logWarning(`Slack thread history unavailable (${errorCode}); using outbound Bee reply cache`);\n\t\t\t\t\treturn this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst server = createHandoffServer(controller);\n\t\tconst host = handoffConfig.host || \"0.0.0.0\";\n\t\tconst port = handoffConfig.port || 8080;\n\t\tserver.listen(port, host, () => log.logInfo(`bee-slack handoff listening on http://${host}:${port}`));\n\t}\n\n\tprivate botDisplayName(): string {\n\t\tif (!this.botUserId) return \"Bee\";\n\t\tconst bot = this.users.get(this.botUserId);\n\t\treturn bot?.displayName || bot?.userName || \"Bee\";\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack, body }) => {\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\tteamId: (event as typeof e & { team?: string }).team || (body as { team_id?: string }).team_id,\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\tstreaming: resolveStreamingPreference(resolved.route, 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\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\nfunction slackErrorCode(error: unknown): string | undefined {\n\tif (!error || typeof error !== \"object\" || !(\"data\" in error)) return undefined;\n\tconst data = error.data;\n\tif (!data || typeof data !== \"object\" || !(\"error\" in data)) return undefined;\n\treturn typeof data.error === \"string\" ? data.error : undefined;\n}\n\nfunction isSlackHistoryPermissionError(errorCode: string | undefined): boolean {\n\treturn errorCode === \"missing_scope\" || errorCode === \"no_permission\" || errorCode === \"not_allowed_token_type\";\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
|
@@ -6,7 +6,7 @@ import { loadConfig } from "./config.js";
|
|
|
6
6
|
import { createHandoffServer, SlackHandoffController } from "./handoff.js";
|
|
7
7
|
import { HandoffReplyCache } from "./handoff-reply-cache.js";
|
|
8
8
|
import * as log from "./log.js";
|
|
9
|
-
import { resolveRoute } from "./router.js";
|
|
9
|
+
import { resolveRoute, resolveStreamingPreference } from "./router.js";
|
|
10
10
|
import { SlackSink } from "./slack-sink.js";
|
|
11
11
|
export class SlackGateway {
|
|
12
12
|
config;
|
|
@@ -117,7 +117,7 @@ export class SlackGateway {
|
|
|
117
117
|
return bot?.displayName || bot?.userName || "Bee";
|
|
118
118
|
}
|
|
119
119
|
setupEventHandlers() {
|
|
120
|
-
this.socketClient.on("app_mention", async ({ event, ack }) => {
|
|
120
|
+
this.socketClient.on("app_mention", async ({ event, ack, body }) => {
|
|
121
121
|
await ack();
|
|
122
122
|
const e = event;
|
|
123
123
|
if (e.channel.startsWith("D"))
|
|
@@ -129,6 +129,7 @@ export class SlackGateway {
|
|
|
129
129
|
threadTs: e.thread_ts,
|
|
130
130
|
ts: e.ts,
|
|
131
131
|
userId: e.user,
|
|
132
|
+
teamId: event.team || body.team_id,
|
|
132
133
|
userName: this.users.get(e.user)?.userName,
|
|
133
134
|
displayName: this.users.get(e.user)?.displayName,
|
|
134
135
|
text: e.text.replace(/<@[A-Z0-9]+>/gi, "").trim(),
|
|
@@ -221,6 +222,11 @@ export class SlackGateway {
|
|
|
221
222
|
text: message.text,
|
|
222
223
|
},
|
|
223
224
|
attachments,
|
|
225
|
+
streaming: resolveStreamingPreference(resolved.route, message, {
|
|
226
|
+
botUserId: this.botUserId,
|
|
227
|
+
teamId: this.teamId,
|
|
228
|
+
teamName: this.teamName || undefined,
|
|
229
|
+
}),
|
|
224
230
|
output,
|
|
225
231
|
};
|
|
226
232
|
}
|
package/dist/gateway.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype 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 { createHandoffServer, SlackHandoffController } from \"./handoff.js\";\nimport { HandoffReplyCache } from \"./handoff-reply-cache.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\tprivate handoffReplyCache = new HandoffReplyCache();\n\tprivate slackThreadHistoryUnavailable = false;\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\t\tposted: (target, ref, text) => this.handoffReplyCache.recordPosted(target, ref, text, this.botDisplayName()),\n\t\t\tupdated: (target, ref, text) => this.handoffReplyCache.recordUpdated(target, ref, text, this.botDisplayName()),\n\t\t});\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.startHandoffServer();\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 startHandoffServer(): void {\n\t\tconst handoffConfig = this.config.handoff;\n\t\tif (!handoffConfig?.enabled) return;\n\t\tif (!this.teamId || !this.engine) throw new Error(\"Gateway must be initialized before handoff server startup\");\n\t\tconst controller = new SlackHandoffController(handoffConfig, {\n\t\t\tteamId: this.teamId,\n\t\t\tpostRootMessage: async (channelId, text) => this.sink.postMessage({ channelId }, text),\n\t\t\tgetPermalink: async (channelId, messageTs) => {\n\t\t\t\tconst result = await this.webClient.chat.getPermalink({ channel: channelId, message_ts: messageTs });\n\t\t\t\tif (!result.permalink) throw new Error(\"Slack did not return a permalink\");\n\t\t\t\treturn result.permalink;\n\t\t\t},\n\t\t\tdispatch: (input) => this.engine!.dispatch(input),\n\t\t\tgetReplies: async (channelId, threadTs) => {\n\t\t\t\tif (this.slackThreadHistoryUnavailable) return this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.webClient.conversations.replies({\n\t\t\t\t\t\tchannel: channelId,\n\t\t\t\t\t\tts: threadTs,\n\t\t\t\t\t\tlimit: 100,\n\t\t\t\t\t});\n\t\t\t\t\treturn (result.messages || []).map((message) => {\n\t\t\t\t\t\tconst typed = message as typeof message & {\n\t\t\t\t\t\t\tbot_id?: string;\n\t\t\t\t\t\t\tbot_profile?: { name?: string };\n\t\t\t\t\t\t\tusername?: string;\n\t\t\t\t\t\t\tthread_ts?: string;\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tts: String(typed.ts || \"\"),\n\t\t\t\t\t\t\tthreadTs: typed.thread_ts,\n\t\t\t\t\t\t\ttext: typed.text || \"\",\n\t\t\t\t\t\t\tauthor: typed.user\n\t\t\t\t\t\t\t\t? this.users.get(typed.user)?.displayName || this.users.get(typed.user)?.userName || typed.user\n\t\t\t\t\t\t\t\t: typed.bot_profile?.name || typed.username || \"Bot\",\n\t\t\t\t\t\t\tisBot: Boolean(typed.bot_id),\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst errorCode = slackErrorCode(error);\n\t\t\t\t\tif (!isSlackHistoryPermissionError(errorCode)) throw error;\n\t\t\t\t\tthis.slackThreadHistoryUnavailable = true;\n\t\t\t\t\tlog.logWarning(`Slack thread history unavailable (${errorCode}); using outbound Bee reply cache`);\n\t\t\t\t\treturn this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst server = createHandoffServer(controller);\n\t\tconst host = handoffConfig.host || \"0.0.0.0\";\n\t\tconst port = handoffConfig.port || 8080;\n\t\tserver.listen(port, host, () => log.logInfo(`bee-slack handoff listening on http://${host}:${port}`));\n\t}\n\n\tprivate botDisplayName(): string {\n\t\tif (!this.botUserId) return \"Bee\";\n\t\tconst bot = this.users.get(this.botUserId);\n\t\treturn bot?.displayName || bot?.userName || \"Bee\";\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\nfunction slackErrorCode(error: unknown): string | undefined {\n\tif (!error || typeof error !== \"object\" || !(\"data\" in error)) return undefined;\n\tconst data = error.data;\n\tif (!data || typeof data !== \"object\" || !(\"error\" in data)) return undefined;\n\treturn typeof data.error === \"string\" ? data.error : undefined;\n}\n\nfunction isSlackHistoryPermissionError(errorCode: string | undefined): boolean {\n\treturn errorCode === \"missing_scope\" || errorCode === \"no_permission\" || errorCode === \"not_allowed_token_type\";\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 { createHandoffServer, SlackHandoffController } from \"./handoff.js\";\nimport { HandoffReplyCache } from \"./handoff-reply-cache.js\";\nimport * as log from \"./log.js\";\nimport { resolveRoute, resolveStreamingPreference } 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\tprivate handoffReplyCache = new HandoffReplyCache();\n\tprivate slackThreadHistoryUnavailable = false;\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\t\tposted: (target, ref, text) => this.handoffReplyCache.recordPosted(target, ref, text, this.botDisplayName()),\n\t\t\tupdated: (target, ref, text) => this.handoffReplyCache.recordUpdated(target, ref, text, this.botDisplayName()),\n\t\t});\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.startHandoffServer();\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 startHandoffServer(): void {\n\t\tconst handoffConfig = this.config.handoff;\n\t\tif (!handoffConfig?.enabled) return;\n\t\tif (!this.teamId || !this.engine) throw new Error(\"Gateway must be initialized before handoff server startup\");\n\t\tconst controller = new SlackHandoffController(handoffConfig, {\n\t\t\tteamId: this.teamId,\n\t\t\tpostRootMessage: async (channelId, text) => this.sink.postMessage({ channelId }, text),\n\t\t\tgetPermalink: async (channelId, messageTs) => {\n\t\t\t\tconst result = await this.webClient.chat.getPermalink({ channel: channelId, message_ts: messageTs });\n\t\t\t\tif (!result.permalink) throw new Error(\"Slack did not return a permalink\");\n\t\t\t\treturn result.permalink;\n\t\t\t},\n\t\t\tdispatch: (input) => this.engine!.dispatch(input),\n\t\t\tgetReplies: async (channelId, threadTs) => {\n\t\t\t\tif (this.slackThreadHistoryUnavailable) return this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.webClient.conversations.replies({\n\t\t\t\t\t\tchannel: channelId,\n\t\t\t\t\t\tts: threadTs,\n\t\t\t\t\t\tlimit: 100,\n\t\t\t\t\t});\n\t\t\t\t\treturn (result.messages || []).map((message) => {\n\t\t\t\t\t\tconst typed = message as typeof message & {\n\t\t\t\t\t\t\tbot_id?: string;\n\t\t\t\t\t\t\tbot_profile?: { name?: string };\n\t\t\t\t\t\t\tusername?: string;\n\t\t\t\t\t\t\tthread_ts?: string;\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tts: String(typed.ts || \"\"),\n\t\t\t\t\t\t\tthreadTs: typed.thread_ts,\n\t\t\t\t\t\t\ttext: typed.text || \"\",\n\t\t\t\t\t\t\tauthor: typed.user\n\t\t\t\t\t\t\t\t? this.users.get(typed.user)?.displayName || this.users.get(typed.user)?.userName || typed.user\n\t\t\t\t\t\t\t\t: typed.bot_profile?.name || typed.username || \"Bot\",\n\t\t\t\t\t\t\tisBot: Boolean(typed.bot_id),\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst errorCode = slackErrorCode(error);\n\t\t\t\t\tif (!isSlackHistoryPermissionError(errorCode)) throw error;\n\t\t\t\t\tthis.slackThreadHistoryUnavailable = true;\n\t\t\t\t\tlog.logWarning(`Slack thread history unavailable (${errorCode}); using outbound Bee reply cache`);\n\t\t\t\t\treturn this.handoffReplyCache.replies(channelId, threadTs);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst server = createHandoffServer(controller);\n\t\tconst host = handoffConfig.host || \"0.0.0.0\";\n\t\tconst port = handoffConfig.port || 8080;\n\t\tserver.listen(port, host, () => log.logInfo(`bee-slack handoff listening on http://${host}:${port}`));\n\t}\n\n\tprivate botDisplayName(): string {\n\t\tif (!this.botUserId) return \"Bee\";\n\t\tconst bot = this.users.get(this.botUserId);\n\t\treturn bot?.displayName || bot?.userName || \"Bee\";\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack, body }) => {\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\tteamId: (event as typeof e & { team?: string }).team || (body as { team_id?: string }).team_id,\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\tstreaming: resolveStreamingPreference(resolved.route, 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\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\nfunction slackErrorCode(error: unknown): string | undefined {\n\tif (!error || typeof error !== \"object\" || !(\"data\" in error)) return undefined;\n\tconst data = error.data;\n\tif (!data || typeof data !== \"object\" || !(\"error\" in data)) return undefined;\n\treturn typeof data.error === \"string\" ? data.error : undefined;\n}\n\nfunction isSlackHistoryPermissionError(errorCode: string | undefined): boolean {\n\treturn errorCode === \"missing_scope\" || errorCode === \"no_permission\" || errorCode === \"not_allowed_token_type\";\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/router.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type TransportStreamingPreference } from "@jobmatchme/bee-gate";
|
|
1
2
|
import type { ResolvedSlackRoute, SlackGatewayContext, SlackInboundMessage, SlackRouteConfig } from "./types.js";
|
|
2
3
|
export declare function resolveRoute(routes: SlackRouteConfig[], message: SlackInboundMessage, context: SlackGatewayContext): ResolvedSlackRoute | null;
|
|
4
|
+
export declare function resolveStreamingPreference(route: SlackRouteConfig, message: SlackInboundMessage, context: SlackGatewayContext): TransportStreamingPreference | undefined;
|
|
3
5
|
//# sourceMappingURL=router.d.ts.map
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,KAAK,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AAC/G,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA0BjH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,gBAAgB,EAAE,EAC1B,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,mBAAmB,GAC1B,kBAAkB,GAAG,IAAI,CAkB3B;AAED,wBAAgB,0BAA0B,CACzC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,mBAAmB,GAC1B,4BAA4B,GAAG,SAAS,CAY1C","sourcesContent":["import { buildConversationId, buildSessionKey, type TransportStreamingPreference } from \"@jobmatchme/bee-gate\";\nimport type { ResolvedSlackRoute, SlackGatewayContext, SlackInboundMessage, SlackRouteConfig } from \"./types.js\";\n\nfunction matchesRoute(route: SlackRouteConfig, message: SlackInboundMessage): boolean {\n\tconst match = route.match;\n\n\tif (message.type === \"dm\") {\n\t\tif (match.dm !== true) return false;\n\t} else if (match.dm === true) {\n\t\treturn false;\n\t}\n\n\tif (match.channelIds && !match.channelIds.includes(message.channelId)) {\n\t\treturn false;\n\t}\n\n\tif (match.channelNames && (!message.channelName || !match.channelNames.includes(message.channelName))) {\n\t\treturn false;\n\t}\n\n\tif (match.textPrefix && !message.text.trim().startsWith(match.textPrefix)) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nexport function resolveRoute(\n\troutes: SlackRouteConfig[],\n\tmessage: SlackInboundMessage,\n\tcontext: SlackGatewayContext,\n): ResolvedSlackRoute | null {\n\tconst route = routes.find((candidate) => matchesRoute(candidate, message));\n\tif (!route) return null;\n\n\tconst threadId = message.threadTs || (message.type === \"mention\" ? message.ts : undefined);\n\tconst strategy = route.session?.strategy || \"thread\";\n\tconst sessionBase = strategy === \"channel\" ? message.channelId : threadId || message.ts;\n\tconst conversationId = buildConversationId([\"slack\", context.teamId, message.channelId, threadId || message.ts]);\n\tconst prefix = route.session?.prefix || route.id;\n\treturn {\n\t\troute,\n\t\tconversationId,\n\t\tsessionId: buildSessionKey(\n\t\t\tprefix,\n\t\t\tbuildConversationId([\"slack\", context.teamId, message.channelId, sessionBase]),\n\t\t),\n\t\tthreadTs: threadId,\n\t};\n}\n\nexport function resolveStreamingPreference(\n\troute: SlackRouteConfig,\n\tmessage: SlackInboundMessage,\n\tcontext: SlackGatewayContext,\n): TransportStreamingPreference | undefined {\n\tif (message.type !== \"mention\" || route.streaming?.enabled !== true) return undefined;\n\n\treturn {\n\t\tenabled: true,\n\t\trouteId: route.id,\n\t\tpresentation: route.streaming.taskDisplayMode || \"timeline\",\n\t\tcontext: {\n\t\t\trecipientUserId: message.userId,\n\t\t\trecipientTeamId: message.teamId || context.teamId,\n\t\t},\n\t};\n}\n"]}
|
package/dist/router.js
CHANGED
|
@@ -35,4 +35,17 @@ export function resolveRoute(routes, message, context) {
|
|
|
35
35
|
threadTs: threadId,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
export function resolveStreamingPreference(route, message, context) {
|
|
39
|
+
if (message.type !== "mention" || route.streaming?.enabled !== true)
|
|
40
|
+
return undefined;
|
|
41
|
+
return {
|
|
42
|
+
enabled: true,
|
|
43
|
+
routeId: route.id,
|
|
44
|
+
presentation: route.streaming.taskDisplayMode || "timeline",
|
|
45
|
+
context: {
|
|
46
|
+
recipientUserId: message.userId,
|
|
47
|
+
recipientTeamId: message.teamId || context.teamId,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
38
51
|
//# sourceMappingURL=router.js.map
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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","sourcesContent":["import { buildConversationId, buildSessionKey, type TransportStreamingPreference } from \"@jobmatchme/bee-gate\";\nimport type { ResolvedSlackRoute, SlackGatewayContext, SlackInboundMessage, SlackRouteConfig } from \"./types.js\";\n\nfunction matchesRoute(route: SlackRouteConfig, message: SlackInboundMessage): boolean {\n\tconst match = route.match;\n\n\tif (message.type === \"dm\") {\n\t\tif (match.dm !== true) return false;\n\t} else if (match.dm === true) {\n\t\treturn false;\n\t}\n\n\tif (match.channelIds && !match.channelIds.includes(message.channelId)) {\n\t\treturn false;\n\t}\n\n\tif (match.channelNames && (!message.channelName || !match.channelNames.includes(message.channelName))) {\n\t\treturn false;\n\t}\n\n\tif (match.textPrefix && !message.text.trim().startsWith(match.textPrefix)) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nexport function resolveRoute(\n\troutes: SlackRouteConfig[],\n\tmessage: SlackInboundMessage,\n\tcontext: SlackGatewayContext,\n): ResolvedSlackRoute | null {\n\tconst route = routes.find((candidate) => matchesRoute(candidate, message));\n\tif (!route) return null;\n\n\tconst threadId = message.threadTs || (message.type === \"mention\" ? message.ts : undefined);\n\tconst strategy = route.session?.strategy || \"thread\";\n\tconst sessionBase = strategy === \"channel\" ? message.channelId : threadId || message.ts;\n\tconst conversationId = buildConversationId([\"slack\", context.teamId, message.channelId, threadId || message.ts]);\n\tconst prefix = route.session?.prefix || route.id;\n\treturn {\n\t\troute,\n\t\tconversationId,\n\t\tsessionId: buildSessionKey(\n\t\t\tprefix,\n\t\t\tbuildConversationId([\"slack\", context.teamId, message.channelId, sessionBase]),\n\t\t),\n\t\tthreadTs: threadId,\n\t};\n}\n\nexport function resolveStreamingPreference(\n\troute: SlackRouteConfig,\n\tmessage: SlackInboundMessage,\n\tcontext: SlackGatewayContext,\n): TransportStreamingPreference | undefined {\n\tif (message.type !== \"mention\" || route.streaming?.enabled !== true) return undefined;\n\n\treturn {\n\t\tenabled: true,\n\t\trouteId: route.id,\n\t\tpresentation: route.streaming.taskDisplayMode || \"timeline\",\n\t\tcontext: {\n\t\t\trecipientUserId: message.userId,\n\t\t\trecipientTeamId: message.teamId || context.teamId,\n\t\t},\n\t};\n}\n"]}
|
package/dist/slack-sink.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type { ArtifactRef, BlobStore, TransportOutputTarget, TransportSink } from "@jobmatchme/bee-gate";
|
|
1
|
+
import type { ActionUpdate, ArtifactRef, BlobStore, TransportOutputTarget, TransportSink, TransportStreamResult, TransportStreamStart } from "@jobmatchme/bee-gate";
|
|
2
2
|
import type { WebClient } from "@slack/web-api";
|
|
3
|
+
export declare const SLACK_STREAM_HEADER = "Ich bearbeite jetzt deine Anfrage...";
|
|
4
|
+
export declare const SLACK_TASK_TEXT_LIMIT = 256;
|
|
5
|
+
export declare const SLACK_MARKDOWN_LIMIT = 12000;
|
|
6
|
+
export declare const SLACK_MARKDOWN_TRUNCATION_SUFFIX = "\n\n_[Antwort auf 12.000 Zeichen gek\u00FCrzt]_";
|
|
7
|
+
export declare function truncateSlackTaskText(text: string): string;
|
|
8
|
+
export declare function truncateSlackMarkdown(text: string): string;
|
|
3
9
|
export interface SlackSinkObserver {
|
|
4
10
|
posted(target: TransportOutputTarget, ref: string, text: string): void;
|
|
5
11
|
updated(target: TransportOutputTarget, ref: string, text: string): void;
|
|
@@ -11,6 +17,10 @@ export declare class SlackSink implements TransportSink<string> {
|
|
|
11
17
|
constructor(webClient: WebClient, blobStore: BlobStore, observer?: SlackSinkObserver | undefined);
|
|
12
18
|
postMessage(target: TransportOutputTarget, text: string): Promise<string>;
|
|
13
19
|
updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void>;
|
|
20
|
+
prepareStreamText(text: string): string;
|
|
21
|
+
startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string>;
|
|
22
|
+
updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void>;
|
|
23
|
+
stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void>;
|
|
14
24
|
publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void>;
|
|
15
25
|
}
|
|
16
26
|
//# sourceMappingURL=slack-sink.d.ts.map
|
package/dist/slack-sink.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
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;IAEE,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,CAgBlG;IAEK,UAAU,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAczG;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\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\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...(action.details === undefined ? {} : { details: truncateSlackTaskText(action.details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\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\tmarkdown_text: truncateSlackMarkdown(result.text),\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t} catch {\n\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\tawait this.webClient.chat.stopStream(payload);\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/dist/slack-sink.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { createReadStream } from "fs";
|
|
2
|
+
export const SLACK_STREAM_HEADER = "Ich bearbeite jetzt deine Anfrage...";
|
|
3
|
+
export const SLACK_TASK_TEXT_LIMIT = 256;
|
|
4
|
+
export const SLACK_MARKDOWN_LIMIT = 12_000;
|
|
5
|
+
export const SLACK_MARKDOWN_TRUNCATION_SUFFIX = "\n\n_[Antwort auf 12.000 Zeichen gekürzt]_";
|
|
6
|
+
function truncateCodePoints(text, limit, suffix) {
|
|
7
|
+
const codePoints = Array.from(text);
|
|
8
|
+
if (codePoints.length <= limit)
|
|
9
|
+
return text;
|
|
10
|
+
const suffixCodePoints = Array.from(suffix);
|
|
11
|
+
return `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join("")}${suffix}`;
|
|
12
|
+
}
|
|
13
|
+
export function truncateSlackTaskText(text) {
|
|
14
|
+
return truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, "…");
|
|
15
|
+
}
|
|
16
|
+
export function truncateSlackMarkdown(text) {
|
|
17
|
+
return truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);
|
|
18
|
+
}
|
|
19
|
+
function requiredContextString(start, key) {
|
|
20
|
+
const value = start.context?.[key];
|
|
21
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
22
|
+
throw new Error(`Missing Slack stream context ${key}`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
2
26
|
export class SlackSink {
|
|
3
27
|
webClient;
|
|
4
28
|
blobStore;
|
|
@@ -32,6 +56,69 @@ export class SlackSink {
|
|
|
32
56
|
});
|
|
33
57
|
this.observer?.updated(target, ref, text);
|
|
34
58
|
}
|
|
59
|
+
prepareStreamText(text) {
|
|
60
|
+
return truncateSlackMarkdown(text);
|
|
61
|
+
}
|
|
62
|
+
async startStream(target, start) {
|
|
63
|
+
if (!target.channelId)
|
|
64
|
+
throw new Error("Missing Slack channel id");
|
|
65
|
+
if (!target.threadId)
|
|
66
|
+
throw new Error("Missing Slack stream thread timestamp");
|
|
67
|
+
const headerChunk = {
|
|
68
|
+
type: "blocks",
|
|
69
|
+
blocks: [
|
|
70
|
+
{
|
|
71
|
+
type: "header",
|
|
72
|
+
text: { type: "plain_text", text: SLACK_STREAM_HEADER },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const result = await this.webClient.chat.startStream({
|
|
77
|
+
channel: target.channelId,
|
|
78
|
+
thread_ts: target.threadId,
|
|
79
|
+
recipient_user_id: requiredContextString(start, "recipientUserId"),
|
|
80
|
+
recipient_team_id: requiredContextString(start, "recipientTeamId"),
|
|
81
|
+
task_display_mode: start.presentation || "timeline",
|
|
82
|
+
// Slack supports block chunks although the current SDK's chunk union omits them.
|
|
83
|
+
chunks: [headerChunk],
|
|
84
|
+
});
|
|
85
|
+
if (!result.ts)
|
|
86
|
+
throw new Error("Slack chat.startStream did not return a message timestamp");
|
|
87
|
+
return result.ts;
|
|
88
|
+
}
|
|
89
|
+
async updateStream(target, ref, action) {
|
|
90
|
+
if (!target.channelId)
|
|
91
|
+
throw new Error("Missing Slack channel id");
|
|
92
|
+
await this.webClient.chat.appendStream({
|
|
93
|
+
channel: target.channelId,
|
|
94
|
+
ts: ref,
|
|
95
|
+
chunks: [
|
|
96
|
+
{
|
|
97
|
+
type: "task_update",
|
|
98
|
+
id: action.id,
|
|
99
|
+
title: truncateSlackTaskText(action.title),
|
|
100
|
+
...(action.details === undefined ? {} : { details: truncateSlackTaskText(action.details) }),
|
|
101
|
+
status: action.status,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async stopStream(target, ref, result) {
|
|
107
|
+
if (!target.channelId)
|
|
108
|
+
throw new Error("Missing Slack channel id");
|
|
109
|
+
const payload = {
|
|
110
|
+
channel: target.channelId,
|
|
111
|
+
ts: ref,
|
|
112
|
+
markdown_text: truncateSlackMarkdown(result.text),
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
await this.webClient.chat.stopStream(payload);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// One explicit retry keeps final-answer delivery resilient while remaining bounded.
|
|
119
|
+
await this.webClient.chat.stopStream(payload);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
35
122
|
async publishArtifact(target, artifact) {
|
|
36
123
|
if (!target.channelId) {
|
|
37
124
|
throw new Error("Missing Slack channel id");
|
package/dist/slack-sink.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAOtC,MAAM,OAAO,SAAS;IAEZ,SAAS;IACT,SAAS;IACT,QAAQ;IAHjB,YACS,SAAoB,EACpB,SAAoB,EACpB,QAA4B,EACnC;yBAHO,SAAS;yBACT,SAAS;wBACT,QAAQ;IACd,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,MAAM,GAAG,GAAG,MAAM,CAAC,EAAY,CAAC;QAChC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACzC,OAAO,GAAG,CAAC;IAAA,CACX;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;QACH,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAAA,CAC1C;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 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\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\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_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\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\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...(action.details === undefined ? {} : { details: truncateSlackTaskText(action.details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\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\tmarkdown_text: truncateSlackMarkdown(result.text),\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t} catch {\n\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\tawait this.webClient.chat.stopStream(payload);\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/dist/types.d.ts
CHANGED
|
@@ -9,11 +9,16 @@ export interface SlackSessionConfig {
|
|
|
9
9
|
strategy?: "channel" | "thread";
|
|
10
10
|
prefix?: string;
|
|
11
11
|
}
|
|
12
|
+
export interface SlackRouteStreamingConfig {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
taskDisplayMode?: "timeline" | "plan" | "dense";
|
|
15
|
+
}
|
|
12
16
|
export interface SlackRouteConfig {
|
|
13
17
|
id: string;
|
|
14
18
|
match: SlackRouteMatch;
|
|
15
19
|
worker: BeeWorkerTargetConfig;
|
|
16
20
|
session?: SlackSessionConfig;
|
|
21
|
+
streaming?: SlackRouteStreamingConfig;
|
|
17
22
|
}
|
|
18
23
|
export interface SlackGatewayConfig {
|
|
19
24
|
appToken: string;
|
|
@@ -88,6 +93,8 @@ export interface SlackInboundMessage {
|
|
|
88
93
|
threadTs?: string;
|
|
89
94
|
ts: string;
|
|
90
95
|
userId: string;
|
|
96
|
+
/** Recipient workspace from the inbound event, relevant for Slack Connect channels. */
|
|
97
|
+
teamId?: string;
|
|
91
98
|
userName?: string;
|
|
92
99
|
displayName?: string;
|
|
93
100
|
text: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,OAAO,CAAC,EAAE,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\nexport interface SlackRouteStreamingConfig {\n\tenabled: boolean;\n\ttaskDisplayMode?: \"timeline\" | \"plan\" | \"dense\";\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n\tstreaming?: SlackRouteStreamingConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n\thandoff?: SlackHandoffConfig;\n}\n\nexport interface SlackHandoffConfig {\n\tenabled?: boolean;\n\thost?: string;\n\tport?: number;\n\tallowedDashboardHosts?: string[];\n\troutes: SlackHandoffRouteConfig[];\n}\n\nexport interface SlackHandoffRouteConfig {\n\tid: string;\n\tlabel?: string;\n\tchannelId: string;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackHandoffActor {\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n}\n\nexport interface SlackHandoffContext {\n\tdashboardTitle?: string;\n\tdashboardUid?: string;\n\tpanelTitle?: string;\n\tpanelId?: number;\n\turl: string;\n\ttimeRange?: string;\n\tvariables?: Record<string, string>;\n}\n\nexport interface SlackHandoffRequest {\n\trouteId: string;\n\ttext: string;\n\tactor: SlackHandoffActor;\n\tcontext: SlackHandoffContext;\n}\n\nexport interface SlackHandoffReply {\n\tts: string;\n\tthreadTs?: string;\n\ttext: string;\n\tauthor?: string;\n\tisBot: boolean;\n}\n\nexport interface SlackHandoffRecord {\n\trouteId: string;\n\tchannelId: string;\n\tthreadTs: string;\n\tpermalink: string;\n\tcreatedAt: string;\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\t/** Recipient workspace from the inbound event, relevant for Slack Connect channels. */\n\tteamId?: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n\thandoff?: SlackHandoffConfig;\n}\n\nexport interface SlackHandoffConfig {\n\tenabled?: boolean;\n\thost?: string;\n\tport?: number;\n\tallowedDashboardHosts?: string[];\n\troutes: SlackHandoffRouteConfig[];\n}\n\nexport interface SlackHandoffRouteConfig {\n\tid: string;\n\tlabel?: string;\n\tchannelId: string;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackHandoffActor {\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n}\n\nexport interface SlackHandoffContext {\n\tdashboardTitle?: string;\n\tdashboardUid?: string;\n\tpanelTitle?: string;\n\tpanelId?: number;\n\turl: string;\n\ttimeRange?: string;\n\tvariables?: Record<string, string>;\n}\n\nexport interface SlackHandoffRequest {\n\trouteId: string;\n\ttext: string;\n\tactor: SlackHandoffActor;\n\tcontext: SlackHandoffContext;\n}\n\nexport interface SlackHandoffReply {\n\tts: string;\n\tthreadTs?: string;\n\ttext: string;\n\tauthor?: string;\n\tisBot: boolean;\n}\n\nexport interface SlackHandoffRecord {\n\trouteId: string;\n\tchannelId: string;\n\tthreadTs: string;\n\tpermalink: string;\n\tcreatedAt: string;\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { BeeWorkerTargetConfig } from \"@jobmatchme/bee-gate\";\n\nexport interface SlackRouteMatch {\n\tchannelIds?: string[];\n\tchannelNames?: string[];\n\tdm?: boolean;\n\ttextPrefix?: string;\n}\n\nexport interface SlackSessionConfig {\n\tstrategy?: \"channel\" | \"thread\";\n\tprefix?: string;\n}\nexport interface SlackRouteStreamingConfig {\n\tenabled: boolean;\n\ttaskDisplayMode?: \"timeline\" | \"plan\" | \"dense\";\n}\n\nexport interface SlackRouteConfig {\n\tid: string;\n\tmatch: SlackRouteMatch;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n\tstreaming?: SlackRouteStreamingConfig;\n}\n\nexport interface SlackGatewayConfig {\n\tappToken: string;\n\tbotToken: string;\n\tnats: {\n\t\tservers: string | string[];\n\t\tname?: string;\n\t};\n\troutes: SlackRouteConfig[];\n\thandoff?: SlackHandoffConfig;\n}\n\nexport interface SlackHandoffConfig {\n\tenabled?: boolean;\n\thost?: string;\n\tport?: number;\n\tallowedDashboardHosts?: string[];\n\troutes: SlackHandoffRouteConfig[];\n}\n\nexport interface SlackHandoffRouteConfig {\n\tid: string;\n\tlabel?: string;\n\tchannelId: string;\n\tworker: BeeWorkerTargetConfig;\n\tsession?: SlackSessionConfig;\n}\n\nexport interface SlackHandoffActor {\n\tuserId: string;\n\tuserName?: string;\n\tdisplayName?: string;\n}\n\nexport interface SlackHandoffContext {\n\tdashboardTitle?: string;\n\tdashboardUid?: string;\n\tpanelTitle?: string;\n\tpanelId?: number;\n\turl: string;\n\ttimeRange?: string;\n\tvariables?: Record<string, string>;\n}\n\nexport interface SlackHandoffRequest {\n\trouteId: string;\n\ttext: string;\n\tactor: SlackHandoffActor;\n\tcontext: SlackHandoffContext;\n}\n\nexport interface SlackHandoffReply {\n\tts: string;\n\tthreadTs?: string;\n\ttext: string;\n\tauthor?: string;\n\tisBot: boolean;\n}\n\nexport interface SlackHandoffRecord {\n\trouteId: string;\n\tchannelId: string;\n\tthreadTs: string;\n\tpermalink: string;\n\tcreatedAt: string;\n}\n\nexport interface SlackFile {\n\tid?: string;\n\tmimetype?: string;\n\tname?: string;\n\tsize?: number;\n\turl_private_download?: string;\n\turl_private?: string;\n}\n\nexport interface SlackInboundMessage {\n\ttype: \"mention\" | \"dm\";\n\tchannelId: string;\n\tchannelName?: string;\n\tthreadTs?: string;\n\tts: string;\n\tuserId: string;\n\t/** Recipient workspace from the inbound event, relevant for Slack Connect channels. */\n\tteamId?: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tfiles?: SlackFile[];\n}\n\nexport interface ResolvedSlackRoute {\n\troute: SlackRouteConfig;\n\tsessionId: string;\n\tthreadTs?: string;\n\tconversationId: string;\n}\n\nexport interface SlackGatewayContext {\n\tteamId: string;\n\tteamName?: string;\n\tbotUserId: string;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobmatchme/bee-slack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Slack adapter for Bee Gate and the Bee Dance protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"prepublishOnly": "npm run clean && npm run check"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@jobmatchme/bee-gate": "0.1.
|
|
51
|
+
"@jobmatchme/bee-gate": "0.1.9",
|
|
52
52
|
"@slack/socket-mode": "^2.0.0",
|
|
53
53
|
"@slack/web-api": "^7.11.0",
|
|
54
54
|
"chalk": "^5.6.2"
|