@meetopenbot/linear 0.0.7 → 0.0.8

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/dist/index.js CHANGED
@@ -1,117 +1,44 @@
1
- import { agentOutput, definePlugin, shouldHandleInvoke, uiWidget, } from "@meetopenbot/plugin-sdk";
2
- import { isCloudMode } from "./cloud-mode.js";
3
- import { CREDITS_NOT_CONFIGURED_MESSAGE, creditsErrorMessage, resolveCreditsAuthConfig, } from "./credits-auth.js";
4
- import { formatMissingCredentials, readLinearConfig, resolveLinearCredentials, } from "./config.js";
5
- import { resolveModelConfigField } from "./model-registry.js";
6
- import { runLinearAgent } from "./linear-agent.js";
7
- import { loadThreadContext } from "./thread-context.js";
8
- const modelField = await resolveModelConfigField();
9
- const linearPluginConfigSchema = {
10
- type: "object",
11
- properties: {
12
- ...(isCloudMode()
13
- ? {
14
- authMode: {
15
- type: "string",
16
- description: "Credits uses your workspace credit balance via OpenBot. BYOK uses `OPENAI_API_KEY` from workspace settings.",
17
- enum: ["credits", "byok"],
18
- default: "credits",
19
- },
20
- }
21
- : {}),
22
- model: {
23
- ...modelField,
24
- description: "OpenAI model for direct Linear agent invocations",
25
- },
26
- apiKey: {
27
- type: "string",
28
- description: "Linear personal API key — create one at [Settings → Account → Security & Access](https://linear.app/settings/account/security)",
29
- format: "password",
30
- },
31
- },
32
- };
33
- export default definePlugin({
34
- name: "Linear",
35
- description: "Linear MCP-backed specialist agent for issues, projects, and comments",
36
- configSchema: linearPluginConfigSchema,
37
- factory: (pluginContext) => (builder) => {
38
- const { agentId, storage } = pluginContext;
39
- builder.on("agent:invoke", async function* (event, ctx) {
40
- if (!shouldHandleInvoke(event, agentId))
41
- return;
42
- if (event.meta?.threadId) {
43
- ctx.state.threadId = event.meta.threadId;
44
- }
45
- const threadId = event.meta?.threadId ?? ctx.state.threadId;
46
- const userMessage = (event.data.content ?? "").trim();
47
- if (!userMessage || !threadId)
48
- return;
49
- const linearConfig = readLinearConfig(pluginContext.config);
50
- const auth = await resolveLinearCredentials(linearConfig, storage);
51
- if (!auth.ok) {
52
- yield agentOutput({
53
- agentId,
54
- content: formatMissingCredentials(auth.missing, auth.authMode),
55
- threadId,
56
- meta: event.meta,
57
- });
58
- return;
59
- }
60
- if (auth.credentials.authMode === "credits" &&
61
- !resolveCreditsAuthConfig()) {
62
- yield agentOutput({
63
- agentId,
64
- content: CREDITS_NOT_CONFIGURED_MESSAGE,
65
- threadId,
66
- meta: event.meta,
67
- });
68
- return;
69
- }
70
- try {
71
- const threadContext = await loadThreadContext(storage, {
72
- channelId: ctx.state.channelId,
73
- threadId,
74
- agentId,
75
- currentMessage: userMessage,
76
- });
77
- for await (const chunk of runLinearAgent({
78
- prompt: userMessage,
79
- threadContext: threadContext || undefined,
80
- authMode: auth.credentials.authMode,
81
- openaiApiKey: auth.credentials.openaiApiKey,
82
- apiKey: auth.credentials.apiKey,
83
- model: auth.credentials.model,
84
- })) {
85
- if (chunk.kind === "widget") {
86
- yield uiWidget({
87
- agentId,
88
- widget: chunk.widget,
89
- threadId,
90
- meta: event.meta,
91
- });
92
- continue;
93
- }
94
- yield agentOutput({
95
- agentId,
96
- content: chunk.content,
97
- threadId,
98
- meta: event.meta,
99
- });
100
- }
101
- }
102
- catch (error) {
103
- const message = error instanceof Error ? error.message : String(error);
104
- const creditsMessage = auth.credentials.authMode === "credits"
105
- ? creditsErrorMessage(message)
106
- : undefined;
107
- yield agentOutput({
108
- agentId,
109
- content: (creditsMessage ?? message) ||
110
- "Linear agent failed for an unknown reason. Please try again.",
111
- threadId,
112
- meta: event.meta,
113
- });
114
- }
115
- });
1
+ import { defineMcpAgent } from '@meetopenbot/plugin-sdk';
2
+ const SYSTEM_PROMPT = `You are the OpenBot Linear specialist agent. Help users search, read, create, and update Linear issues, projects, teams, comments, and documents.
3
+
4
+ ## Ground rules
5
+ - Use Linear MCP tools for all workspace facts. Never invent issue IDs, titles, statuses, assignees, URLs, or counts.
6
+ - Authentication is already configured. Do not call auth tools.
7
+ - Use prior conversation context when provided. Treat the latest user message as the current request.
8
+ - If the request is ambiguous (missing team, assignee, or required fields), ask a short clarifying question instead of guessing.
9
+ - Omit optional tool parameters when unused. Never pass empty strings or empty arrays for optional filters or pagination.
10
+ - Confirm before destructive actions unless the user was explicit.
11
+
12
+ ## Tool selection
13
+ - Known issue identifier (e.g. ENG-123): get_issue.
14
+ - Discovery or filters (status, assignee, team, text): list_issues. For the current user's issues, use assignee "me".
15
+ - Projects: list_projects to browse, get_project for one project.
16
+ - Teams, workflow states, and labels: list_teams, get_team, list_issue_statuses, list_issue_labels.
17
+ - Current user: get_user.
18
+ - Comments: list_comments, create_comment.
19
+ - Product help: search_documentation.
20
+
21
+ ## Create and update workflow
22
+ 1. Resolve team, status, label, assignee, and project details via list_teams, list_issue_statuses, list_issue_labels, list_projects, or get_user as needed.
23
+ 2. Perform the mutation with create_issue, update_issue, create_project, or update_project.
24
+ 3. Summarize what changed, including identifiers (e.g. ENG-123), titles, and key fields.
25
+
26
+ Priority values: 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.
27
+
28
+ ## Response style
29
+ Reply in concise plain text. Lead with the outcome, then key details (identifiers, titles, status, assignee). Mention if you searched but found nothing.`;
30
+ export const plugin = await defineMcpAgent({
31
+ name: 'Linear',
32
+ description: 'Linear MCP-backed specialist agent for issues, projects, and comments',
33
+ models: { providers: ['openai'], default: 'openai/gpt-4o-mini' },
34
+ secret: { envKeys: ['LINEAR_API_KEY'] },
35
+ mcp: {
36
+ url: 'https://mcp.linear.app/mcp',
37
+ headers: (apiKey) => ({ Authorization: `Bearer ${apiKey}` }),
116
38
  },
39
+ system: SYSTEM_PROMPT,
40
+ maxSteps: 10,
41
+ history: true,
42
+ sanitizeEmptyArgs: true,
117
43
  });
44
+ export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/linear",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Linear MCP-backed specialist agent for OpenBot",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,10 +19,7 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@ai-sdk/mcp": "^2.0.14",
23
- "@ai-sdk/openai": "^4.0.15",
24
- "ai": "^7.0.29",
25
- "@meetopenbot/plugin-sdk": "^0.2.0"
22
+ "@meetopenbot/plugin-sdk": "^0.3.0"
26
23
  },
27
24
  "devDependencies": {
28
25
  "@types/node": "^20.10.1",
@@ -1,10 +0,0 @@
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 DELETED
@@ -1,73 +0,0 @@
1
- import { resolveAuthMode } from "./cloud-mode.js";
2
- import { shouldUseCreditsAuth } from "./credits-auth.js";
3
- function variableValue(variables, key) {
4
- const entry = variables[key];
5
- if (typeof entry === "string")
6
- return entry || undefined;
7
- return entry?.value || undefined;
8
- }
9
- export function readLinearConfig(config) {
10
- return {
11
- apiKey: typeof config.apiKey === "string" && config.apiKey.trim()
12
- ? config.apiKey.trim()
13
- : undefined,
14
- authMode: config.authMode === "byok" || config.authMode === "credits"
15
- ? config.authMode
16
- : undefined,
17
- model: typeof config.model === "string" && config.model.trim()
18
- ? config.model.trim()
19
- : undefined,
20
- };
21
- }
22
- export function formatMissingCredentials(missing, authMode) {
23
- const lines = [
24
- "Linear agent setup is incomplete. Configure the following in plugin config, workspace settings, or environment variables:",
25
- ];
26
- if (missing.includes("apiKey")) {
27
- lines.push("- `apiKey` / `LINEAR_API_KEY` — Linear personal API key (Settings → Account → Security & Access → Personal API keys)");
28
- }
29
- if (missing.includes("openaiApiKey")) {
30
- if (authMode === "credits") {
31
- lines.push("- OpenAI API key is required in BYOK mode — add `OPENAI_API_KEY` under workspace settings, or switch `authMode` to `credits` on cloud");
32
- }
33
- else {
34
- lines.push("- `OPENAI_API_KEY` — OpenAI API key for the agent loop (BYOK mode), or switch `authMode` to `credits` on cloud");
35
- }
36
- }
37
- return lines.join("\n");
38
- }
39
- export async function resolveLinearCredentials(config, storage) {
40
- const authMode = resolveAuthMode({ authMode: config.authMode });
41
- const useCredits = shouldUseCreditsAuth({ authMode });
42
- const variables = (await storage.getVariables().catch(() => ({})));
43
- const resolve = (configKey, envKey) => {
44
- const fromConfig = config[configKey];
45
- if (typeof fromConfig === "string" && fromConfig.trim()) {
46
- return fromConfig.trim();
47
- }
48
- if (process.env[envKey]?.trim())
49
- return process.env[envKey].trim();
50
- return variableValue(variables, envKey)?.trim();
51
- };
52
- const apiKey = resolve("apiKey", "LINEAR_API_KEY");
53
- const openaiApiKey = process.env.OPENAI_API_KEY?.trim() ||
54
- variableValue(variables, "OPENAI_API_KEY");
55
- const model = resolve("model", "OPENAI_MODEL") ?? "openai/gpt-4o-mini";
56
- const missing = [];
57
- if (!apiKey)
58
- missing.push("apiKey");
59
- if (!useCredits && !openaiApiKey)
60
- missing.push("openaiApiKey");
61
- if (missing.length > 0) {
62
- return { ok: false, missing, authMode };
63
- }
64
- return {
65
- ok: true,
66
- credentials: {
67
- apiKey: apiKey,
68
- authMode,
69
- openaiApiKey: openaiApiKey || undefined,
70
- model,
71
- },
72
- };
73
- }
@@ -1,53 +0,0 @@
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 = "Linear 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
- }
@@ -1,152 +0,0 @@
1
- import { toolTraceWidget } from "@meetopenbot/plugin-sdk";
2
- import { generateText, stepCountIs } from "ai";
3
- import { createLinearMcpClient } from "./linear-mcp.js";
4
- import { throwMcpError } from "./mcp-errors.js";
5
- import { wrapMcpTools } from "./mcp-tool-args.js";
6
- import { resolveOpenAiModel } from "./model.js";
7
- const TOOL_OUTPUT_MAX_LENGTH = 2_000;
8
- function formatToolOutput(output) {
9
- if (output === undefined || output === null)
10
- return "Done.";
11
- if (typeof output === "string")
12
- return truncate(output);
13
- try {
14
- return truncate(JSON.stringify(output, null, 2));
15
- }
16
- catch {
17
- return truncate(String(output));
18
- }
19
- }
20
- function truncate(text) {
21
- if (text.length <= TOOL_OUTPUT_MAX_LENGTH)
22
- return text;
23
- return `${text.slice(0, TOOL_OUTPUT_MAX_LENGTH)}…`;
24
- }
25
- function toolCallWidget(args) {
26
- return toolTraceWidget({
27
- widgetId: args.widgetId,
28
- groupId: "linear:tools",
29
- title: args.title,
30
- body: args.body,
31
- });
32
- }
33
- const SYSTEM_PROMPT = `You are the OpenBot Linear specialist agent. Help users search, read, create, and update Linear issues, projects, teams, comments, and documents.
34
-
35
- ## Ground rules
36
- - Use Linear MCP tools for all workspace facts. Never invent issue IDs, titles, statuses, assignees, URLs, or counts.
37
- - Authentication is already configured. Do not call auth tools.
38
- - Use prior conversation context when provided. Treat the latest user message as the current request.
39
- - If the request is ambiguous (missing team, assignee, or required fields), ask a short clarifying question instead of guessing.
40
- - Omit optional tool parameters when unused. Never pass empty strings or empty arrays for optional filters or pagination.
41
- - Confirm before destructive actions unless the user was explicit.
42
-
43
- ## Tool selection
44
- - Known issue identifier (e.g. ENG-123): get_issue.
45
- - Discovery or filters (status, assignee, team, text): list_issues. For the current user's issues, use assignee "me".
46
- - Projects: list_projects to browse, get_project for one project.
47
- - Teams, workflow states, and labels: list_teams, get_team, list_issue_statuses, list_issue_labels.
48
- - Current user: get_user.
49
- - Comments: list_comments, create_comment.
50
- - Product help: search_documentation.
51
-
52
- ## Create and update workflow
53
- 1. Resolve team, status, label, assignee, and project details via list_teams, list_issue_statuses, list_issue_labels, list_projects, or get_user as needed.
54
- 2. Perform the mutation with create_issue, update_issue, create_project, or update_project.
55
- 3. Summarize what changed, including identifiers (e.g. ENG-123), titles, and key fields.
56
-
57
- Priority values: 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.
58
-
59
- ## Response style
60
- Reply in concise plain text. Lead with the outcome, then key details (identifiers, titles, status, assignee). Mention if you searched but found nothing.`;
61
- export async function* runLinearAgent(args) {
62
- const model = resolveOpenAiModel(args.model ?? "openai/gpt-4o-mini", {
63
- authMode: args.authMode,
64
- openaiApiKey: args.openaiApiKey,
65
- });
66
- const mcpSession = await createLinearMcpClient({
67
- apiKey: args.apiKey,
68
- }).catch((error) => {
69
- throwMcpError("launch", error);
70
- });
71
- const queue = [];
72
- let wake;
73
- let agentDone = false;
74
- let agentError;
75
- const enqueue = (event) => {
76
- queue.push(event);
77
- wake?.();
78
- wake = undefined;
79
- };
80
- const waitForQueue = () => new Promise((resolve) => {
81
- wake = resolve;
82
- });
83
- const agentTask = (async () => {
84
- try {
85
- const rawTools = await mcpSession.client.tools().catch((error) => {
86
- throwMcpError("tools", error);
87
- });
88
- if (Object.keys(rawTools).length === 0) {
89
- throwMcpError("tools", new Error("Linear MCP server returned no tools"));
90
- }
91
- const tools = wrapMcpTools(rawTools);
92
- const prompt = args.threadContext
93
- ? `${args.threadContext}\n\nCurrent message:\n${args.prompt}`
94
- : args.prompt;
95
- const result = await generateText({
96
- model,
97
- system: SYSTEM_PROMPT,
98
- prompt,
99
- tools,
100
- stopWhen: stepCountIs(10),
101
- onToolExecutionStart: ({ toolCall }) => {
102
- enqueue({
103
- kind: "widget",
104
- widget: toolCallWidget({
105
- widgetId: toolCall.toolCallId,
106
- title: toolCall.toolName,
107
- }),
108
- });
109
- },
110
- onToolExecutionEnd: ({ toolCall, toolOutput }) => {
111
- enqueue({
112
- kind: "widget",
113
- widget: toolCallWidget({
114
- widgetId: toolCall.toolCallId,
115
- title: toolCall.toolName,
116
- body: formatToolOutput(toolOutput),
117
- }),
118
- });
119
- },
120
- }).catch((error) => {
121
- throwMcpError("agent", error);
122
- });
123
- enqueue({
124
- kind: "reply",
125
- content: result.text.trim() || "Done.",
126
- });
127
- }
128
- catch (error) {
129
- agentError = error;
130
- }
131
- finally {
132
- agentDone = true;
133
- wake?.();
134
- wake = undefined;
135
- }
136
- })();
137
- try {
138
- while (!agentDone || queue.length > 0) {
139
- if (queue.length === 0) {
140
- await waitForQueue();
141
- continue;
142
- }
143
- yield queue.shift();
144
- }
145
- await agentTask;
146
- if (agentError)
147
- throw agentError;
148
- }
149
- finally {
150
- await mcpSession.client.close().catch(() => undefined);
151
- }
152
- }
@@ -1,14 +0,0 @@
1
- import { createMCPClient } from "@ai-sdk/mcp";
2
- const LINEAR_MCP_URL = "https://mcp.linear.app/mcp";
3
- export async function createLinearMcpClient(args) {
4
- const client = await createMCPClient({
5
- transport: {
6
- type: "http",
7
- url: LINEAR_MCP_URL,
8
- headers: {
9
- Authorization: `Bearer ${args.apiKey}`,
10
- },
11
- },
12
- });
13
- return { client };
14
- }
@@ -1,40 +0,0 @@
1
- function summarizeMessage(message, maxLength = 240) {
2
- const trimmed = message.replace(/\s+/g, " ").trim();
3
- if (trimmed.length <= maxLength)
4
- return trimmed;
5
- return `${trimmed.slice(0, maxLength - 1)}…`;
6
- }
7
- export function formatMcpUserError(error, phase) {
8
- const message = error instanceof Error ? error.message : String(error);
9
- const lower = message.toLowerCase();
10
- if (phase === "launch" ||
11
- lower.includes("enoent") ||
12
- lower.includes("err_invalid_arg_value") ||
13
- (lower.includes("spawn") && lower.includes("npx")) ||
14
- lower.includes("command not found")) {
15
- return "Could not connect to Linear MCP at mcp.linear.app. Check your network access and API key, then try again.";
16
- }
17
- if (lower.includes("401") ||
18
- lower.includes("unauthorized") ||
19
- lower.includes("invalid api key") ||
20
- lower.includes("invalid token") ||
21
- (lower.includes("authentication") && lower.includes("linear"))) {
22
- return "Linear API key appears invalid or expired. Update your personal API key in plugin config or the LINEAR_API_KEY workspace secret.";
23
- }
24
- if ((lower.includes("connection") && lower.includes("closed")) ||
25
- lower.includes("transport closed") ||
26
- lower.includes("econnreset") ||
27
- lower.includes("econnrefused")) {
28
- return "The Linear MCP server disconnected unexpectedly. Try your request again; if it keeps failing, restart the OpenBot runtime.";
29
- }
30
- if (phase === "tools") {
31
- return `Could not connect to Linear MCP tools: ${summarizeMessage(message)}`;
32
- }
33
- if (phase === "agent") {
34
- return `Linear agent failed while talking to MCP: ${summarizeMessage(message)}`;
35
- }
36
- return `Could not start the Linear MCP server: ${summarizeMessage(message)}`;
37
- }
38
- export function throwMcpError(phase, error) {
39
- throw new Error(formatMcpUserError(error, phase));
40
- }
@@ -1,38 +0,0 @@
1
- export function sanitizeMcpToolArgs(args) {
2
- if (args === null || args === undefined)
3
- return args;
4
- if (typeof args !== "object" || Array.isArray(args))
5
- return args;
6
- const result = {};
7
- for (const [key, value] of Object.entries(args)) {
8
- if (value === "")
9
- continue;
10
- if (Array.isArray(value) && value.length === 0)
11
- continue;
12
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
13
- const nested = sanitizeMcpToolArgs(value);
14
- if (nested &&
15
- typeof nested === "object" &&
16
- !Array.isArray(nested) &&
17
- Object.keys(nested).length > 0) {
18
- result[key] = nested;
19
- }
20
- continue;
21
- }
22
- result[key] = value;
23
- }
24
- return result;
25
- }
26
- export function wrapMcpTools(tools) {
27
- const wrapped = { ...tools };
28
- for (const [name, tool] of Object.entries(tools)) {
29
- if (!tool.execute)
30
- continue;
31
- const originalExecute = tool.execute.bind(tool);
32
- wrapped[name] = {
33
- ...tool,
34
- execute: (input, options) => originalExecute(sanitizeMcpToolArgs(input), options),
35
- };
36
- }
37
- return wrapped;
38
- }
@@ -1,40 +0,0 @@
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 DELETED
@@ -1,24 +0,0 @@
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,72 +0,0 @@
1
- export const MAX_THREAD_CONTEXT_LINES = 20;
2
- export function threadEventsToContextLines(events, agentId) {
3
- const lines = [];
4
- for (const raw of events) {
5
- if (!raw || typeof raw !== "object")
6
- continue;
7
- const event = raw;
8
- if (event.meta?.parentToolCallId)
9
- continue;
10
- switch (event.type) {
11
- case "agent:invoke": {
12
- const role = event.data?.role ?? "user";
13
- const content = typeof event.data?.content === "string"
14
- ? event.data.content.trim()
15
- : "";
16
- if (role === "user" && content) {
17
- lines.push({ role: "user", content });
18
- }
19
- break;
20
- }
21
- case "agent:output": {
22
- if (event.meta?.agentId !== agentId)
23
- break;
24
- const content = typeof event.data?.content === "string"
25
- ? event.data.content.trim()
26
- : "";
27
- if (!content)
28
- break;
29
- const last = lines[lines.length - 1];
30
- if (last?.role === "assistant") {
31
- last.content += content;
32
- }
33
- else {
34
- lines.push({ role: "assistant", content });
35
- }
36
- break;
37
- }
38
- }
39
- }
40
- if (lines.length > MAX_THREAD_CONTEXT_LINES) {
41
- return lines.slice(-MAX_THREAD_CONTEXT_LINES);
42
- }
43
- return lines;
44
- }
45
- export function formatThreadContext(lines) {
46
- if (lines.length === 0)
47
- return "";
48
- const jsonl = lines.map((line) => JSON.stringify(line)).join("\n");
49
- return `Previous conversation context (most recent last):\n${jsonl}`;
50
- }
51
- export async function loadThreadContext(storage, args) {
52
- if (!args.channelId)
53
- return "";
54
- try {
55
- const events = await storage.getEvents({
56
- channelId: args.channelId,
57
- threadId: args.threadId,
58
- });
59
- let lines = threadEventsToContextLines(events, args.agentId);
60
- const currentMessage = args.currentMessage?.trim();
61
- if (currentMessage) {
62
- const last = lines[lines.length - 1];
63
- if (last?.role === "user" && last.content === currentMessage) {
64
- lines = lines.slice(0, -1);
65
- }
66
- }
67
- return formatThreadContext(lines);
68
- }
69
- catch {
70
- return "";
71
- }
72
- }