@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/dist/index.js CHANGED
@@ -1,110 +1,40 @@
1
1
  import { definePlugin, shouldHandleInvoke, agentOutput, uiWidget, } from '@meetopenbot/plugin-sdk';
2
- import { Octokit } from '@octokit/rest';
3
- import { generateText, stepCountIs, tool } from 'ai';
4
- import { createOpenAI } from '@ai-sdk/openai';
5
- import { z } from 'zod';
6
- const listReposSchema = z.object({
7
- limit: z.number().optional().describe('Maximum number of repositories to return'),
8
- visibility: z.enum(['all', 'public', 'private']).optional().describe('Filter by repository visibility'),
9
- sort: z.enum(['created', 'updated', 'pushed', 'full_name']).optional().describe('How to sort the repositories'),
10
- });
11
- const getRepoSchema = z.object({
12
- owner: z.string().describe('The owner of the repository (user or organization)'),
13
- repo: z.string().describe('The name of the repository'),
14
- });
15
- const createRepoSchema = z.object({
16
- name: z.string().describe('The name of the repository'),
17
- org: z.string().optional().describe('The organization to create the repository in. If not provided, it will be created for the authenticated user.'),
18
- description: z.string().optional().describe('A short description of the repository'),
19
- private: z.boolean().optional().describe('Whether the repository is private'),
20
- autoInit: z.boolean().optional().describe('Whether to create an initial commit with an empty README'),
21
- });
22
- export default definePlugin({
23
- id: 'github',
24
- name: 'GitHub',
25
- description: 'Manage your GitHub repositories',
26
- configSchema: {
27
- type: 'object',
28
- properties: {
29
- githubToken: {
30
- type: 'string',
31
- description: 'GitHub Personal Access Token',
32
- format: 'password',
33
- },
34
- openaiApiKey: {
35
- type: 'string',
36
- description: 'OpenAI API Key (optional if provided via environment)',
37
- format: 'password',
38
- },
39
- },
40
- required: ['githubToken'],
41
- },
42
- toolDefinitions: {
43
- list_repos: {
44
- description: 'List repositories for the authenticated user',
45
- inputSchema: {
46
- type: 'object',
47
- properties: {
48
- limit: { type: 'number', description: 'Maximum number of repositories to return' },
49
- visibility: { type: 'string', enum: ['all', 'public', 'private'], description: 'Filter by repository visibility' },
50
- sort: { type: 'string', enum: ['created', 'updated', 'pushed', 'full_name'], description: 'How to sort the repositories' },
2
+ import { runGithubAgent } from './agent.js';
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';
6
+ const GITHUB_TOKEN_WIDGET_ID = 'github-token-form';
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',
51
17
  },
52
- },
18
+ }
19
+ : {}),
20
+ githubToken: {
21
+ type: 'string',
22
+ description: 'GitHub Personal Access Token',
23
+ format: 'password',
53
24
  },
54
- get_repo: {
55
- description: 'Get details of a specific repository',
56
- inputSchema: {
57
- type: 'object',
58
- properties: {
59
- owner: { type: 'string', description: 'The owner of the repository' },
60
- repo: { type: 'string', description: 'The name of the repository' },
61
- },
62
- required: ['owner', 'repo'],
63
- },
64
- },
65
- create_repo: {
66
- description: 'Create a new repository',
67
- inputSchema: {
68
- type: 'object',
69
- properties: {
70
- name: { type: 'string', description: 'The name of the repository' },
71
- org: { type: 'string', description: 'The organization to create the repository in' },
72
- description: { type: 'string', description: 'A short description of the repository' },
73
- private: { type: 'boolean', description: 'Whether the repository is private' },
74
- autoInit: { type: 'boolean', description: 'Whether to create an initial commit with an empty README' },
75
- },
76
- required: ['name'],
77
- },
25
+ model: {
26
+ type: 'string',
27
+ description: 'OpenAI model for GitHub agent invocations',
28
+ default: 'openai/gpt-4o',
78
29
  },
79
30
  },
31
+ };
32
+ export default definePlugin({
33
+ name: 'GitHub',
34
+ description: 'Manage GitHub repositories, issues, and pull requests',
35
+ configSchema: githubPluginConfigSchema,
80
36
  factory: (context) => {
81
- const getCredentials = async () => {
82
- const config = context.config;
83
- const env = process.env;
84
- const variables = await context.storage.getVariables();
85
- const getVal = (key, envKey) => {
86
- if (config[key])
87
- return config[key];
88
- if (env[envKey])
89
- return env[envKey];
90
- const v = variables[envKey];
91
- return typeof v === 'string' ? v : v?.value;
92
- };
93
- return {
94
- githubToken: config.githubToken,
95
- openaiApiKey: getVal('openaiApiKey', 'OPENAI_API_KEY'),
96
- };
97
- };
98
- const getOctokit = (githubToken) => {
99
- return new Octokit({
100
- auth: githubToken,
101
- headers: {
102
- 'X-GitHub-Api-Version': '2022-11-28',
103
- },
104
- });
105
- };
106
37
  return (builder) => {
107
- // Handle agent:invoke for natural language queries
108
38
  builder.on('agent:invoke', async function* (event) {
109
39
  if (!shouldHandleInvoke(event, context.agentId))
110
40
  return;
@@ -112,272 +42,103 @@ export default definePlugin({
112
42
  const threadId = event.meta?.threadId;
113
43
  if (!userMessage)
114
44
  return;
115
- const { githubToken, openaiApiKey } = await getCredentials();
116
- if (!openaiApiKey) {
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
+ }
117
71
  yield agentOutput({
118
72
  agentId: context.agentId,
119
- content: 'I need an OpenAI API key to help you manage GitHub. Please provide it below:',
73
+ content: formatMissingCredentials(auth.missing, auth.authMode),
120
74
  threadId,
121
75
  });
122
- yield uiWidget({
76
+ return;
77
+ }
78
+ if (auth.credentials.authMode === 'credits' &&
79
+ !resolveCreditsAuthConfig()) {
80
+ yield agentOutput({
123
81
  agentId: context.agentId,
82
+ content: CREDITS_NOT_CONFIGURED_MESSAGE,
124
83
  threadId,
125
- widget: {
126
- kind: 'form',
127
- widgetId: 'github-config-form',
128
- title: 'OpenAI Configuration',
129
- description: 'Enter your OpenAI API key to get started.',
130
- fields: [
131
- {
132
- id: 'openaiApiKey',
133
- label: 'OpenAI API Key',
134
- type: 'text',
135
- placeholder: 'sk-...',
136
- required: true,
137
- },
138
- ],
139
- submitLabel: 'Save Configuration',
140
- },
141
84
  });
142
85
  return;
143
86
  }
144
- const octokit = getOctokit(githubToken);
145
- const openai = createOpenAI({ apiKey: openaiApiKey });
146
87
  try {
147
- const { text, steps } = await generateText({
148
- model: openai('gpt-4o'),
149
- stopWhen: stepCountIs(5),
150
- system: `You are a GitHub management assistant. Help users manage their repositories.
151
- - Use list_repos to see the authenticated user's repositories.
152
- - Use get_repo to get details about a repository.
153
- - Use create_repo to create a new repository.
154
- When a tool requires an owner and repo, parse them from the "owner/repo" format the user provides.
155
- If a tool returns an error, explain it to the user or try an alternative approach if appropriate.
156
- Be concise and helpful.`,
88
+ for await (const chunk of runGithubAgent({
157
89
  prompt: userMessage,
158
- tools: {
159
- list_repos: tool({
160
- description: 'List repositories for the authenticated user',
161
- inputSchema: listReposSchema,
162
- execute: async ({ limit, visibility, sort }) => {
163
- try {
164
- const result = await octokit.repos.listForAuthenticatedUser({
165
- per_page: limit || 20,
166
- visibility: visibility || 'all',
167
- sort: sort || 'updated',
168
- });
169
- return result.data;
170
- }
171
- catch (error) {
172
- return { error: error.message, status: error.status };
173
- }
174
- },
175
- }),
176
- get_repo: tool({
177
- description: 'Get details of a specific repository',
178
- inputSchema: getRepoSchema,
179
- execute: async ({ owner, repo }) => {
180
- try {
181
- const result = await octokit.repos.get({ owner, repo });
182
- return result.data;
183
- }
184
- catch (error) {
185
- return { error: error.message, status: error.status };
186
- }
187
- },
188
- }),
189
- create_repo: tool({
190
- description: 'Create a new repository',
191
- inputSchema: createRepoSchema,
192
- execute: async ({ name, org, description, private: isPrivate, autoInit }) => {
193
- try {
194
- const params = {
195
- name,
196
- description,
197
- private: isPrivate,
198
- auto_init: autoInit,
199
- };
200
- const result = org
201
- ? await octokit.repos.createInOrg({ ...params, org })
202
- : await octokit.repos.createForAuthenticatedUser(params);
203
- return result.data;
204
- }
205
- catch (error) {
206
- return {
207
- error: error.message,
208
- status: error.status,
209
- details: error.response?.data,
210
- };
211
- }
212
- },
213
- }),
214
- },
215
- });
216
- if (text.trim()) {
90
+ githubToken: auth.credentials.githubToken,
91
+ authMode: auth.credentials.authMode,
92
+ openaiApiKey: auth.credentials.openaiApiKey,
93
+ model: auth.credentials.model,
94
+ })) {
95
+ if (chunk.kind === 'widget') {
96
+ yield uiWidget({
97
+ agentId: context.agentId,
98
+ threadId,
99
+ widget: chunk.widget,
100
+ meta: event.meta,
101
+ });
102
+ continue;
103
+ }
217
104
  yield agentOutput({
218
105
  agentId: context.agentId,
219
- content: text,
106
+ content: chunk.content,
220
107
  threadId,
108
+ meta: event.meta,
221
109
  });
222
110
  }
223
- for (const step of steps) {
224
- for (const toolResult of step.toolResults) {
225
- if (toolResult.toolName === 'list_repos') {
226
- const repos = toolResult.output;
227
- yield uiWidget({
228
- agentId: context.agentId,
229
- threadId,
230
- widget: {
231
- kind: 'list',
232
- title: 'GitHub Repositories',
233
- items: (repos || []).map((r) => ({
234
- id: String(r.id),
235
- label: r.full_name,
236
- description: r.description || (r.private ? 'Private' : 'Public'),
237
- status: 'done',
238
- })),
239
- },
240
- });
241
- }
242
- }
243
- }
244
111
  }
245
112
  catch (error) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+ const creditsMessage = auth.credentials.authMode === 'credits'
115
+ ? creditsErrorMessage(message)
116
+ : undefined;
246
117
  yield agentOutput({
247
118
  agentId: context.agentId,
248
- content: `I encountered an error: ${error.message}`,
119
+ content: (creditsMessage ?? message)
120
+ ? `I encountered an error: ${creditsMessage ?? message}`
121
+ : 'GitHub agent failed for an unknown reason. Please try again.',
249
122
  threadId,
250
123
  });
251
124
  }
252
125
  });
253
126
  builder.on('client:ui:widget:response', async function* (event) {
254
- if (event.data?.widgetId !== 'github-config-form')
127
+ if (event.data?.widgetId !== GITHUB_TOKEN_WIDGET_ID)
255
128
  return;
256
- const { openaiApiKey } = event.data.values || {};
257
- if (openaiApiKey) {
258
- await context.storage.createVariable({
259
- key: 'OPENAI_API_KEY',
260
- value: openaiApiKey,
261
- secret: true,
262
- });
263
- yield agentOutput({
264
- agentId: context.agentId,
265
- content: "OpenAI API key saved! I'm now ready to help you with GitHub.",
266
- threadId: event.meta?.threadId,
267
- });
268
- }
269
- });
270
- // Handle tool calls
271
- builder.on('action:list_repos', async function* (event) {
272
- const { githubToken } = await getCredentials();
273
- const octokit = getOctokit(githubToken);
274
- const { limit, visibility, sort } = (event.data || {});
275
- try {
276
- const result = await octokit.repos.listForAuthenticatedUser({
277
- per_page: limit || 20,
278
- visibility: visibility || 'all',
279
- sort: sort || 'updated',
280
- });
281
- yield uiWidget({
282
- agentId: context.agentId,
283
- threadId: event.meta?.threadId,
284
- widget: {
285
- kind: 'list',
286
- title: 'GitHub Repositories',
287
- items: result.data.map((r) => ({
288
- id: String(r.id),
289
- label: r.full_name,
290
- description: r.description || (r.private ? 'Private' : 'Public'),
291
- status: 'done',
292
- })),
293
- },
294
- });
295
- yield {
296
- type: 'action:list_repos:result',
297
- data: {
298
- ...result.data,
299
- output: `Successfully listed ${result.data.length} repositories.`,
300
- },
301
- meta: event.meta,
302
- };
303
- }
304
- catch (error) {
305
- yield {
306
- type: 'action:list_repos:result',
307
- data: {
308
- error: error.message,
309
- status: error.status,
310
- details: error.response?.data,
311
- output: `Failed to list repositories: ${error.message}`,
312
- },
313
- meta: event.meta,
314
- };
315
- }
316
- });
317
- builder.on('action:get_repo', async function* (event) {
318
- const { githubToken } = await getCredentials();
319
- const octokit = getOctokit(githubToken);
320
- const { owner, repo } = (event.data || {});
321
- try {
322
- const result = await octokit.repos.get({ owner, repo });
323
- yield {
324
- type: 'action:get_repo:result',
325
- data: {
326
- ...result.data,
327
- output: `Successfully retrieved details for ${owner}/${repo}.`,
328
- },
329
- meta: event.meta,
330
- };
331
- }
332
- catch (error) {
333
- yield {
334
- type: 'action:get_repo:result',
335
- data: {
336
- error: error.message,
337
- status: error.status,
338
- details: error.response?.data,
339
- output: `Failed to get repository ${owner}/${repo}: ${error.message}`,
340
- },
341
- meta: event.meta,
342
- };
343
- }
344
- });
345
- builder.on('action:create_repo', async function* (event) {
346
- console.log('action:create_repo', event);
347
- const { githubToken } = await getCredentials();
348
- const octokit = getOctokit(githubToken);
349
- const { name, org, description, private: isPrivate, autoInit } = (event.data || {});
350
- try {
351
- const params = {
352
- name,
353
- description,
354
- private: isPrivate,
355
- auto_init: autoInit,
356
- };
357
- const result = org
358
- ? await octokit.repos.createInOrg({ ...params, org })
359
- : await octokit.repos.createForAuthenticatedUser(params);
360
- yield {
361
- type: 'action:create_repo:result',
362
- data: {
363
- ...result.data,
364
- output: `Successfully created repository ${result.data.full_name}.`,
365
- },
366
- meta: event.meta,
367
- };
368
- }
369
- catch (error) {
370
- yield {
371
- type: 'action:create_repo:result',
372
- data: {
373
- error: error.message,
374
- status: error.status,
375
- details: error.response?.data,
376
- output: `Failed to create repository ${name}: ${error.message}`,
377
- },
378
- meta: event.meta,
379
- };
380
- }
129
+ const githubToken = event.data.values?.githubToken;
130
+ if (typeof githubToken !== 'string' || !githubToken.trim())
131
+ return;
132
+ await context.storage.createVariable({
133
+ key: GITHUB_TOKEN_VAR,
134
+ value: githubToken.trim(),
135
+ secret: true,
136
+ });
137
+ yield agentOutput({
138
+ agentId: context.agentId,
139
+ content: 'GitHub access token saved. Retry your last request.',
140
+ threadId: event.meta?.threadId,
141
+ });
381
142
  });
382
143
  };
383
144
  },
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
+ }
package/package.json CHANGED
@@ -1,27 +1,39 @@
1
1
  {
2
2
  "name": "@meetopenbot/github",
3
- "version": "0.0.1",
4
- "description": "Manage your GitHub repositories, issues, and pull requests from OpenBot",
3
+ "version": "0.1.1",
4
+ "description": "Manage GitHub repositories, issues, and pull requests from OpenBot",
5
5
  "type": "module",
6
- "main": "dist/index.js",
7
- "scripts": {
8
- "build": "tsc",
9
- "start": "node dist/index.js",
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- },
6
+ "main": "./dist/index.js",
12
7
  "publishConfig": {
13
8
  "access": "public"
14
9
  },
15
10
  "dependencies": {
16
- "@ai-sdk/openai": "^3.0.64",
17
- "@meetopenbot/plugin-sdk": "^0.1.2",
18
- "@octokit/rest": "^22.0.0",
19
- "ai": "^6.0.185",
20
- "zod": "^4.4.3"
11
+ "@ai-sdk/mcp": "^2.0.14",
12
+ "@ai-sdk/openai": "^4.0.15",
13
+ "ai": "^7.0.29",
14
+ "@meetopenbot/plugin-sdk": "^0.2.0"
21
15
  },
22
16
  "devDependencies": {
23
17
  "@types/node": "^25.9.1",
24
18
  "ts-node": "^10.9.2",
25
19
  "typescript": "^6.0.3"
20
+ },
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc && node ../../scripts/write-plugin-declaration.mjs",
34
+ "start": "node dist/index.js",
35
+ "dev": "tsc --watch --preserveWatchOutput",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "node --experimental-strip-types --test src/diff.test.ts"
26
38
  }
27
39
  }