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