@meetopenbot/slack 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,11 +24,13 @@ plugins:
24
24
  signingSecret: your-signing-secret
25
25
  botToken: xoxb-your-bot-token
26
26
  teamId: T01234567
27
- openaiApiKey: sk-...
28
- model: gpt-4o-mini
27
+ authMode: credits
28
+ model: openai/gpt-4o-mini
29
29
  channelMappings: "C01234567:engineering,C89ABCDEF:support"
30
30
  ```
31
31
 
32
+ On cloud, `authMode: credits` uses your workspace credit balance. For BYOK (bring your own key), set `authMode: byok` and add `OPENAI_API_KEY` under workspace settings — no API key field in plugin config.
33
+
32
34
  ### Channel routing (webhook ingress)
33
35
 
34
36
  When a Slack message arrives via the Events API webhook, the plugin delegates to the OpenBot **system** agent in a target channel. Set `channelMappings` as comma-separated `slackChannelId:openbotChannelId` pairs:
@@ -0,0 +1,10 @@
1
+ /** True when this runtime is a platform-managed cloud deployment. */
2
+ export const isCloudMode = () => process.env.OPENBOT_CLOUD_MODE === "1";
3
+ /** Default auth mode: Credits on cloud, BYOK locally. */
4
+ export const defaultAuthMode = () => isCloudMode() ? "credits" : "byok";
5
+ export function resolveAuthMode(config) {
6
+ if (config.authMode === "byok" || config.authMode === "credits") {
7
+ return config.authMode;
8
+ }
9
+ return defaultAuthMode();
10
+ }
package/dist/config.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { resolveAuthMode } from "./cloud-mode.js";
2
+ import { shouldUseCreditsAuth } from "./credits-auth.js";
1
3
  export const DEFAULT_OPENBOT_CHANNEL_ID = "uncategorized";
2
4
  /** Parse `C0123:engineering,C0456:support` into a lookup index. */
3
5
  export function buildSlackChannelMappingIndex(config) {
@@ -30,13 +32,58 @@ export function resolveOpenBotChannelId(slackChannelId, config) {
30
32
  const mapped = buildSlackChannelMappingIndex(config).get(normalized);
31
33
  return mapped ?? DEFAULT_OPENBOT_CHANNEL_ID;
32
34
  }
33
- function varValue(variables, key) {
34
- const variable = variables[key];
35
- return typeof variable === "string" ? variable : variable?.value;
35
+ function variableValue(variables, key) {
36
+ const entry = variables[key];
37
+ if (typeof entry === "string")
38
+ return entry || undefined;
39
+ return entry?.value || undefined;
40
+ }
41
+ export function readSlackConfig(config) {
42
+ return {
43
+ signingSecret: typeof config.signingSecret === "string" && config.signingSecret.trim()
44
+ ? config.signingSecret.trim()
45
+ : undefined,
46
+ botToken: typeof config.botToken === "string" && config.botToken.trim()
47
+ ? config.botToken.trim()
48
+ : undefined,
49
+ teamId: typeof config.teamId === "string" && config.teamId.trim()
50
+ ? config.teamId.trim()
51
+ : undefined,
52
+ channelMappings: typeof config.channelMappings === "string" &&
53
+ config.channelMappings.trim()
54
+ ? config.channelMappings.trim()
55
+ : undefined,
56
+ authMode: resolveAuthMode(config),
57
+ model: typeof config.model === "string" && config.model.trim()
58
+ ? config.model.trim()
59
+ : undefined,
60
+ };
61
+ }
62
+ export function formatMissingCredentials(missing, authMode) {
63
+ const lines = [
64
+ "Slack agent setup is incomplete. Configure the following in plugin config, workspace settings, or environment variables:",
65
+ ];
66
+ if (missing.includes("botToken")) {
67
+ lines.push("- `botToken` / `SLACK_BOT_TOKEN` — Slack bot OAuth token (`xoxb-…`)");
68
+ }
69
+ if (missing.includes("teamId")) {
70
+ lines.push("- `teamId` / `SLACK_TEAM_ID` — workspace team id (`T…`)");
71
+ }
72
+ if (missing.includes("openaiApiKey")) {
73
+ if (authMode === "credits") {
74
+ lines.push("- OpenAI API key is required in BYOK mode — add `OPENAI_API_KEY` under workspace settings, or switch `authMode` to `credits` on cloud");
75
+ }
76
+ else {
77
+ lines.push("- `OPENAI_API_KEY` — OpenAI API key for the agent loop (BYOK mode), or switch `authMode` to `credits` on cloud");
78
+ }
79
+ }
80
+ return lines.join("\n");
36
81
  }
37
82
  export async function resolveSlackCredentials(config, storage, options) {
38
- const requireOpenAi = options?.requireOpenAi ?? true;
39
- const variables = await storage.getVariables();
83
+ const authMode = config.authMode ?? resolveAuthMode({});
84
+ const useCredits = shouldUseCreditsAuth({ authMode });
85
+ const requireOpenAi = options?.requireOpenAi ?? !useCredits;
86
+ const variables = (await storage.getVariables().catch(() => ({})));
40
87
  const resolve = (configKey, envKey) => {
41
88
  const fromConfig = config[configKey];
42
89
  if (typeof fromConfig === "string" && fromConfig.trim()) {
@@ -44,16 +91,17 @@ export async function resolveSlackCredentials(config, storage, options) {
44
91
  }
45
92
  if (process.env[envKey]?.trim())
46
93
  return process.env[envKey].trim();
47
- return varValue(variables, envKey)?.trim();
94
+ return variableValue(variables, envKey)?.trim();
48
95
  };
49
96
  const signingSecret = (typeof config.signingSecret === "string" && config.signingSecret.trim()) ||
50
97
  process.env.SLACK_SIGNING_SECRET?.trim() ||
51
- varValue(variables, "SLACK_SIGNING_SECRET") ||
98
+ variableValue(variables, "SLACK_SIGNING_SECRET") ||
52
99
  "";
53
100
  const botToken = resolve("botToken", "SLACK_BOT_TOKEN");
54
101
  const teamId = resolve("teamId", "SLACK_TEAM_ID");
55
- const openaiApiKey = resolve("openaiApiKey", "OPENAI_API_KEY");
56
- const model = resolve("model", "OPENAI_MODEL") ?? "gpt-4o-mini";
102
+ const openaiApiKey = process.env.OPENAI_API_KEY?.trim() ||
103
+ variableValue(variables, "OPENAI_API_KEY");
104
+ const model = resolve("model", "OPENAI_MODEL") ?? "openai/gpt-4o-mini";
57
105
  const missing = [];
58
106
  if (!botToken)
59
107
  missing.push("botToken");
@@ -70,7 +118,8 @@ export async function resolveSlackCredentials(config, storage, options) {
70
118
  signingSecret,
71
119
  botToken: botToken,
72
120
  teamId: teamId,
73
- openaiApiKey: openaiApiKey ?? "",
121
+ authMode,
122
+ openaiApiKey: openaiApiKey || undefined,
74
123
  model,
75
124
  },
76
125
  };
@@ -0,0 +1,53 @@
1
+ import { isCloudMode } from "./cloud-mode.js";
2
+ export const INTEGRATIONS_TOKEN_HEADER = "x-openbot-integrations-token";
3
+ export const CREDITS_API_KEY_PLACEHOLDER = "openbot-credits";
4
+ /** Cloud host injects these when routing LLM calls through OpenBot Credits. */
5
+ export function resolveCreditsAuthConfig() {
6
+ const baseUrl = process.env.OPENBOT_INTEGRATIONS_BASE_URL?.trim();
7
+ const token = process.env.OPENBOT_INTEGRATIONS_TOKEN?.trim();
8
+ if (!baseUrl || !token)
9
+ return undefined;
10
+ return { baseUrl: baseUrl.replace(/\/$/, ""), token };
11
+ }
12
+ export function creditsProviderBaseUrl(config) {
13
+ return `${config.baseUrl}/openai/v1`;
14
+ }
15
+ export function shouldUseCreditsAuth(options) {
16
+ if (options?.authMode === "byok")
17
+ return false;
18
+ if (options?.authMode === "credits")
19
+ return true;
20
+ return isCloudMode() && resolveCreditsAuthConfig() !== undefined;
21
+ }
22
+ export function isCreditsErrorMessage(message) {
23
+ const lower = message.toLowerCase();
24
+ return (lower.includes("insufficient_credits") ||
25
+ lower.includes("insufficient credits") ||
26
+ lower.includes("402"));
27
+ }
28
+ export function isAuthErrorMessage(message) {
29
+ const lower = message.toLowerCase();
30
+ return (lower.includes("api key") ||
31
+ lower.includes("401") ||
32
+ lower.includes("unauthorized") ||
33
+ lower.includes("authentication"));
34
+ }
35
+ export function isIntegrationsProviderError(message) {
36
+ const lower = message.toLowerCase();
37
+ return (lower.includes("provider api key not configured") ||
38
+ (lower.includes("503") && lower.includes("provider")));
39
+ }
40
+ export const CREDITS_NOT_CONFIGURED_MESSAGE = "OpenBot Credits is not configured on this runtime. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN (try redeploying the workspace).";
41
+ export const CREDITS_PROVIDER_UNAVAILABLE_MESSAGE = "OpenBot Credits could not reach OpenAI — the platform provider API key is not configured yet. Try again later or switch this agent to BYOK mode.";
42
+ export const CREDITS_AUTH_FAILED_MESSAGE = "Slack could not authenticate via OpenBot Credits. Check your workspace credit balance in settings, or switch this agent to BYOK mode.";
43
+ export function creditsErrorMessage(message) {
44
+ if (isIntegrationsProviderError(message)) {
45
+ return CREDITS_PROVIDER_UNAVAILABLE_MESSAGE;
46
+ }
47
+ if (isCreditsErrorMessage(message)) {
48
+ return "Insufficient workspace credits. Add credits in workspace settings or switch this agent to BYOK mode.";
49
+ }
50
+ if (isAuthErrorMessage(message))
51
+ return CREDITS_AUTH_FAILED_MESSAGE;
52
+ return undefined;
53
+ }
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { agentOutput, definePlugin, decodeWebhookRawBody, getWebhookHeader, shouldHandleInvoke, webhookHttpResponse, } from "@meetopenbot/plugin-sdk";
3
- import { DEFAULT_OPENBOT_CHANNEL_ID, resolveOpenBotChannelId, resolveSlackCredentials, } from "./config.js";
3
+ import { isCloudMode } from "./cloud-mode.js";
4
+ import { CREDITS_NOT_CONFIGURED_MESSAGE, creditsErrorMessage, resolveCreditsAuthConfig, } from "./credits-auth.js";
5
+ import { DEFAULT_OPENBOT_CHANNEL_ID, formatMissingCredentials, readSlackConfig, resolveOpenBotChannelId, resolveSlackCredentials, } from "./config.js";
6
+ import { resolveModelConfigField } from "./model-registry.js";
4
7
  import { postSlackMessage } from "./slack-post.js";
5
8
  import { runSlackAgent } from "./slack-agent.js";
6
9
  import { verifySlackSignature } from "./slack-verify.js";
7
10
  const ROUTING_AGENT_ID = "system";
11
+ const SLACK_USER_DISPLAY_NAME = "Slack user";
8
12
  function isSlackWebhookIngress(meta) {
9
13
  return meta?.source === "slack";
10
14
  }
@@ -51,21 +55,6 @@ function getSlackThreadMeta(meta) {
51
55
  return undefined;
52
56
  return slack;
53
57
  }
54
- function formatMissingCredentials(missing) {
55
- const lines = [
56
- "Slack agent setup is incomplete. Configure the following in plugin config or environment variables:",
57
- ];
58
- if (missing.includes("botToken")) {
59
- lines.push("- `botToken` / `SLACK_BOT_TOKEN` — Slack bot OAuth token (`xoxb-…`)");
60
- }
61
- if (missing.includes("teamId")) {
62
- lines.push("- `teamId` / `SLACK_TEAM_ID` — workspace team id (`T…`)");
63
- }
64
- if (missing.includes("openaiApiKey")) {
65
- lines.push("- `openaiApiKey` / `OPENAI_API_KEY` — OpenAI API key for the agent loop");
66
- }
67
- return lines.join("\n");
68
- }
69
58
  async function* bridgeSystemAgentRun(args) {
70
59
  const runId = `slack_${randomUUID()}`;
71
60
  const slackMeta = getSlackThreadMeta(args.meta);
@@ -91,6 +80,10 @@ async function* bridgeSystemAgentRun(args) {
91
80
  threadId: args.threadId,
92
81
  source: "slack",
93
82
  slack: slackMeta,
83
+ userName: SLACK_USER_DISPLAY_NAME,
84
+ ...(typeof args.meta?.userId === "string"
85
+ ? { userId: args.meta.userId }
86
+ : {}),
94
87
  },
95
88
  },
96
89
  onEvent: async (outEvent) => {
@@ -146,36 +139,41 @@ async function* bridgeSystemAgentRun(args) {
146
139
  }
147
140
  await runPromise;
148
141
  }
142
+ const modelField = await resolveModelConfigField();
149
143
  const slackPluginConfigSchema = {
150
144
  type: "object",
151
145
  properties: {
152
- signingSecret: {
153
- type: "string",
154
- description: "Slack app signing secret (Basic Information → App Credentials)",
155
- format: "password",
146
+ ...(isCloudMode()
147
+ ? {
148
+ authMode: {
149
+ type: "string",
150
+ description: "Outbound — Credits uses your workspace credit balance via OpenBot. BYOK uses `OPENAI_API_KEY` from workspace settings.",
151
+ enum: ["credits", "byok"],
152
+ default: "credits",
153
+ },
154
+ }
155
+ : {}),
156
+ model: {
157
+ ...modelField,
158
+ description: "Outbound — OpenAI model for direct Slack agent invocations",
156
159
  },
157
160
  botToken: {
158
161
  type: "string",
159
- description: "Slack bot token (OAuth & Permissions → Bot User OAuth Token)",
162
+ description: "Outbound — Slack bot token (OAuth & Permissions → Bot User OAuth Token)",
160
163
  format: "password",
161
164
  },
162
165
  teamId: {
163
166
  type: "string",
164
- description: "Slack workspace team id (starts with T)",
165
- },
166
- channelMappings: {
167
- type: "string",
168
- description: "Webhook ingress routing. Comma-separated slackChannelId:openbotChannelId pairs (e.g. C01234567:engineering,C89ABCDEF:support). Unmapped Slack channels use uncategorized.",
167
+ description: "Outbound — Slack workspace team id (starts with T)",
169
168
  },
170
- openaiApiKey: {
169
+ signingSecret: {
171
170
  type: "string",
172
- description: "OpenAI API key for direct Slack agent invocations",
171
+ description: "Inbound Slack app signing secret (Basic Information → App Credentials)",
173
172
  format: "password",
174
173
  },
175
- model: {
174
+ channelMappings: {
176
175
  type: "string",
177
- description: "OpenAI model id for direct Slack agent invocations (default: gpt-4o-mini)",
178
- default: "gpt-4o-mini",
176
+ description: "Inbound webhook routing. Comma-separated slackChannelId:openbotChannelId pairs (e.g. C01234567:engineering,C89ABCDEF:support). Unmapped Slack channels use uncategorized.",
179
177
  },
180
178
  },
181
179
  required: ["signingSecret"],
@@ -186,7 +184,7 @@ export default definePlugin({
186
184
  configSchema: slackPluginConfigSchema,
187
185
  factory: (pluginContext) => (builder) => {
188
186
  const { agentId, config, storage } = pluginContext;
189
- const slackConfig = config;
187
+ const slackConfig = readSlackConfig(config);
190
188
  const host = pluginContext.host;
191
189
  const publicBaseUrl = pluginContext.publicBaseUrl ?? "";
192
190
  builder.on("action:webhook", async function* (event, ctx) {
@@ -274,6 +272,8 @@ export default definePlugin({
274
272
  threadId,
275
273
  source: "slack",
276
274
  slack: slackMeta,
275
+ userName: SLACK_USER_DISPLAY_NAME,
276
+ ...(slackEvent.user ? { userId: slackEvent.user } : {}),
277
277
  },
278
278
  };
279
279
  });
@@ -301,7 +301,9 @@ export default definePlugin({
301
301
  if (!auth.ok) {
302
302
  yield {
303
303
  type: "agent:output",
304
- data: { content: formatMissingCredentials(auth.missing) },
304
+ data: {
305
+ content: formatMissingCredentials(auth.missing, slackConfig.authMode),
306
+ },
305
307
  meta: {
306
308
  agentId: ROUTING_AGENT_ID,
307
309
  threadId,
@@ -361,7 +363,17 @@ export default definePlugin({
361
363
  if (!auth.ok) {
362
364
  yield agentOutput({
363
365
  agentId,
364
- content: formatMissingCredentials(auth.missing),
366
+ content: formatMissingCredentials(auth.missing, slackConfig.authMode),
367
+ threadId,
368
+ meta: event.meta,
369
+ });
370
+ return;
371
+ }
372
+ if (auth.credentials.authMode === "credits" &&
373
+ !resolveCreditsAuthConfig()) {
374
+ yield agentOutput({
375
+ agentId,
376
+ content: CREDITS_NOT_CONFIGURED_MESSAGE,
365
377
  threadId,
366
378
  meta: event.meta,
367
379
  });
@@ -370,6 +382,7 @@ export default definePlugin({
370
382
  try {
371
383
  const reply = await runSlackAgent({
372
384
  prompt: userMessage,
385
+ authMode: auth.credentials.authMode,
373
386
  openaiApiKey: auth.credentials.openaiApiKey,
374
387
  botToken: auth.credentials.botToken,
375
388
  teamId: auth.credentials.teamId,
@@ -384,9 +397,12 @@ export default definePlugin({
384
397
  }
385
398
  catch (error) {
386
399
  const message = error instanceof Error ? error.message : String(error);
400
+ const creditsMessage = auth.credentials.authMode === "credits"
401
+ ? creditsErrorMessage(message)
402
+ : undefined;
387
403
  yield agentOutput({
388
404
  agentId,
389
- content: `Slack agent error: ${message}`,
405
+ content: creditsMessage ?? `Slack agent error: ${message}`,
390
406
  threadId,
391
407
  meta: event.meta,
392
408
  });
@@ -0,0 +1,40 @@
1
+ const REGISTRY_URL = "https://raw.githubusercontent.com/meetopenbot/openbot-registry/main/registry.json";
2
+ const OPENAI_PROVIDER = "openai";
3
+ const freeInputModelField = () => ({
4
+ type: "string",
5
+ override: true,
6
+ description: "OpenAI model in provider/model-id format (e.g. openai/gpt-4o-mini).",
7
+ default: "openai/gpt-4o-mini",
8
+ });
9
+ /** Registry-backed model field for plugin configSchema (`enum` + labeled `options`). */
10
+ export async function resolveModelConfigField() {
11
+ try {
12
+ const res = await fetch(REGISTRY_URL, {
13
+ headers: { Accept: "application/json" },
14
+ signal: AbortSignal.timeout(15_000),
15
+ });
16
+ if (!res.ok)
17
+ return freeInputModelField();
18
+ const registry = (await res.json());
19
+ const provider = registry.providers?.[OPENAI_PROVIDER];
20
+ const models = provider?.models ?? [];
21
+ if (models.length === 0)
22
+ return freeInputModelField();
23
+ const defaultModel = models.find((model) => model.id === "gpt-4o-mini")?.id ?? models[0].id;
24
+ return {
25
+ type: "string",
26
+ override: true,
27
+ description: "OpenAI model from the OpenBot registry.",
28
+ default: `${OPENAI_PROVIDER}/${defaultModel}`,
29
+ enum: models.map((model) => `${OPENAI_PROVIDER}/${model.id}`),
30
+ options: models.map((model) => ({
31
+ label: `${provider?.label ?? "OpenAI"} — ${model.label}`,
32
+ value: `${OPENAI_PROVIDER}/${model.id}`,
33
+ description: model.description,
34
+ })),
35
+ };
36
+ }
37
+ catch {
38
+ return freeInputModelField();
39
+ }
40
+ }
package/dist/model.js ADDED
@@ -0,0 +1,24 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ import { CREDITS_API_KEY_PLACEHOLDER, INTEGRATIONS_TOKEN_HEADER, creditsProviderBaseUrl, resolveCreditsAuthConfig, shouldUseCreditsAuth, } from "./credits-auth.js";
3
+ function normalizeOpenAiModelId(model) {
4
+ return model.includes("/") ? model.split("/").slice(1).join("/") : model;
5
+ }
6
+ export function resolveOpenAiModel(model, options) {
7
+ const modelId = normalizeOpenAiModelId(model);
8
+ const useCredits = shouldUseCreditsAuth(options);
9
+ if (useCredits) {
10
+ const config = resolveCreditsAuthConfig();
11
+ if (!config) {
12
+ throw new Error("OpenBot Credits is not configured. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN.");
13
+ }
14
+ const baseURL = creditsProviderBaseUrl(config);
15
+ const headers = { [INTEGRATIONS_TOKEN_HEADER]: config.token };
16
+ const apiKey = config.token || CREDITS_API_KEY_PLACEHOLDER;
17
+ return createOpenAI({ baseURL, apiKey, headers })(modelId);
18
+ }
19
+ const apiKey = options?.openaiApiKey?.trim();
20
+ if (!apiKey) {
21
+ throw new Error("OpenAI API key is required in BYOK mode. Add `OPENAI_API_KEY` under workspace settings or switch `authMode` to `credits` on cloud.");
22
+ }
23
+ return createOpenAI({ apiKey })(modelId);
24
+ }
@@ -1,11 +1,14 @@
1
- import { createOpenAI } from "@ai-sdk/openai";
2
1
  import { generateText, stepCountIs } from "ai";
2
+ import { resolveOpenAiModel } from "./model.js";
3
3
  import { createSlackMcpClient } from "./slack-mcp.js";
4
4
  const SYSTEM_PROMPT = `You are the OpenBot Slack specialist agent.
5
5
  Use Slack MCP tools to post messages, list channels, read history, and perform other workspace actions when needed.
6
6
  Summarize results clearly in plain text for the user. Be concise and friendly.`;
7
7
  export async function runSlackAgent(args) {
8
- const openai = createOpenAI({ apiKey: args.openaiApiKey });
8
+ const model = resolveOpenAiModel(args.model ?? "openai/gpt-4o-mini", {
9
+ authMode: args.authMode,
10
+ openaiApiKey: args.openaiApiKey,
11
+ });
9
12
  const mcpClient = await createSlackMcpClient({
10
13
  botToken: args.botToken,
11
14
  teamId: args.teamId,
@@ -13,7 +16,7 @@ export async function runSlackAgent(args) {
13
16
  try {
14
17
  const tools = await mcpClient.tools();
15
18
  const result = await generateText({
16
- model: openai(args.model ?? "gpt-4o-mini"),
19
+ model,
17
20
  system: SYSTEM_PROMPT,
18
21
  prompt: args.prompt,
19
22
  tools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/slack",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Slack Events API ingress, simple agent replies, and thread delivery for OpenBot",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",