@meetopenbot/github 0.0.1 → 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 CHANGED
@@ -1,53 +1,37 @@
1
- # GitHub OpenBot Plugin
1
+ # @meetopenbot/github
2
2
 
3
- This plugin lets OpenBot interact with your GitHub account, enabling you to browse repositories, manage issues, and work with pull requests through natural language or direct tool calls.
3
+ GitHub specialist agent for OpenBot, backed by GitHub's official remote MCP.
4
4
 
5
- ## Features
5
+ ## Setup
6
6
 
7
- - **List Repositories**: View repositories for the authenticated user.
8
- - **Get Repository**: Get detailed information about a repository.
9
- - **List Issues**: See open or closed issues for a repository.
10
- - **Create Issue**: Open a new issue.
11
- - **List Pull Requests**: See pull requests for a repository.
12
- - **Get Pull Request**: Get details about a specific pull request.
13
- - **Create Pull Request**: Open a new pull request.
14
- - **Natural Language Support**: Ask the agent about your repos and it will summarize results.
7
+ ### GitHub token
15
8
 
16
- ## Configuration
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.
17
10
 
18
- To use this plugin, you need a GitHub Personal Access Token. You can create one in your [GitHub Developer Settings](https://github.com/settings/tokens) with `repo` scope.
11
+ You can also set the token in `AGENT.md`:
19
12
 
20
- ### Config Schema
13
+ ```yaml
14
+ plugins:
15
+ - id: "@meetopenbot/github"
16
+ config:
17
+ githubToken: ghp_your_token_here
18
+ ```
21
19
 
22
- - `githubToken` (Required): Your GitHub Personal Access Token.
23
- - `openaiApiKey` (Optional): OpenAI API key for natural language support (can also be set via environment).
20
+ Create a Personal Access Token at [GitHub Developer Settings](https://github.com/settings/tokens) with `repo` scope.
24
21
 
25
- ## Usage
26
-
27
- ### Natural Language
22
+ ### OpenAI
28
23
 
29
- You can ask things like:
30
-
31
- - "What repositories do I have on GitHub?"
32
- - "Show me the open issues for owner/repo"
33
- - "Create an issue in owner/repo titled 'Fix login bug'"
34
- - "List the pull requests for owner/repo"
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.
35
25
 
36
- ### Tools
26
+ Locally, the agent uses BYOK from `OPENAI_API_KEY` in the environment or workspace variables.
37
27
 
38
- The plugin provides the following tools:
28
+ ## Usage
39
29
 
40
- - `list_repos`: List repositories for the authenticated user.
41
- - `get_repo`: Get repository details.
42
- - `list_issues`: List issues for a repository.
43
- - `create_issue`: Create a new issue.
44
- - `list_pull_requests`: List pull requests for a repository.
45
- - `get_pull_request`: Get pull request details.
46
- - `create_pull_request`: Create a new pull request.
30
+ Ask things like:
47
31
 
48
- ## Installation
32
+ - "What repositories do I have on GitHub?"
33
+ - "Show me the open issues for owner/repo"
34
+ - "Create an issue in owner/repo titled Fix login bug"
35
+ - "Show the diff for PR 42 in owner/repo"
49
36
 
50
- 1. Clone this repository into your OpenBot plugins directory.
51
- 2. Run `npm install`.
52
- 3. Run `npm run build`.
53
- 4. Configure the plugin in your `AGENT.md` or via the OpenBot UI.
37
+ Pull request and commit file changes render as a **Diff** widget in the chat timeline.
package/dist/agent.js ADDED
@@ -0,0 +1,191 @@
1
+ import { createMCPClient } from '@ai-sdk/mcp';
2
+ import { toolTraceWidget } from '@meetopenbot/plugin-sdk';
3
+ import { generateText, stepCountIs } from 'ai';
4
+ import { resolveOpenAiModel } from './model.js';
5
+ import { diffWidgetFromTool, prIdentityFromInput, shouldFollowUpPrFiles, toolInputFrom, unwrapToolOutput, } from './diff.js';
6
+ export const GITHUB_MCP_URL = 'https://api.githubcopilot.com/mcp/';
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: 'github:tools',
29
+ title: args.title,
30
+ body: args.body,
31
+ });
32
+ }
33
+ function findMcpTool(tools, name) {
34
+ const direct = tools[name];
35
+ if (direct)
36
+ return direct;
37
+ const needle = name.toLowerCase();
38
+ for (const [key, tool] of Object.entries(tools)) {
39
+ if (key.toLowerCase() === needle || key.toLowerCase().includes(needle)) {
40
+ return tool;
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ async function fetchPrFilesDiffWidget(args) {
46
+ const identity = prIdentityFromInput(args.input);
47
+ if (!identity)
48
+ return null;
49
+ const tool = findMcpTool(args.tools, 'pull_request_read');
50
+ if (!tool?.execute)
51
+ return null;
52
+ try {
53
+ const output = await tool.execute({
54
+ method: 'get_files',
55
+ owner: identity.owner,
56
+ repo: identity.repo,
57
+ pullNumber: identity.pullNumber,
58
+ perPage: 100,
59
+ }, { toolCallId: args.widgetId, messages: [] });
60
+ return diffWidgetFromTool({
61
+ widgetId: args.widgetId,
62
+ toolName: args.toolName,
63
+ input: {
64
+ ...(typeof args.input === 'object' && args.input ? args.input : {}),
65
+ method: 'get_files',
66
+ ...identity,
67
+ },
68
+ output,
69
+ });
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ const SYSTEM_PROMPT = `You are a GitHub assistant. Use the GitHub MCP tools to help with repositories, issues, and pull requests.
76
+ Parse owner/repo from "owner/repo" when the user provides that format.
77
+ Be concise and helpful.
78
+
79
+ When the user asks to inspect, review, or see a pull request, commit, or code change:
80
+ - Call pull_request_read with method get for title/author/status if needed.
81
+ - Always also call pull_request_read with method get_files (preferred; includes per-file patches) or get_diff.
82
+ A Diff widget is rendered from that file/diff result — keep your text reply to a short summary, not a pasted patch.`;
83
+ export async function* runGithubAgent(args) {
84
+ const mcpClient = await createMCPClient({
85
+ transport: {
86
+ type: 'http',
87
+ url: GITHUB_MCP_URL,
88
+ headers: {
89
+ Authorization: `Bearer ${args.githubToken}`,
90
+ 'X-MCP-Toolsets': 'repos,issues,pull_requests',
91
+ },
92
+ },
93
+ });
94
+ const queue = [];
95
+ let wake;
96
+ let agentDone = false;
97
+ let agentError;
98
+ const enqueue = (event) => {
99
+ queue.push(event);
100
+ wake?.();
101
+ wake = undefined;
102
+ };
103
+ const waitForQueue = () => new Promise((resolve) => {
104
+ wake = resolve;
105
+ });
106
+ const agentTask = (async () => {
107
+ try {
108
+ const tools = await mcpClient.tools();
109
+ const model = resolveOpenAiModel(args.model ?? 'openai/gpt-4o', {
110
+ authMode: args.authMode,
111
+ openaiApiKey: args.openaiApiKey,
112
+ });
113
+ const result = await generateText({
114
+ model,
115
+ stopWhen: stepCountIs(5),
116
+ system: SYSTEM_PROMPT,
117
+ prompt: args.prompt,
118
+ tools,
119
+ onToolExecutionStart: ({ toolCall }) => {
120
+ enqueue({
121
+ kind: 'widget',
122
+ widget: toolCallWidget({
123
+ widgetId: toolCall.toolCallId,
124
+ title: toolCall.toolName,
125
+ }),
126
+ });
127
+ },
128
+ onToolExecutionEnd: async ({ toolCall, toolOutput }) => {
129
+ const input = toolInputFrom(toolCall, toolOutput);
130
+ const payload = unwrapToolOutput(toolOutput);
131
+ enqueue({
132
+ kind: 'widget',
133
+ widget: toolCallWidget({
134
+ widgetId: toolCall.toolCallId,
135
+ title: toolCall.toolName,
136
+ body: formatToolOutput(payload),
137
+ }),
138
+ });
139
+ const widgetId = `github-diff:${toolCall.toolCallId}`;
140
+ let widget = diffWidgetFromTool({
141
+ widgetId,
142
+ toolName: toolCall.toolName,
143
+ input,
144
+ output: payload,
145
+ });
146
+ if (!widget &&
147
+ shouldFollowUpPrFiles({
148
+ toolName: toolCall.toolName,
149
+ input,
150
+ hasDiffWidget: false,
151
+ })) {
152
+ widget = await fetchPrFilesDiffWidget({
153
+ tools: tools,
154
+ widgetId,
155
+ toolName: toolCall.toolName,
156
+ input,
157
+ });
158
+ }
159
+ if (widget)
160
+ enqueue({ kind: 'widget', widget });
161
+ },
162
+ });
163
+ if (result.text.trim()) {
164
+ enqueue({ kind: 'reply', content: result.text.trim() });
165
+ }
166
+ }
167
+ catch (error) {
168
+ agentError = error;
169
+ }
170
+ finally {
171
+ agentDone = true;
172
+ wake?.();
173
+ wake = undefined;
174
+ }
175
+ })();
176
+ try {
177
+ while (!agentDone || queue.length > 0) {
178
+ if (queue.length === 0) {
179
+ await waitForQueue();
180
+ continue;
181
+ }
182
+ yield queue.shift();
183
+ }
184
+ await agentTask;
185
+ if (agentError)
186
+ throw agentError;
187
+ }
188
+ finally {
189
+ await mcpClient.close().catch(() => undefined);
190
+ }
191
+ }
@@ -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
+ }