@opentag/slack 0.1.0

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 ADDED
@@ -0,0 +1,49 @@
1
+ # @opentag/slack
2
+
3
+ Slack adapter helpers for OpenTag.
4
+
5
+ Use this package to normalize Slack `app_mention` events into `OpenTagEvent` objects and to encode or parse Slack callback thread keys.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @opentag/slack
11
+ ```
12
+
13
+ ## Exports
14
+
15
+ - `normalizeSlackAppMention`: converts a Slack app mention into an `OpenTagEvent`.
16
+ - `slackThreadKey`: encodes team, channel, and thread timestamp for callback routing.
17
+ - `parseSlackThreadKey`: decodes a Slack thread key for `chat.postMessage`.
18
+ - `SlackChannelBinding`: channel-to-repository binding contract.
19
+
20
+ ## Example
21
+
22
+ ```ts
23
+ import { normalizeSlackAppMention } from "@opentag/slack";
24
+
25
+ const event = normalizeSlackAppMention({
26
+ teamId: "T123",
27
+ channelId: "C123",
28
+ userId: "U456",
29
+ text: "<@U_APP> investigate this deploy failure",
30
+ ts: "1710000000.000100",
31
+ eventId: "Ev123",
32
+ eventTime: 1710000000,
33
+ botUserId: "U_APP",
34
+ binding: {
35
+ teamId: "T123",
36
+ channelId: "C123",
37
+ owner: "acme",
38
+ repo: "demo"
39
+ }
40
+ });
41
+
42
+ if (event) {
43
+ // Send event to @opentag/client or your own OpenTag-compatible control plane.
44
+ }
45
+ ```
46
+
47
+ ## Stability
48
+
49
+ Thread key format is public because callback sinks depend on it. Change it only with a migration path.
@@ -0,0 +1,2 @@
1
+ export * from "./normalize.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ // src/normalize.ts
2
+ import { commandFromRawText } from "@opentag/core";
3
+ function stripSlackAppMention(text, botUserId) {
4
+ const patterns = botUserId ? [new RegExp(`^<@${botUserId}>\\s*`, "i"), /^<@[^>]+>\s*/] : [/^<@[^>]+>\s*/];
5
+ for (const pattern of patterns) {
6
+ const stripped = text.replace(pattern, "").trim();
7
+ if (stripped !== text.trim()) {
8
+ return stripped.length > 0 ? stripped : null;
9
+ }
10
+ }
11
+ return null;
12
+ }
13
+ function encodeSlackThreadKey(input) {
14
+ return `${input.teamId}|${input.channelId}|${input.threadTs}`;
15
+ }
16
+ function parseSlackThreadKey(threadKey) {
17
+ const [teamId, channelId, threadTs] = threadKey.split("|");
18
+ if (!teamId || !channelId || !threadTs) {
19
+ throw new Error(`Invalid Slack thread key: ${threadKey}`);
20
+ }
21
+ return { teamId, channelId, threadTs };
22
+ }
23
+ function permissionsForIntent(intent) {
24
+ const permissions = [
25
+ {
26
+ scope: "chat:postMessage",
27
+ reason: "reply in the originating Slack thread"
28
+ },
29
+ {
30
+ scope: "runner:local",
31
+ reason: "execute the run on a paired local daemon"
32
+ }
33
+ ];
34
+ if (intent === "fix" || intent === "run") {
35
+ permissions.push(
36
+ {
37
+ scope: "repo:read",
38
+ reason: "inspect the repository in the paired local checkout"
39
+ },
40
+ {
41
+ scope: "repo:write",
42
+ reason: "commit code changes on an isolated run branch"
43
+ },
44
+ {
45
+ scope: "pr:create",
46
+ reason: "open a pull request for completed code changes"
47
+ }
48
+ );
49
+ }
50
+ return permissions;
51
+ }
52
+ function normalizeSlackAppMention(input) {
53
+ const rawText = stripSlackAppMention(input.text, input.botUserId);
54
+ if (!rawText) return null;
55
+ const command = commandFromRawText(rawText);
56
+ const replyThreadTs = input.threadTs ?? input.ts;
57
+ return {
58
+ id: `evt_slack_app_mention_${input.eventId}`,
59
+ source: "slack",
60
+ sourceEventId: input.eventId,
61
+ receivedAt: new Date(input.eventTime * 1e3).toISOString(),
62
+ actor: {
63
+ provider: "slack",
64
+ providerUserId: input.userId,
65
+ handle: input.userId,
66
+ organizationId: input.teamId
67
+ },
68
+ target: {
69
+ mention: input.botUserId ? `<@${input.botUserId}>` : "<@app>",
70
+ agentId: "opentag"
71
+ },
72
+ command,
73
+ context: [
74
+ {
75
+ kind: "url",
76
+ uri: `slack://team/${input.teamId}/channel/${input.channelId}/message/${input.ts}`,
77
+ visibility: "organization",
78
+ title: "Slack message"
79
+ },
80
+ {
81
+ kind: "text",
82
+ uri: input.text,
83
+ visibility: "organization",
84
+ title: "Slack message text"
85
+ }
86
+ ],
87
+ permissions: permissionsForIntent(command.intent),
88
+ callback: {
89
+ provider: "slack",
90
+ uri: input.callbackUri ?? "https://slack.com/api/chat.postMessage",
91
+ threadKey: encodeSlackThreadKey({
92
+ teamId: input.teamId,
93
+ channelId: input.channelId,
94
+ threadTs: replyThreadTs
95
+ })
96
+ },
97
+ metadata: {
98
+ teamId: input.teamId,
99
+ channelId: input.channelId,
100
+ messageTs: input.ts,
101
+ repoProvider: "github",
102
+ owner: input.binding.owner,
103
+ repo: input.binding.repo
104
+ }
105
+ };
106
+ }
107
+ export {
108
+ encodeSlackThreadKey,
109
+ normalizeSlackAppMention,
110
+ parseSlackThreadKey,
111
+ stripSlackAppMention
112
+ };
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/normalize.ts"],"sourcesContent":["import { commandFromRawText, type OpenTagEvent, type PermissionGrant } from \"@opentag/core\";\n\nexport type SlackChannelBinding = {\n teamId: string;\n channelId: string;\n owner: string;\n repo: string;\n};\n\nexport type SlackAppMentionInput = {\n teamId: string;\n channelId: string;\n userId: string;\n text: string;\n ts: string;\n threadTs?: string;\n eventId: string;\n eventTime: number;\n botUserId?: string;\n callbackUri?: string;\n binding: SlackChannelBinding;\n};\n\nexport function stripSlackAppMention(text: string, botUserId?: string): string | null {\n const patterns = botUserId\n ? [new RegExp(`^<@${botUserId}>\\\\s*`, \"i\"), /^<@[^>]+>\\s*/]\n : [/^<@[^>]+>\\s*/];\n\n for (const pattern of patterns) {\n const stripped = text.replace(pattern, \"\").trim();\n if (stripped !== text.trim()) {\n return stripped.length > 0 ? stripped : null;\n }\n }\n\n return null;\n}\n\nexport function encodeSlackThreadKey(input: { teamId: string; channelId: string; threadTs: string }): string {\n return `${input.teamId}|${input.channelId}|${input.threadTs}`;\n}\n\nexport function parseSlackThreadKey(threadKey: string): { teamId: string; channelId: string; threadTs: string } {\n const [teamId, channelId, threadTs] = threadKey.split(\"|\");\n if (!teamId || !channelId || !threadTs) {\n throw new Error(`Invalid Slack thread key: ${threadKey}`);\n }\n return { teamId, channelId, threadTs };\n}\n\nfunction permissionsForIntent(intent: ReturnType<typeof commandFromRawText>[\"intent\"]): PermissionGrant[] {\n const permissions: PermissionGrant[] = [\n {\n scope: \"chat:postMessage\",\n reason: \"reply in the originating Slack thread\"\n },\n {\n scope: \"runner:local\",\n reason: \"execute the run on a paired local daemon\"\n }\n ];\n\n if (intent === \"fix\" || intent === \"run\") {\n permissions.push(\n {\n scope: \"repo:read\",\n reason: \"inspect the repository in the paired local checkout\"\n },\n {\n scope: \"repo:write\",\n reason: \"commit code changes on an isolated run branch\"\n },\n {\n scope: \"pr:create\",\n reason: \"open a pull request for completed code changes\"\n }\n );\n }\n\n return permissions;\n}\n\nexport function normalizeSlackAppMention(input: SlackAppMentionInput): OpenTagEvent | null {\n const rawText = stripSlackAppMention(input.text, input.botUserId);\n if (!rawText) return null;\n\n const command = commandFromRawText(rawText);\n const replyThreadTs = input.threadTs ?? input.ts;\n\n return {\n id: `evt_slack_app_mention_${input.eventId}`,\n source: \"slack\",\n sourceEventId: input.eventId,\n receivedAt: new Date(input.eventTime * 1000).toISOString(),\n actor: {\n provider: \"slack\",\n providerUserId: input.userId,\n handle: input.userId,\n organizationId: input.teamId\n },\n target: {\n mention: input.botUserId ? `<@${input.botUserId}>` : \"<@app>\",\n agentId: \"opentag\"\n },\n command,\n context: [\n {\n kind: \"url\",\n uri: `slack://team/${input.teamId}/channel/${input.channelId}/message/${input.ts}`,\n visibility: \"organization\",\n title: \"Slack message\"\n },\n {\n kind: \"text\",\n uri: input.text,\n visibility: \"organization\",\n title: \"Slack message text\"\n }\n ],\n permissions: permissionsForIntent(command.intent),\n callback: {\n provider: \"slack\",\n uri: input.callbackUri ?? \"https://slack.com/api/chat.postMessage\",\n threadKey: encodeSlackThreadKey({\n teamId: input.teamId,\n channelId: input.channelId,\n threadTs: replyThreadTs\n })\n },\n metadata: {\n teamId: input.teamId,\n channelId: input.channelId,\n messageTs: input.ts,\n repoProvider: \"github\",\n owner: input.binding.owner,\n repo: input.binding.repo\n }\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,33 @@
1
+ import { type OpenTagEvent } from "@opentag/core";
2
+ export type SlackChannelBinding = {
3
+ teamId: string;
4
+ channelId: string;
5
+ owner: string;
6
+ repo: string;
7
+ };
8
+ export type SlackAppMentionInput = {
9
+ teamId: string;
10
+ channelId: string;
11
+ userId: string;
12
+ text: string;
13
+ ts: string;
14
+ threadTs?: string;
15
+ eventId: string;
16
+ eventTime: number;
17
+ botUserId?: string;
18
+ callbackUri?: string;
19
+ binding: SlackChannelBinding;
20
+ };
21
+ export declare function stripSlackAppMention(text: string, botUserId?: string): string | null;
22
+ export declare function encodeSlackThreadKey(input: {
23
+ teamId: string;
24
+ channelId: string;
25
+ threadTs: string;
26
+ }): string;
27
+ export declare function parseSlackThreadKey(threadKey: string): {
28
+ teamId: string;
29
+ channelId: string;
30
+ threadTs: string;
31
+ };
32
+ export declare function normalizeSlackAppMention(input: SlackAppMentionInput): OpenTagEvent | null;
33
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,YAAY,EAAwB,MAAM,eAAe,CAAC;AAE5F,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAE3G;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAM9G;AAkCD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,oBAAoB,GAAG,YAAY,GAAG,IAAI,CAwDzF"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@opentag/slack",
3
+ "version": "0.1.0",
4
+ "description": "Slack app mention normalization and callback helpers for OpenTag.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "development": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "keywords": [
23
+ "opentag",
24
+ "slack",
25
+ "events",
26
+ "agents",
27
+ "webhooks"
28
+ ],
29
+ "license": "Apache-2.0",
30
+ "dependencies": {
31
+ "@opentag/core": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "tsup": "^8.5.1",
35
+ "typescript": "^5.9.3"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup && tsc -b tsconfig.json --emitDeclarationOnly --force",
39
+ "lint": "tsc --noEmit"
40
+ }
41
+ }