@plosson/agentio 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  "dependencies": {
49
49
  "commander": "^14.0.2",
50
50
  "googleapis": "^169.0.0",
51
+ "libsodium-wrappers": "^0.8.1",
51
52
  "rss-parser": "^3.13.0"
52
53
  }
53
54
  }
@@ -0,0 +1,141 @@
1
+ import { createServer, type Server } from 'http';
2
+ import { URL } from 'url';
3
+ import { GITHUB_OAUTH_CONFIG } from '../config/credentials';
4
+
5
+ const GITHUB_SCOPES = ['repo'];
6
+
7
+ const PORT_RANGE_START = 3000;
8
+ const PORT_RANGE_END = 3010;
9
+
10
+ async function findAvailablePort(): Promise<number> {
11
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
12
+ try {
13
+ await new Promise<void>((resolve, reject) => {
14
+ const server = createServer();
15
+ server.listen(port, () => {
16
+ server.close(() => resolve());
17
+ });
18
+ server.on('error', reject);
19
+ });
20
+ return port;
21
+ } catch {
22
+ continue;
23
+ }
24
+ }
25
+ throw new Error(`No available port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
26
+ }
27
+
28
+ export interface GitHubOAuthResult {
29
+ accessToken: string;
30
+ }
31
+
32
+ export async function performGitHubOAuthFlow(): Promise<GitHubOAuthResult> {
33
+ const port = await findAvailablePort();
34
+ const redirectUri = `http://localhost:${port}/callback`;
35
+
36
+ const authUrl = new URL('https://github.com/login/oauth/authorize');
37
+ authUrl.searchParams.set('client_id', GITHUB_OAUTH_CONFIG.clientId);
38
+ authUrl.searchParams.set('redirect_uri', redirectUri);
39
+ authUrl.searchParams.set('scope', GITHUB_SCOPES.join(' '));
40
+ authUrl.searchParams.set('state', Math.random().toString(36).substring(7));
41
+
42
+ return new Promise((resolve, reject) => {
43
+ let server: Server;
44
+
45
+ const timeout = setTimeout(() => {
46
+ server?.close();
47
+ reject(new Error('OAuth flow timed out after 5 minutes'));
48
+ }, 5 * 60 * 1000);
49
+
50
+ server = createServer(async (req, res) => {
51
+ const url = new URL(req.url || '', `http://localhost:${port}`);
52
+
53
+ if (url.pathname !== '/callback') {
54
+ res.writeHead(404);
55
+ res.end('Not found');
56
+ return;
57
+ }
58
+
59
+ const code = url.searchParams.get('code');
60
+ const error = url.searchParams.get('error');
61
+ const errorDescription = url.searchParams.get('error_description');
62
+
63
+ if (error) {
64
+ res.writeHead(200, { 'Content-Type': 'text/html' });
65
+ res.end('<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>');
66
+ clearTimeout(timeout);
67
+ server.close();
68
+ reject(new Error(`GitHub OAuth error: ${error} - ${errorDescription || 'Unknown error'}`));
69
+ return;
70
+ }
71
+
72
+ if (!code) {
73
+ res.writeHead(400, { 'Content-Type': 'text/html' });
74
+ res.end('<html><body><h1>Missing Authorization Code</h1><p>You can close this window.</p></body></html>');
75
+ clearTimeout(timeout);
76
+ server.close();
77
+ reject(new Error('Missing authorization code in OAuth callback'));
78
+ return;
79
+ }
80
+
81
+ try {
82
+ // Exchange code for access token
83
+ const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
84
+ method: 'POST',
85
+ headers: {
86
+ Accept: 'application/json',
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ body: JSON.stringify({
90
+ client_id: GITHUB_OAUTH_CONFIG.clientId,
91
+ client_secret: GITHUB_OAUTH_CONFIG.clientSecret,
92
+ code,
93
+ redirect_uri: redirectUri,
94
+ }),
95
+ });
96
+
97
+ const tokenData = await tokenResponse.json() as {
98
+ access_token?: string;
99
+ error?: string;
100
+ error_description?: string;
101
+ };
102
+
103
+ if (tokenData.error || !tokenData.access_token) {
104
+ throw new Error(tokenData.error_description || tokenData.error || 'Failed to get access token');
105
+ }
106
+
107
+ res.writeHead(200, { 'Content-Type': 'text/html' });
108
+ res.end('<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
109
+
110
+ clearTimeout(timeout);
111
+ server.close();
112
+
113
+ resolve({
114
+ accessToken: tokenData.access_token,
115
+ });
116
+ } catch (err) {
117
+ res.writeHead(500);
118
+ res.end('Failed to exchange authorization code');
119
+ clearTimeout(timeout);
120
+ server.close();
121
+ reject(err);
122
+ }
123
+ });
124
+
125
+ server.listen(port, () => {
126
+ console.error(`\nOpening browser for GitHub authorization...`);
127
+ console.error(`If browser doesn't open, visit:\n${authUrl.toString()}\n`);
128
+
129
+ // Open browser
130
+ const open = process.platform === 'darwin' ? 'open' :
131
+ process.platform === 'win32' ? 'start' : 'xdg-open';
132
+ Bun.spawn([open, authUrl.toString()], { stdout: 'ignore', stderr: 'ignore' });
133
+ });
134
+
135
+ server.on('error', (err) => {
136
+ clearTimeout(timeout);
137
+ server?.close();
138
+ reject(err);
139
+ });
140
+ });
141
+ }
@@ -58,6 +58,31 @@ function decrypt(data: string, key: Buffer): string {
58
58
  return decrypted.toString('utf-8');
59
59
  }
60
60
 
61
+ /**
62
+ * Generate encrypted config data for CI/CD environments.
63
+ * Returns the key and encrypted config that can be used as environment variables.
64
+ */
65
+ export async function generateExportData(): Promise<{ key: string; config: string }> {
66
+ const encryptionKey = generateKey();
67
+
68
+ const configData = await loadConfig();
69
+ const credentials = await getAllCredentials();
70
+
71
+ const exportData: ExportedData = {
72
+ version: 1,
73
+ config: configData,
74
+ credentials,
75
+ };
76
+
77
+ const key = deriveKeyFromPassword(encryptionKey);
78
+ const encrypted = encrypt(JSON.stringify(exportData), key);
79
+
80
+ return {
81
+ key: encryptionKey,
82
+ config: encrypted,
83
+ };
84
+ }
85
+
61
86
  export function registerConfigCommands(program: Command): void {
62
87
  const config = program
63
88
  .command('config')
@@ -0,0 +1,169 @@
1
+ import { Command } from 'commander';
2
+ import { setCredentials, getCredentials } from '../auth/token-store';
3
+ import { setProfile, getProfile } from '../config/config-manager';
4
+ import { createProfileCommands } from '../utils/profile-commands';
5
+ import { GitHubClient } from '../services/github/client';
6
+ import { performGitHubOAuthFlow } from '../auth/github-oauth';
7
+ import { generateExportData } from './config';
8
+ import { CliError, handleError } from '../utils/errors';
9
+ import { resolveProfileName } from '../utils/stdin';
10
+ import type { GitHubCredentials } from '../types/github';
11
+
12
+ async function getGitHubClient(profileName?: string): Promise<{ client: GitHubClient; profile: string }> {
13
+ const profile = await getProfile('github', profileName);
14
+
15
+ if (!profile) {
16
+ throw new CliError(
17
+ 'PROFILE_NOT_FOUND',
18
+ profileName
19
+ ? `Profile "${profileName}" not found for github`
20
+ : 'No default profile configured for github',
21
+ 'Run: agentio github profile add'
22
+ );
23
+ }
24
+
25
+ const credentials = await getCredentials<GitHubCredentials>('github', profile);
26
+
27
+ if (!credentials) {
28
+ throw new CliError(
29
+ 'AUTH_FAILED',
30
+ `No credentials found for github profile "${profile}"`,
31
+ `Run: agentio github profile add --profile ${profile}`
32
+ );
33
+ }
34
+
35
+ return {
36
+ client: new GitHubClient(credentials),
37
+ profile,
38
+ };
39
+ }
40
+
41
+ function parseRepo(repo: string): { owner: string; name: string } {
42
+ const parts = repo.split('/');
43
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
44
+ throw new CliError(
45
+ 'INVALID_PARAMS',
46
+ `Invalid repository format: "${repo}"`,
47
+ 'Use the format: owner/repo (e.g., octocat/hello-world)'
48
+ );
49
+ }
50
+ return { owner: parts[0], name: parts[1] };
51
+ }
52
+
53
+ export function registerGitHubCommands(program: Command): void {
54
+ const github = program
55
+ .command('github')
56
+ .description('GitHub operations');
57
+
58
+ github
59
+ .command('install')
60
+ .description('Install AGENTIO_KEY and AGENTIO_CONFIG as GitHub Actions secrets')
61
+ .argument('<repo>', 'Repository in owner/repo format')
62
+ .option('--profile <name>', 'Profile name')
63
+ .action(async (repo: string, options) => {
64
+ try {
65
+ // Validate repo format
66
+ parseRepo(repo);
67
+
68
+ const { client, profile } = await getGitHubClient(options.profile);
69
+
70
+ console.error(`Using GitHub profile: ${profile}`);
71
+ console.error(`Installing secrets to: ${repo}`);
72
+
73
+ // Generate the export data
74
+ const exportData = await generateExportData();
75
+
76
+ // Set secrets on the repo
77
+ console.error('\nSetting AGENTIO_KEY...');
78
+ await client.setRepoSecret(repo, 'AGENTIO_KEY', exportData.key);
79
+
80
+ console.error('Setting AGENTIO_CONFIG...');
81
+ await client.setRepoSecret(repo, 'AGENTIO_CONFIG', exportData.config);
82
+
83
+ console.log(`\nInstalled AGENTIO_KEY and AGENTIO_CONFIG to ${repo}`);
84
+ console.log('\nIn your GitHub Actions workflow, use:');
85
+ console.log(' env:');
86
+ console.log(' AGENTIO_KEY: ${{ secrets.AGENTIO_KEY }}');
87
+ console.log(' AGENTIO_CONFIG: ${{ secrets.AGENTIO_CONFIG }}');
88
+ } catch (error) {
89
+ handleError(error);
90
+ }
91
+ });
92
+
93
+ github
94
+ .command('uninstall')
95
+ .description('Remove AGENTIO_KEY and AGENTIO_CONFIG secrets from a repository')
96
+ .argument('<repo>', 'Repository in owner/repo format')
97
+ .option('--profile <name>', 'Profile name')
98
+ .action(async (repo: string, options) => {
99
+ try {
100
+ // Validate repo format
101
+ parseRepo(repo);
102
+
103
+ const { client, profile } = await getGitHubClient(options.profile);
104
+
105
+ console.error(`Using GitHub profile: ${profile}`);
106
+ console.error(`Removing secrets from: ${repo}`);
107
+
108
+ // Delete secrets from the repo
109
+ console.error('\nDeleting AGENTIO_KEY...');
110
+ await client.deleteRepoSecret(repo, 'AGENTIO_KEY');
111
+
112
+ console.error('Deleting AGENTIO_CONFIG...');
113
+ await client.deleteRepoSecret(repo, 'AGENTIO_CONFIG');
114
+
115
+ console.log(`\nRemoved AGENTIO_KEY and AGENTIO_CONFIG from ${repo}`);
116
+ } catch (error) {
117
+ handleError(error);
118
+ }
119
+ });
120
+
121
+ // Profile management
122
+ const profile = createProfileCommands<GitHubCredentials>(github, {
123
+ service: 'github',
124
+ displayName: 'GitHub',
125
+ getExtraInfo: (credentials) => credentials?.username ? ` (${credentials.username})` : '',
126
+ });
127
+
128
+ profile
129
+ .command('add')
130
+ .description('Add a new GitHub profile')
131
+ .option('--profile <name>', 'Profile name', 'default')
132
+ .action(async (options) => {
133
+ try {
134
+ const profileName = await resolveProfileName('github', options.profile);
135
+
136
+ console.error('\nGitHub Setup\n');
137
+ console.error('This will open your browser to authorize agentio with GitHub.');
138
+ console.error('You will need to grant access to repositories where you want to set secrets.\n');
139
+
140
+ // Perform OAuth flow
141
+ const oauthResult = await performGitHubOAuthFlow();
142
+
143
+ // Create client to fetch user info
144
+ const credentials: GitHubCredentials = {
145
+ accessToken: oauthResult.accessToken,
146
+ username: '',
147
+ email: null,
148
+ };
149
+
150
+ const client = new GitHubClient(credentials);
151
+ const user = await client.getUser();
152
+
153
+ // Update credentials with user info
154
+ credentials.username = user.login;
155
+ credentials.email = user.email;
156
+
157
+ console.error(`\nAuthenticated as: ${user.login}${user.email ? ` (${user.email})` : ''}`);
158
+
159
+ // Save credentials
160
+ await setProfile('github', profileName);
161
+ await setCredentials('github', profileName, credentials);
162
+
163
+ console.log(`\nProfile "${profileName}" configured!`);
164
+ console.log(` Install secrets: agentio github install owner/repo --profile ${profileName}`);
165
+ } catch (error) {
166
+ handleError(error);
167
+ }
168
+ });
169
+ }
@@ -275,26 +275,28 @@ Query Syntax Examples:
275
275
  });
276
276
 
277
277
  gmail
278
- .command('archive <message-id>')
279
- .description('Archive a message')
278
+ .command('archive <message-id...>')
279
+ .description('Archive one or more messages')
280
280
  .option('--profile <name>', 'Profile name')
281
- .action(async (messageId: string, options) => {
281
+ .action(async (messageIds: string[], options) => {
282
282
  try {
283
283
  const { client } = await getGmailClient(options.profile);
284
- await client.archive(messageId);
285
- printArchived(messageId);
284
+ for (const messageId of messageIds) {
285
+ await client.archive(messageId);
286
+ printArchived(messageId);
287
+ }
286
288
  } catch (error) {
287
289
  handleError(error);
288
290
  }
289
291
  });
290
292
 
291
293
  gmail
292
- .command('mark <message-id>')
293
- .description('Mark message as read or unread')
294
+ .command('mark <message-id...>')
295
+ .description('Mark one or more messages as read or unread')
294
296
  .option('--profile <name>', 'Profile name')
295
297
  .option('--read', 'Mark as read')
296
298
  .option('--unread', 'Mark as unread')
297
- .action(async (messageId: string, options) => {
299
+ .action(async (messageIds: string[], options) => {
298
300
  try {
299
301
  if (!options.read && !options.unread) {
300
302
  throw new CliError('INVALID_PARAMS', 'Specify --read or --unread');
@@ -304,8 +306,10 @@ Query Syntax Examples:
304
306
  }
305
307
 
306
308
  const { client } = await getGmailClient(options.profile);
307
- await client.mark(messageId, options.read);
308
- printMarked(messageId, options.read);
309
+ for (const messageId of messageIds) {
310
+ await client.mark(messageId, options.read);
311
+ printMarked(messageId, options.read);
312
+ }
309
313
  } catch (error) {
310
314
  handleError(error);
311
315
  }
@@ -5,6 +5,7 @@ import { createGoogleAuth } from '../auth/token-manager';
5
5
  import { refreshJiraToken } from '../auth/jira-oauth';
6
6
  import { TelegramClient } from '../services/telegram/client';
7
7
  import { GmailClient } from '../services/gmail/client';
8
+ import { GitHubClient } from '../services/github/client';
8
9
  import { JiraClient } from '../services/jira/client';
9
10
  import { GChatClient } from '../services/gchat/client';
10
11
  import { SlackClient } from '../services/slack/client';
@@ -14,6 +15,7 @@ import type { ServiceClient, ValidationResult } from '../types/service';
14
15
  import type { ServiceName } from '../types/config';
15
16
  import type { OAuthTokens } from '../types/tokens';
16
17
  import type { TelegramCredentials } from '../types/telegram';
18
+ import type { GitHubCredentials } from '../types/github';
17
19
  import type { JiraCredentials } from '../types/jira';
18
20
  import type { GChatCredentials } from '../types/gchat';
19
21
  import type { SlackCredentials } from '../types/slack';
@@ -49,22 +51,36 @@ async function createServiceClient(
49
51
  return new TelegramClient(creds.bot_token, creds.channel_id);
50
52
  }
51
53
 
54
+ case 'github': {
55
+ const creds = credentials as GitHubCredentials;
56
+ return new GitHubClient(creds);
57
+ }
58
+
52
59
  case 'jira': {
53
60
  let creds = credentials as JiraCredentials;
54
- // Refresh token if expired or about to expire
55
- const bufferTime = 5 * 60 * 1000;
56
- if (creds.expiryDate && Date.now() + bufferTime >= creds.expiryDate) {
61
+
62
+ // Helper to refresh token
63
+ const tryRefresh = async (): Promise<JiraCredentials | null> => {
57
64
  try {
58
65
  const refreshed = await refreshJiraToken(creds.refreshToken);
59
- creds = {
66
+ const newCreds = {
60
67
  ...creds,
61
68
  accessToken: refreshed.accessToken,
62
69
  refreshToken: refreshed.refreshToken,
63
70
  expiryDate: Date.now() + refreshed.expiresIn * 1000,
64
71
  };
65
- await setCredentials('jira', profileName, creds);
72
+ await setCredentials('jira', profileName, newCreds);
73
+ return newCreds;
66
74
  } catch {
67
- // Return a mock client that reports refresh failure
75
+ return null;
76
+ }
77
+ };
78
+
79
+ // Refresh token if expired or about to expire
80
+ const bufferTime = 5 * 60 * 1000;
81
+ if (creds.expiryDate && Date.now() + bufferTime >= creds.expiryDate) {
82
+ const refreshedCreds = await tryRefresh();
83
+ if (!refreshedCreds) {
68
84
  return {
69
85
  validate: async () => ({
70
86
  valid: false,
@@ -72,8 +88,32 @@ async function createServiceClient(
72
88
  }),
73
89
  };
74
90
  }
91
+ creds = refreshedCreds;
75
92
  }
76
- return new JiraClient(creds);
93
+
94
+ // Create client and return a wrapper that attempts refresh on validation failure
95
+ const client = new JiraClient(creds);
96
+ return {
97
+ validate: async () => {
98
+ const result = await client.validate();
99
+ if (result.valid) {
100
+ return result;
101
+ }
102
+
103
+ // Validation failed - try to refresh token and retry
104
+ const refreshedCreds = await tryRefresh();
105
+ if (!refreshedCreds) {
106
+ return {
107
+ valid: false,
108
+ error: 'refresh token expired, re-authenticate',
109
+ };
110
+ }
111
+
112
+ // Retry validation with refreshed credentials
113
+ const refreshedClient = new JiraClient(refreshedCreds);
114
+ return refreshedClient.validate();
115
+ },
116
+ };
77
117
  }
78
118
 
79
119
  case 'gchat': {
@@ -20,3 +20,12 @@ export const JIRA_OAUTH_CONFIG = {
20
20
  clientId: JIRA_CLIENT_ID,
21
21
  clientSecret: reveal(JIRA_CLIENT_SECRET_ENC),
22
22
  };
23
+
24
+ // GitHub OAuth credentials
25
+ const GITHUB_CLIENT_ID = 'Ov23liR1X63IRAf6eONJ';
26
+ const GITHUB_CLIENT_SECRET_ENC = 'Sztvs0AEbI5enapA40GFdqQc4RMgf8tmrMGXZ7RQIxYnuKmllPl8bZluGh5e15QfTjRe7HwZ5Bc';
27
+
28
+ export const GITHUB_OAUTH_CONFIG = {
29
+ clientId: GITHUB_CLIENT_ID,
30
+ clientSecret: reveal(GITHUB_CLIENT_SECRET_ENC),
31
+ };
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { Command } from 'commander';
3
3
  import { registerGmailCommands } from './commands/gmail';
4
4
  import { registerTelegramCommands } from './commands/telegram';
5
5
  import { registerGChatCommands } from './commands/gchat';
6
+ import { registerGitHubCommands } from './commands/github';
6
7
  import { registerJiraCommands } from './commands/jira';
7
8
  import { registerSlackCommands } from './commands/slack';
8
9
  import { registerRssCommands } from './commands/rss';
@@ -33,6 +34,7 @@ program
33
34
  registerGmailCommands(program);
34
35
  registerTelegramCommands(program);
35
36
  registerGChatCommands(program);
37
+ registerGitHubCommands(program);
36
38
  registerJiraCommands(program);
37
39
  registerSlackCommands(program);
38
40
  registerRssCommands(program);
@@ -0,0 +1,112 @@
1
+ import sodium from 'libsodium-wrappers';
2
+ import type { GitHubCredentials, GitHubUser, GitHubPublicKey } from '../../types/github';
3
+ import type { ServiceClient, ValidationResult } from '../../types/service';
4
+ import { CliError } from '../../utils/errors';
5
+
6
+ const GITHUB_API_BASE = 'https://api.github.com';
7
+
8
+ export class GitHubClient implements ServiceClient {
9
+ private accessToken: string;
10
+
11
+ constructor(credentials: GitHubCredentials) {
12
+ this.accessToken = credentials.accessToken;
13
+ }
14
+
15
+ async validate(): Promise<ValidationResult> {
16
+ try {
17
+ const user = await this.getUser();
18
+ return { valid: true, info: user.login };
19
+ } catch (error) {
20
+ return {
21
+ valid: false,
22
+ error: error instanceof Error ? error.message : 'Unknown error',
23
+ };
24
+ }
25
+ }
26
+
27
+ private async request<T>(
28
+ method: string,
29
+ path: string,
30
+ body?: Record<string, unknown>
31
+ ): Promise<T> {
32
+ const url = `${GITHUB_API_BASE}${path}`;
33
+
34
+ try {
35
+ const response = await fetch(url, {
36
+ method,
37
+ headers: {
38
+ Authorization: `Bearer ${this.accessToken}`,
39
+ Accept: 'application/vnd.github+json',
40
+ 'X-GitHub-Api-Version': '2022-11-28',
41
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
42
+ },
43
+ body: body ? JSON.stringify(body) : undefined,
44
+ });
45
+
46
+ // Handle no-content responses (204)
47
+ if (response.status === 204) {
48
+ return {} as T;
49
+ }
50
+
51
+ const data = await response.json();
52
+
53
+ if (!response.ok) {
54
+ const errorMessage = data.message || 'Unknown GitHub API error';
55
+
56
+ if (response.status === 401) {
57
+ throw new CliError('AUTH_FAILED', `GitHub authentication failed: ${errorMessage}`);
58
+ }
59
+ if (response.status === 403) {
60
+ throw new CliError('PERMISSION_DENIED', `Permission denied: ${errorMessage}`, 'You need admin access to set secrets on this repository');
61
+ }
62
+ if (response.status === 404) {
63
+ throw new CliError('NOT_FOUND', `Not found: ${errorMessage}`, 'Check that the repository exists and you have access to it');
64
+ }
65
+ if (response.status === 429) {
66
+ throw new CliError('RATE_LIMITED', `Rate limited: ${errorMessage}`);
67
+ }
68
+
69
+ throw new CliError('API_ERROR', `GitHub API error: ${errorMessage}`);
70
+ }
71
+
72
+ return data as T;
73
+ } catch (error) {
74
+ if (error instanceof CliError) throw error;
75
+
76
+ const message = error instanceof Error ? error.message : 'Unknown error';
77
+ throw new CliError('NETWORK_ERROR', `Failed to connect to GitHub: ${message}`);
78
+ }
79
+ }
80
+
81
+ async getUser(): Promise<GitHubUser> {
82
+ return this.request<GitHubUser>('GET', '/user');
83
+ }
84
+
85
+ async getRepoPublicKey(repo: string): Promise<GitHubPublicKey> {
86
+ return this.request<GitHubPublicKey>('GET', `/repos/${repo}/actions/secrets/public-key`);
87
+ }
88
+
89
+ async setRepoSecret(repo: string, secretName: string, secretValue: string): Promise<void> {
90
+ // Ensure libsodium is ready
91
+ await sodium.ready;
92
+
93
+ // Get the repo's public key
94
+ const publicKey = await this.getRepoPublicKey(repo);
95
+
96
+ // Encrypt the secret using libsodium sealed box
97
+ const keyBytes = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
98
+ const messageBytes = sodium.from_string(secretValue);
99
+ const encryptedBytes = sodium.crypto_box_seal(messageBytes, keyBytes);
100
+ const encryptedValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
101
+
102
+ // Upload the encrypted secret
103
+ await this.request('PUT', `/repos/${repo}/actions/secrets/${secretName}`, {
104
+ encrypted_value: encryptedValue,
105
+ key_id: publicKey.key_id,
106
+ });
107
+ }
108
+
109
+ async deleteRepoSecret(repo: string, secretName: string): Promise<void> {
110
+ await this.request('DELETE', `/repos/${repo}/actions/secrets/${secretName}`);
111
+ }
112
+ }
@@ -2,6 +2,7 @@ export interface Config {
2
2
  profiles: {
3
3
  gmail?: string[];
4
4
  gchat?: string[];
5
+ github?: string[];
5
6
  jira?: string[];
6
7
  slack?: string[];
7
8
  telegram?: string[];
@@ -11,6 +12,7 @@ export interface Config {
11
12
  defaults: {
12
13
  gmail?: string;
13
14
  gchat?: string;
15
+ github?: string;
14
16
  jira?: string;
15
17
  slack?: string;
16
18
  telegram?: string;
@@ -20,4 +22,4 @@ export interface Config {
20
22
  env?: Record<string, string>;
21
23
  }
22
24
 
23
- export type ServiceName = 'gmail' | 'gchat' | 'jira' | 'slack' | 'telegram' | 'discourse' | 'sql';
25
+ export type ServiceName = 'gmail' | 'gchat' | 'github' | 'jira' | 'slack' | 'telegram' | 'discourse' | 'sql';
@@ -0,0 +1,16 @@
1
+ export interface GitHubCredentials {
2
+ accessToken: string;
3
+ username: string;
4
+ email: string | null;
5
+ }
6
+
7
+ export interface GitHubUser {
8
+ login: string;
9
+ email: string | null;
10
+ name: string | null;
11
+ }
12
+
13
+ export interface GitHubPublicKey {
14
+ key_id: string;
15
+ key: string;
16
+ }