@meetopenbot/slack 0.0.1 → 0.0.2

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,6 +1,9 @@
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";
@@ -51,21 +54,6 @@ function getSlackThreadMeta(meta) {
51
54
  return undefined;
52
55
  return slack;
53
56
  }
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
57
  async function* bridgeSystemAgentRun(args) {
70
58
  const runId = `slack_${randomUUID()}`;
71
59
  const slackMeta = getSlackThreadMeta(args.meta);
@@ -146,36 +134,41 @@ async function* bridgeSystemAgentRun(args) {
146
134
  }
147
135
  await runPromise;
148
136
  }
137
+ const modelField = await resolveModelConfigField();
149
138
  const slackPluginConfigSchema = {
150
139
  type: "object",
151
140
  properties: {
152
- signingSecret: {
153
- type: "string",
154
- description: "Slack app signing secret (Basic Information → App Credentials)",
155
- format: "password",
141
+ ...(isCloudMode()
142
+ ? {
143
+ authMode: {
144
+ type: "string",
145
+ description: "Outbound — Credits uses your workspace credit balance via OpenBot. BYOK uses `OPENAI_API_KEY` from workspace settings.",
146
+ enum: ["credits", "byok"],
147
+ default: "credits",
148
+ },
149
+ }
150
+ : {}),
151
+ model: {
152
+ ...modelField,
153
+ description: "Outbound — OpenAI model for direct Slack agent invocations",
156
154
  },
157
155
  botToken: {
158
156
  type: "string",
159
- description: "Slack bot token (OAuth & Permissions → Bot User OAuth Token)",
157
+ description: "Outbound — Slack bot token (OAuth & Permissions → Bot User OAuth Token)",
160
158
  format: "password",
161
159
  },
162
160
  teamId: {
163
161
  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.",
162
+ description: "Outbound — Slack workspace team id (starts with T)",
169
163
  },
170
- openaiApiKey: {
164
+ signingSecret: {
171
165
  type: "string",
172
- description: "OpenAI API key for direct Slack agent invocations",
166
+ description: "Inbound Slack app signing secret (Basic Information → App Credentials)",
173
167
  format: "password",
174
168
  },
175
- model: {
169
+ channelMappings: {
176
170
  type: "string",
177
- description: "OpenAI model id for direct Slack agent invocations (default: gpt-4o-mini)",
178
- default: "gpt-4o-mini",
171
+ description: "Inbound webhook routing. Comma-separated slackChannelId:openbotChannelId pairs (e.g. C01234567:engineering,C89ABCDEF:support). Unmapped Slack channels use uncategorized.",
179
172
  },
180
173
  },
181
174
  required: ["signingSecret"],
@@ -186,7 +179,7 @@ export default definePlugin({
186
179
  configSchema: slackPluginConfigSchema,
187
180
  factory: (pluginContext) => (builder) => {
188
181
  const { agentId, config, storage } = pluginContext;
189
- const slackConfig = config;
182
+ const slackConfig = readSlackConfig(config);
190
183
  const host = pluginContext.host;
191
184
  const publicBaseUrl = pluginContext.publicBaseUrl ?? "";
192
185
  builder.on("action:webhook", async function* (event, ctx) {
@@ -301,7 +294,9 @@ export default definePlugin({
301
294
  if (!auth.ok) {
302
295
  yield {
303
296
  type: "agent:output",
304
- data: { content: formatMissingCredentials(auth.missing) },
297
+ data: {
298
+ content: formatMissingCredentials(auth.missing, slackConfig.authMode),
299
+ },
305
300
  meta: {
306
301
  agentId: ROUTING_AGENT_ID,
307
302
  threadId,
@@ -361,7 +356,17 @@ export default definePlugin({
361
356
  if (!auth.ok) {
362
357
  yield agentOutput({
363
358
  agentId,
364
- content: formatMissingCredentials(auth.missing),
359
+ content: formatMissingCredentials(auth.missing, slackConfig.authMode),
360
+ threadId,
361
+ meta: event.meta,
362
+ });
363
+ return;
364
+ }
365
+ if (auth.credentials.authMode === "credits" &&
366
+ !resolveCreditsAuthConfig()) {
367
+ yield agentOutput({
368
+ agentId,
369
+ content: CREDITS_NOT_CONFIGURED_MESSAGE,
365
370
  threadId,
366
371
  meta: event.meta,
367
372
  });
@@ -370,6 +375,7 @@ export default definePlugin({
370
375
  try {
371
376
  const reply = await runSlackAgent({
372
377
  prompt: userMessage,
378
+ authMode: auth.credentials.authMode,
373
379
  openaiApiKey: auth.credentials.openaiApiKey,
374
380
  botToken: auth.credentials.botToken,
375
381
  teamId: auth.credentials.teamId,
@@ -384,9 +390,12 @@ export default definePlugin({
384
390
  }
385
391
  catch (error) {
386
392
  const message = error instanceof Error ? error.message : String(error);
393
+ const creditsMessage = auth.credentials.authMode === "credits"
394
+ ? creditsErrorMessage(message)
395
+ : undefined;
387
396
  yield agentOutput({
388
397
  agentId,
389
- content: `Slack agent error: ${message}`,
398
+ content: creditsMessage ?? `Slack agent error: ${message}`,
390
399
  threadId,
391
400
  meta: event.meta,
392
401
  });
@@ -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.2",
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",