@meetopenbot/github 0.1.0 → 0.1.1
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 +7 -1
- package/dist/agent.js +6 -3
- package/dist/cloud-mode.js +10 -0
- package/dist/config.js +74 -0
- package/dist/credits-auth.js +53 -0
- package/dist/index.js +76 -45
- package/dist/model.js +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ GitHub specialist agent for OpenBot, backed by GitHub's official remote MCP.
|
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### GitHub token
|
|
8
8
|
|
|
9
9
|
If no GitHub token is already available (plugin config, `GITHUB_TOKEN` env, or a workspace variable), the agent shows a form. Submitting it stores the token as a secret `GITHUB_TOKEN` workspace variable.
|
|
10
10
|
|
|
@@ -19,6 +19,12 @@ plugins:
|
|
|
19
19
|
|
|
20
20
|
Create a Personal Access Token at [GitHub Developer Settings](https://github.com/settings/tokens) with `repo` scope.
|
|
21
21
|
|
|
22
|
+
### OpenAI
|
|
23
|
+
|
|
24
|
+
On cloud, `authMode: credits` (the default) uses your workspace credit balance via OpenBot. For BYOK, set `authMode: byok` and add `OPENAI_API_KEY` under workspace settings.
|
|
25
|
+
|
|
26
|
+
Locally, the agent uses BYOK from `OPENAI_API_KEY` in the environment or workspace variables.
|
|
27
|
+
|
|
22
28
|
## Usage
|
|
23
29
|
|
|
24
30
|
Ask things like:
|
package/dist/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createMCPClient } from '@ai-sdk/mcp';
|
|
2
|
-
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
2
|
import { toolTraceWidget } from '@meetopenbot/plugin-sdk';
|
|
4
3
|
import { generateText, stepCountIs } from 'ai';
|
|
4
|
+
import { resolveOpenAiModel } from './model.js';
|
|
5
5
|
import { diffWidgetFromTool, prIdentityFromInput, shouldFollowUpPrFiles, toolInputFrom, unwrapToolOutput, } from './diff.js';
|
|
6
6
|
export const GITHUB_MCP_URL = 'https://api.githubcopilot.com/mcp/';
|
|
7
7
|
const TOOL_OUTPUT_MAX_LENGTH = 2_000;
|
|
@@ -106,9 +106,12 @@ export async function* runGithubAgent(args) {
|
|
|
106
106
|
const agentTask = (async () => {
|
|
107
107
|
try {
|
|
108
108
|
const tools = await mcpClient.tools();
|
|
109
|
-
const
|
|
109
|
+
const model = resolveOpenAiModel(args.model ?? 'openai/gpt-4o', {
|
|
110
|
+
authMode: args.authMode,
|
|
111
|
+
openaiApiKey: args.openaiApiKey,
|
|
112
|
+
});
|
|
110
113
|
const result = await generateText({
|
|
111
|
-
model
|
|
114
|
+
model,
|
|
112
115
|
stopWhen: stepCountIs(5),
|
|
113
116
|
system: SYSTEM_PROMPT,
|
|
114
117
|
prompt: args.prompt,
|
|
@@ -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
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolveAuthMode } from "./cloud-mode.js";
|
|
2
|
+
import { shouldUseCreditsAuth } from "./credits-auth.js";
|
|
3
|
+
export const GITHUB_TOKEN_VAR = "GITHUB_TOKEN";
|
|
4
|
+
function variableValue(variables, key) {
|
|
5
|
+
const entry = variables[key];
|
|
6
|
+
if (typeof entry === "string")
|
|
7
|
+
return entry || undefined;
|
|
8
|
+
return entry?.value || undefined;
|
|
9
|
+
}
|
|
10
|
+
export function readGithubConfig(config) {
|
|
11
|
+
return {
|
|
12
|
+
githubToken: typeof config.githubToken === "string" && config.githubToken.trim()
|
|
13
|
+
? config.githubToken.trim()
|
|
14
|
+
: undefined,
|
|
15
|
+
authMode: config.authMode === "byok" || config.authMode === "credits"
|
|
16
|
+
? config.authMode
|
|
17
|
+
: undefined,
|
|
18
|
+
model: typeof config.model === "string" && config.model.trim()
|
|
19
|
+
? config.model.trim()
|
|
20
|
+
: undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function formatMissingCredentials(missing, authMode) {
|
|
24
|
+
const lines = [
|
|
25
|
+
"GitHub agent setup is incomplete. Configure the following in plugin config, workspace settings, or environment variables:",
|
|
26
|
+
];
|
|
27
|
+
if (missing.includes("githubToken")) {
|
|
28
|
+
lines.push("- `githubToken` / `GITHUB_TOKEN` — GitHub Personal Access Token with repo scope");
|
|
29
|
+
}
|
|
30
|
+
if (missing.includes("openaiApiKey")) {
|
|
31
|
+
if (authMode === "credits") {
|
|
32
|
+
lines.push("- OpenAI API key is required in BYOK mode — add `OPENAI_API_KEY` under workspace settings, or switch `authMode` to `credits` on cloud");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
lines.push("- `OPENAI_API_KEY` — OpenAI API key for the agent loop (BYOK mode), or switch `authMode` to `credits` on cloud");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
export async function resolveGithubCredentials(config, storage) {
|
|
41
|
+
const authMode = resolveAuthMode({ authMode: config.authMode });
|
|
42
|
+
const useCredits = shouldUseCreditsAuth({ authMode });
|
|
43
|
+
const variables = (await storage.getVariables().catch(() => ({})));
|
|
44
|
+
const resolve = (configKey, envKey) => {
|
|
45
|
+
const fromConfig = config[configKey];
|
|
46
|
+
if (typeof fromConfig === "string" && fromConfig.trim()) {
|
|
47
|
+
return fromConfig.trim();
|
|
48
|
+
}
|
|
49
|
+
if (process.env[envKey]?.trim())
|
|
50
|
+
return process.env[envKey].trim();
|
|
51
|
+
return variableValue(variables, envKey)?.trim();
|
|
52
|
+
};
|
|
53
|
+
const githubToken = resolve("githubToken", GITHUB_TOKEN_VAR);
|
|
54
|
+
const openaiApiKey = process.env.OPENAI_API_KEY?.trim() ||
|
|
55
|
+
variableValue(variables, "OPENAI_API_KEY");
|
|
56
|
+
const model = resolve("model", "OPENAI_MODEL") ?? "openai/gpt-4o";
|
|
57
|
+
const missing = [];
|
|
58
|
+
if (!githubToken)
|
|
59
|
+
missing.push("githubToken");
|
|
60
|
+
if (!useCredits && !openaiApiKey)
|
|
61
|
+
missing.push("openaiApiKey");
|
|
62
|
+
if (missing.length > 0) {
|
|
63
|
+
return { ok: false, missing, authMode };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
credentials: {
|
|
68
|
+
githubToken: githubToken,
|
|
69
|
+
authMode,
|
|
70
|
+
openaiApiKey: openaiApiKey || undefined,
|
|
71
|
+
model,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -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 = "GitHub 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,34 +1,39 @@
|
|
|
1
1
|
import { definePlugin, shouldHandleInvoke, agentOutput, uiWidget, } from '@meetopenbot/plugin-sdk';
|
|
2
2
|
import { runGithubAgent } from './agent.js';
|
|
3
|
-
|
|
3
|
+
import { isCloudMode } from './cloud-mode.js';
|
|
4
|
+
import { CREDITS_NOT_CONFIGURED_MESSAGE, creditsErrorMessage, resolveCreditsAuthConfig, } from './credits-auth.js';
|
|
5
|
+
import { formatMissingCredentials, GITHUB_TOKEN_VAR, readGithubConfig, resolveGithubCredentials, } from './config.js';
|
|
4
6
|
const GITHUB_TOKEN_WIDGET_ID = 'github-token-form';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const githubPluginConfigSchema = {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
...(isCloudMode()
|
|
11
|
+
? {
|
|
12
|
+
authMode: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Credits uses your workspace credit balance via OpenBot. BYOK uses `OPENAI_API_KEY` from workspace settings.',
|
|
15
|
+
enum: ['credits', 'byok'],
|
|
16
|
+
default: 'credits',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
: {}),
|
|
20
|
+
githubToken: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'GitHub Personal Access Token',
|
|
23
|
+
format: 'password',
|
|
24
|
+
},
|
|
25
|
+
model: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'OpenAI model for GitHub agent invocations',
|
|
28
|
+
default: 'openai/gpt-4o',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
11
32
|
export default definePlugin({
|
|
12
33
|
name: 'GitHub',
|
|
13
34
|
description: 'Manage GitHub repositories, issues, and pull requests',
|
|
14
|
-
configSchema:
|
|
15
|
-
type: 'object',
|
|
16
|
-
properties: {
|
|
17
|
-
githubToken: {
|
|
18
|
-
type: 'string',
|
|
19
|
-
description: 'GitHub Personal Access Token',
|
|
20
|
-
format: 'password',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
},
|
|
35
|
+
configSchema: githubPluginConfigSchema,
|
|
24
36
|
factory: (context) => {
|
|
25
|
-
const getGithubToken = async () => {
|
|
26
|
-
const config = context.config;
|
|
27
|
-
const variables = await context.storage.getVariables();
|
|
28
|
-
return (config.githubToken ||
|
|
29
|
-
process.env.GITHUB_TOKEN ||
|
|
30
|
-
readVariable(variables, GITHUB_TOKEN_VAR));
|
|
31
|
-
};
|
|
32
37
|
return (builder) => {
|
|
33
38
|
builder.on('agent:invoke', async function* (event) {
|
|
34
39
|
if (!shouldHandleInvoke(event, context.agentId))
|
|
@@ -37,34 +42,55 @@ export default definePlugin({
|
|
|
37
42
|
const threadId = event.meta?.threadId;
|
|
38
43
|
if (!userMessage)
|
|
39
44
|
return;
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
const githubConfig = readGithubConfig(context.config);
|
|
46
|
+
const auth = await resolveGithubCredentials(githubConfig, context.storage);
|
|
47
|
+
if (!auth.ok) {
|
|
48
|
+
if (auth.missing.includes('githubToken')) {
|
|
49
|
+
yield uiWidget({
|
|
50
|
+
agentId: context.agentId,
|
|
51
|
+
threadId,
|
|
52
|
+
widget: {
|
|
53
|
+
kind: 'form',
|
|
54
|
+
widgetId: GITHUB_TOKEN_WIDGET_ID,
|
|
55
|
+
title: 'GitHub Access Token',
|
|
56
|
+
description: 'Enter a GitHub Personal Access Token with repo scope to continue.',
|
|
57
|
+
fields: [
|
|
58
|
+
{
|
|
59
|
+
id: 'githubToken',
|
|
60
|
+
label: 'GitHub Access Token',
|
|
61
|
+
type: 'password',
|
|
62
|
+
placeholder: 'ghp_...',
|
|
63
|
+
required: true,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
submitLabel: 'Save Token',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
yield agentOutput({
|
|
72
|
+
agentId: context.agentId,
|
|
73
|
+
content: formatMissingCredentials(auth.missing, auth.authMode),
|
|
74
|
+
threadId,
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (auth.credentials.authMode === 'credits' &&
|
|
79
|
+
!resolveCreditsAuthConfig()) {
|
|
80
|
+
yield agentOutput({
|
|
43
81
|
agentId: context.agentId,
|
|
82
|
+
content: CREDITS_NOT_CONFIGURED_MESSAGE,
|
|
44
83
|
threadId,
|
|
45
|
-
widget: {
|
|
46
|
-
kind: 'form',
|
|
47
|
-
widgetId: GITHUB_TOKEN_WIDGET_ID,
|
|
48
|
-
title: 'GitHub Access Token',
|
|
49
|
-
description: 'Enter a GitHub Personal Access Token with repo scope to continue.',
|
|
50
|
-
fields: [
|
|
51
|
-
{
|
|
52
|
-
id: 'githubToken',
|
|
53
|
-
label: 'GitHub Access Token',
|
|
54
|
-
type: 'password',
|
|
55
|
-
placeholder: 'ghp_...',
|
|
56
|
-
required: true,
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
submitLabel: 'Save Token',
|
|
60
|
-
},
|
|
61
84
|
});
|
|
62
85
|
return;
|
|
63
86
|
}
|
|
64
87
|
try {
|
|
65
88
|
for await (const chunk of runGithubAgent({
|
|
66
89
|
prompt: userMessage,
|
|
67
|
-
githubToken,
|
|
90
|
+
githubToken: auth.credentials.githubToken,
|
|
91
|
+
authMode: auth.credentials.authMode,
|
|
92
|
+
openaiApiKey: auth.credentials.openaiApiKey,
|
|
93
|
+
model: auth.credentials.model,
|
|
68
94
|
})) {
|
|
69
95
|
if (chunk.kind === 'widget') {
|
|
70
96
|
yield uiWidget({
|
|
@@ -85,9 +111,14 @@ export default definePlugin({
|
|
|
85
111
|
}
|
|
86
112
|
catch (error) {
|
|
87
113
|
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
const creditsMessage = auth.credentials.authMode === 'credits'
|
|
115
|
+
? creditsErrorMessage(message)
|
|
116
|
+
: undefined;
|
|
88
117
|
yield agentOutput({
|
|
89
118
|
agentId: context.agentId,
|
|
90
|
-
content:
|
|
119
|
+
content: (creditsMessage ?? message)
|
|
120
|
+
? `I encountered an error: ${creditsMessage ?? message}`
|
|
121
|
+
: 'GitHub agent failed for an unknown reason. Please try again.',
|
|
91
122
|
threadId,
|
|
92
123
|
});
|
|
93
124
|
}
|
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
|
+
}
|