@plosson/agentio 0.2.0 → 0.2.2

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.0",
3
+ "version": "0.2.2",
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
+ }
@@ -10,7 +10,6 @@ import type { Config } from '../types/config';
10
10
  import type { StoredCredentials } from '../types/tokens';
11
11
 
12
12
  const ALGORITHM = 'aes-256-gcm';
13
- const DEFAULT_EXPORT_FILE = 'agentio.config';
14
13
 
15
14
  interface ExportedData {
16
15
  version: number;
@@ -59,6 +58,31 @@ function decrypt(data: string, key: Buffer): string {
59
58
  return decrypted.toString('utf-8');
60
59
  }
61
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
+
62
86
  export function registerConfigCommands(program: Command): void {
63
87
  const config = program
64
88
  .command('config')
@@ -66,10 +90,9 @@ export function registerConfigCommands(program: Command): void {
66
90
 
67
91
  config
68
92
  .command('export')
69
- .description('Export configuration and credentials to an encrypted file')
93
+ .description('Export configuration and credentials (as environment variables by default, or to a file)')
70
94
  .option('--key <key>', 'Encryption key (64 hex characters). If not provided, a random key will be generated')
71
- .option('--output <file>', 'Output file path', DEFAULT_EXPORT_FILE)
72
- .option('--env', 'Output as environment variables instead of writing to file')
95
+ .option('--file <path>', 'Write encrypted config to file instead of outputting AGENTIO_CONFIG')
73
96
  .action(async (options) => {
74
97
  try {
75
98
  // Validate key if provided
@@ -101,19 +124,15 @@ export function registerConfigCommands(program: Command): void {
101
124
  const key = deriveKeyFromPassword(encryptionKey);
102
125
  const encrypted = encrypt(JSON.stringify(exportData), key);
103
126
 
104
- if (options.env) {
127
+ if (options.file) {
128
+ // Write to file, output just the key
129
+ const filePath = options.file.startsWith('/') ? options.file : join(process.cwd(), options.file);
130
+ await writeFile(filePath, encrypted, { mode: 0o600 });
131
+ console.log(`AGENTIO_KEY=${encryptionKey}`);
132
+ } else {
105
133
  // Output as environment variables
106
134
  console.log(`AGENTIO_KEY=${encryptionKey}`);
107
135
  console.log(`AGENTIO_CONFIG=${encrypted}`);
108
- } else {
109
- // Write to file
110
- const outputPath = join(process.cwd(), options.output);
111
- await writeFile(outputPath, encrypted, { mode: 0o600 });
112
-
113
- console.log(`Configuration exported to: ${outputPath}`);
114
- console.log(`Encryption key: ${encryptionKey}`);
115
- console.log('');
116
- console.log('Keep this key safe! You will need it to import the configuration.');
117
136
  }
118
137
  } catch (error) {
119
138
  handleError(error);
@@ -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
+ }
@@ -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
+ }