@plosson/agentio 0.1.27 ā 0.1.29
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 +207 -176
- package/package.json +1 -1
- package/src/auth/jira-oauth.ts +5 -23
- package/src/commands/config.ts +33 -12
- package/src/commands/discourse.ts +12 -51
- package/src/commands/gchat.ts +8 -49
- package/src/commands/gmail.ts +10 -51
- package/src/commands/jira.ts +12 -53
- package/src/commands/slack.ts +8 -49
- package/src/commands/status.ts +211 -0
- package/src/commands/telegram.ts +19 -60
- package/src/config/config-manager.ts +17 -1
- package/src/config/credentials.ts +2 -2
- package/src/index.ts +2 -0
- package/src/services/discourse/client.ts +16 -2
- package/src/services/gchat/client.ts +23 -1
- package/src/services/gmail/client.ts +16 -1
- package/src/services/jira/client.ts +44 -1
- package/src/services/slack/client.ts +9 -1
- package/src/services/telegram/client.ts +14 -1
- package/src/types/service.ts +21 -0
- package/src/utils/profile-commands.ts +90 -0
package/src/commands/telegram.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { setCredentials,
|
|
3
|
-
import { setProfile,
|
|
2
|
+
import { setCredentials, getCredentials } from '../auth/token-store';
|
|
3
|
+
import { setProfile, getProfile } from '../config/config-manager';
|
|
4
|
+
import { createProfileCommands } from '../utils/profile-commands';
|
|
4
5
|
import { TelegramClient } from '../services/telegram/client';
|
|
5
6
|
import { CliError, handleError } from '../utils/errors';
|
|
6
7
|
import { readStdin, prompt, resolveProfileName } from '../utils/stdin';
|
|
@@ -82,9 +83,11 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
// Profile management
|
|
85
|
-
const profile = telegram
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const profile = createProfileCommands<TelegramCredentials>(telegram, {
|
|
87
|
+
service: 'telegram',
|
|
88
|
+
displayName: 'Telegram',
|
|
89
|
+
getExtraInfo: (credentials) => credentials?.channel_name ? ` - ${credentials.channel_name}` : '',
|
|
90
|
+
});
|
|
88
91
|
|
|
89
92
|
profile
|
|
90
93
|
.command('add')
|
|
@@ -94,16 +97,16 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
94
97
|
try {
|
|
95
98
|
const profileName = await resolveProfileName('telegram', options.profile);
|
|
96
99
|
|
|
97
|
-
console.error('\
|
|
100
|
+
console.error('\nTelegram Bot Setup\n');
|
|
98
101
|
|
|
99
102
|
// Step 1: Create bot
|
|
100
103
|
console.error('Step 1: Create your bot');
|
|
101
104
|
console.error(' Open Telegram and message @BotFather');
|
|
102
|
-
console.error('
|
|
105
|
+
console.error(' -> https://t.me/BotFather\n');
|
|
103
106
|
console.error(' Send these commands:');
|
|
104
107
|
console.error(' /newbot');
|
|
105
|
-
console.error('
|
|
106
|
-
console.error('
|
|
108
|
+
console.error(' -> Enter a display name (e.g., "My Announcements Bot")');
|
|
109
|
+
console.error(' -> Enter a username ending in "bot" (e.g., "my_announce_bot")\n');
|
|
107
110
|
console.error(' BotFather will give you a token like:');
|
|
108
111
|
console.error(' 123456789:ABCdefGHIjklMNOpqrsTUVwxyz\n');
|
|
109
112
|
|
|
@@ -125,17 +128,17 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
125
128
|
throw error;
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
console.error(`\
|
|
131
|
+
console.error(`\nBot verified: @${botInfo.username}\n`);
|
|
129
132
|
|
|
130
133
|
// Step 2: Add bot to channel
|
|
131
134
|
console.error('Step 2: Add bot to your channel');
|
|
132
135
|
console.error(' 1. Open your Telegram channel');
|
|
133
|
-
console.error(' 2. Go to Channel Settings
|
|
136
|
+
console.error(' 2. Go to Channel Settings -> Administrators');
|
|
134
137
|
console.error(` 3. Add @${botInfo.username} as admin with "Post Messages" permission\n`);
|
|
135
138
|
|
|
136
139
|
console.error(' How to find your channel ID:');
|
|
137
|
-
console.error('
|
|
138
|
-
console.error('
|
|
140
|
+
console.error(' - Public channel: Use @username (e.g., @mychannel)');
|
|
141
|
+
console.error(' - Private channel: Forward any message from the channel to @userinfobot');
|
|
139
142
|
console.error(' The bot will reply with the channel ID (starts with -100)\n');
|
|
140
143
|
|
|
141
144
|
const channelId = await prompt('? Enter channel ID: ');
|
|
@@ -162,8 +165,8 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const channelName = chatInfo.title || chatInfo.username || channelId;
|
|
165
|
-
console.error(`\
|
|
166
|
-
console.error('
|
|
168
|
+
console.error(`\nChannel verified: ${channelName}`);
|
|
169
|
+
console.error('Bot can post to this channel\n');
|
|
167
170
|
|
|
168
171
|
// Step 3: Optional customization tips
|
|
169
172
|
console.error('Step 3: Customize your bot (optional)');
|
|
@@ -182,54 +185,10 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
182
185
|
await setProfile('telegram', profileName);
|
|
183
186
|
await setCredentials('telegram', profileName, credentials);
|
|
184
187
|
|
|
185
|
-
console.log(`\
|
|
188
|
+
console.log(`\nProfile "${profileName}" configured!`);
|
|
186
189
|
console.log(` Test with: agentio telegram send --profile ${profileName} "Hello world"`);
|
|
187
190
|
} catch (error) {
|
|
188
191
|
handleError(error);
|
|
189
192
|
}
|
|
190
193
|
});
|
|
191
|
-
|
|
192
|
-
profile
|
|
193
|
-
.command('list')
|
|
194
|
-
.description('List Telegram profiles')
|
|
195
|
-
.action(async () => {
|
|
196
|
-
try {
|
|
197
|
-
const result = await listProfiles('telegram');
|
|
198
|
-
const { profiles, default: defaultProfile } = result[0];
|
|
199
|
-
|
|
200
|
-
if (profiles.length === 0) {
|
|
201
|
-
console.log('No profiles configured');
|
|
202
|
-
} else {
|
|
203
|
-
for (const name of profiles) {
|
|
204
|
-
const marker = name === defaultProfile ? ' (default)' : '';
|
|
205
|
-
const credentials = await getCredentials<TelegramCredentials>('telegram', name);
|
|
206
|
-
const channelInfo = credentials?.channel_name ? ` - ${credentials.channel_name}` : '';
|
|
207
|
-
console.log(`${name}${marker}${channelInfo}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
} catch (error) {
|
|
211
|
-
handleError(error);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
profile
|
|
216
|
-
.command('remove')
|
|
217
|
-
.description('Remove a Telegram profile')
|
|
218
|
-
.requiredOption('--profile <name>', 'Profile name')
|
|
219
|
-
.action(async (options) => {
|
|
220
|
-
try {
|
|
221
|
-
const profileName = options.profile;
|
|
222
|
-
|
|
223
|
-
const removed = await removeProfile('telegram', profileName);
|
|
224
|
-
await removeCredentials('telegram', profileName);
|
|
225
|
-
|
|
226
|
-
if (removed) {
|
|
227
|
-
console.error(`Removed profile "${profileName}"`);
|
|
228
|
-
} else {
|
|
229
|
-
console.error(`Profile "${profileName}" not found`);
|
|
230
|
-
}
|
|
231
|
-
} catch (error) {
|
|
232
|
-
handleError(error);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
194
|
}
|
|
@@ -109,13 +109,29 @@ export async function removeProfile(
|
|
|
109
109
|
return true;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
export async function setDefault(
|
|
113
|
+
service: ServiceName,
|
|
114
|
+
profileName: string
|
|
115
|
+
): Promise<boolean> {
|
|
116
|
+
const config = await loadConfig();
|
|
117
|
+
|
|
118
|
+
const serviceProfiles = config.profiles[service] || [];
|
|
119
|
+
if (!serviceProfiles.includes(profileName)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
config.defaults[service] = profileName;
|
|
124
|
+
await saveConfig(config);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
export async function listProfiles(service?: ServiceName): Promise<{
|
|
113
129
|
service: ServiceName;
|
|
114
130
|
profiles: string[];
|
|
115
131
|
default?: string;
|
|
116
132
|
}[]> {
|
|
117
133
|
const config = await loadConfig();
|
|
118
|
-
const services: ServiceName[] = service ? [service] : ['gmail', 'gchat', 'jira', 'slack', 'telegram'];
|
|
134
|
+
const services: ServiceName[] = service ? [service] : ['gmail', 'gchat', 'jira', 'slack', 'telegram', 'discourse'];
|
|
119
135
|
|
|
120
136
|
return services.map((svc) => ({
|
|
121
137
|
service: svc,
|
|
@@ -13,8 +13,8 @@ export const GOOGLE_OAUTH_CONFIG = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
// JIRA/Atlassian OAuth credentials
|
|
16
|
-
const JIRA_CLIENT_ID = '
|
|
17
|
-
const JIRA_CLIENT_SECRET_ENC = '
|
|
16
|
+
const JIRA_CLIENT_ID = 'cVyhx1kQLRUef6gr50M9cTDke7ZPL4CN';
|
|
17
|
+
const JIRA_CLIENT_SECRET_ENC = 'cFN1vM5KVVVCIkv9YlE5O0rerKJUkr-CszeusEVxofAH7W0evcCidzAB_OdTygfAcq2LjbN1IXK7ZiBBl3XrBsIO7RfxSGcEfHWpSbbHWxnKPP6H2iOoQZbOfns';
|
|
18
18
|
|
|
19
19
|
export const JIRA_OAUTH_CONFIG = {
|
|
20
20
|
clientId: JIRA_CLIENT_ID,
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { registerDiscourseCommands } from './commands/discourse';
|
|
|
10
10
|
import { registerUpdateCommand } from './commands/update';
|
|
11
11
|
import { registerConfigCommands } from './commands/config';
|
|
12
12
|
import { registerClaudeCommands } from './commands/claude';
|
|
13
|
+
import { registerStatusCommand } from './commands/status';
|
|
13
14
|
|
|
14
15
|
declare const BUILD_VERSION: string | undefined;
|
|
15
16
|
|
|
@@ -38,5 +39,6 @@ registerDiscourseCommands(program);
|
|
|
38
39
|
registerUpdateCommand(program);
|
|
39
40
|
registerConfigCommands(program);
|
|
40
41
|
registerClaudeCommands(program);
|
|
42
|
+
registerStatusCommand(program);
|
|
41
43
|
|
|
42
44
|
program.parse();
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
DiscoursePost,
|
|
7
7
|
DiscourseListOptions,
|
|
8
8
|
} from '../../types/discourse';
|
|
9
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
9
10
|
import { CliError, type ErrorCode } from '../../utils/errors';
|
|
10
11
|
|
|
11
12
|
interface DiscourseApiResponse {
|
|
@@ -85,7 +86,7 @@ interface RawPost {
|
|
|
85
86
|
like_count: number;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export class DiscourseClient {
|
|
89
|
+
export class DiscourseClient implements ServiceClient {
|
|
89
90
|
private baseUrl: string;
|
|
90
91
|
private apiKey: string;
|
|
91
92
|
private username: string;
|
|
@@ -97,6 +98,18 @@ export class DiscourseClient {
|
|
|
97
98
|
this.username = credentials.username;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
async validate(): Promise<ValidationResult> {
|
|
102
|
+
try {
|
|
103
|
+
await this.getCategories();
|
|
104
|
+
return { valid: true, info: this.baseUrl };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
valid: false,
|
|
108
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
private async request<T>(
|
|
101
114
|
method: string,
|
|
102
115
|
path: string,
|
|
@@ -140,7 +153,8 @@ export class DiscourseClient {
|
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
private getErrorCode(status: number): ErrorCode {
|
|
143
|
-
if (status === 401
|
|
156
|
+
if (status === 401) return 'AUTH_FAILED';
|
|
157
|
+
if (status === 403) return 'PERMISSION_DENIED';
|
|
144
158
|
if (status === 404) return 'NOT_FOUND';
|
|
145
159
|
if (status === 429) return 'RATE_LIMITED';
|
|
146
160
|
return 'API_ERROR';
|
|
@@ -2,6 +2,7 @@ import { google } from 'googleapis';
|
|
|
2
2
|
import type { chat_v1 } from 'googleapis';
|
|
3
3
|
import { CliError } from '../../utils/errors';
|
|
4
4
|
import type { ErrorCode } from '../../utils/errors';
|
|
5
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
5
6
|
import { GOOGLE_OAUTH_CONFIG } from '../../config/credentials';
|
|
6
7
|
import type {
|
|
7
8
|
GChatCredentials,
|
|
@@ -14,13 +15,34 @@ import type {
|
|
|
14
15
|
GChatMessage,
|
|
15
16
|
} from '../../types/gchat';
|
|
16
17
|
|
|
17
|
-
export class GChatClient {
|
|
18
|
+
export class GChatClient implements ServiceClient {
|
|
18
19
|
private credentials: GChatCredentials;
|
|
19
20
|
|
|
20
21
|
constructor(credentials: GChatCredentials) {
|
|
21
22
|
this.credentials = credentials;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
async validate(): Promise<ValidationResult> {
|
|
26
|
+
if (this.credentials.type === 'webhook') {
|
|
27
|
+
// Cannot validate webhooks without sending a message
|
|
28
|
+
return { valid: true, info: 'webhook' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const oauthCreds = this.credentials as GChatOAuthCredentials;
|
|
33
|
+
const auth = this.createOAuthClient(oauthCreds);
|
|
34
|
+
const chat = google.chat({ version: 'v1', auth });
|
|
35
|
+
await chat.spaces.list({ pageSize: 1 });
|
|
36
|
+
return { valid: true, info: 'oauth' };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
39
|
+
if (message.includes('invalid_grant') || message.includes('Token has been expired or revoked')) {
|
|
40
|
+
return { valid: false, error: 'refresh token expired, re-authenticate' };
|
|
41
|
+
}
|
|
42
|
+
return { valid: false, error: message };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
async send(options: GChatSendOptions & { spaceId?: string }): Promise<GChatSendResult> {
|
|
25
47
|
if (this.credentials.type === 'webhook') {
|
|
26
48
|
return this.sendViaWebhook(options);
|
|
@@ -2,6 +2,7 @@ import { google, gmail_v1 } from 'googleapis';
|
|
|
2
2
|
import type { OAuth2Client } from 'google-auth-library';
|
|
3
3
|
import { basename } from 'path';
|
|
4
4
|
import type { GmailMessage, GmailListOptions, GmailSendOptions, GmailReplyOptions, GmailAttachment, GmailAttachmentInfo } from '../../types/gmail';
|
|
5
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
5
6
|
import { CliError } from '../../utils/errors';
|
|
6
7
|
|
|
7
8
|
// Common MIME types by extension
|
|
@@ -38,7 +39,7 @@ function getMimeType(filename: string): string {
|
|
|
38
39
|
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export class GmailClient {
|
|
42
|
+
export class GmailClient implements ServiceClient {
|
|
42
43
|
private gmail: gmail_v1.Gmail;
|
|
43
44
|
private userEmail: string | null = null;
|
|
44
45
|
|
|
@@ -46,6 +47,20 @@ export class GmailClient {
|
|
|
46
47
|
this.gmail = google.gmail({ version: 'v1', auth });
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
async validate(): Promise<ValidationResult> {
|
|
51
|
+
try {
|
|
52
|
+
const email = await this.getUserEmail();
|
|
53
|
+
return { valid: true, info: email };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
56
|
+
// Check if it's a refresh failure
|
|
57
|
+
if (message.includes('invalid_grant') || message.includes('Token has been expired or revoked')) {
|
|
58
|
+
return { valid: false, error: 'refresh token expired, re-authenticate' };
|
|
59
|
+
}
|
|
60
|
+
return { valid: false, error: message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
private async getUserEmail(): Promise<string> {
|
|
50
65
|
if (this.userEmail) return this.userEmail;
|
|
51
66
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError, type ErrorCode } from '../../utils/errors';
|
|
2
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
2
3
|
import type {
|
|
3
4
|
JiraCredentials,
|
|
4
5
|
JiraProject,
|
|
@@ -11,7 +12,7 @@ import type {
|
|
|
11
12
|
JiraTransitionResult,
|
|
12
13
|
} from '../../types/jira';
|
|
13
14
|
|
|
14
|
-
export class JiraClient {
|
|
15
|
+
export class JiraClient implements ServiceClient {
|
|
15
16
|
private credentials: JiraCredentials;
|
|
16
17
|
private baseUrl: string;
|
|
17
18
|
|
|
@@ -20,6 +21,48 @@ export class JiraClient {
|
|
|
20
21
|
this.baseUrl = `https://api.atlassian.com/ex/jira/${credentials.cloudId}/rest/api/3`;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
async validate(): Promise<ValidationResult> {
|
|
25
|
+
try {
|
|
26
|
+
// Try /myself endpoint first (requires read:me scope)
|
|
27
|
+
const url = `${this.baseUrl}/myself`;
|
|
28
|
+
const response = await fetch(url, {
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
31
|
+
Accept: 'application/json',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const user = await response.json() as { displayName?: string; emailAddress?: string };
|
|
37
|
+
const info = user.displayName || user.emailAddress || this.credentials.siteUrl;
|
|
38
|
+
return { valid: true, info };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fall back to project search if /myself fails (older tokens without read:me scope)
|
|
42
|
+
const fallbackUrl = `${this.baseUrl}/project/search?maxResults=1`;
|
|
43
|
+
const fallbackResponse = await fetch(fallbackUrl, {
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
46
|
+
Accept: 'application/json',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (fallbackResponse.ok) {
|
|
51
|
+
return { valid: true, info: this.credentials.siteUrl };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
error: `API returned ${fallbackResponse.status}`,
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
23
66
|
private async request<T>(
|
|
24
67
|
method: string,
|
|
25
68
|
path: string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from '../../utils/errors';
|
|
2
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
2
3
|
import type {
|
|
3
4
|
SlackCredentials,
|
|
4
5
|
SlackSendOptions,
|
|
@@ -6,13 +7,20 @@ import type {
|
|
|
6
7
|
SlackWebhookCredentials,
|
|
7
8
|
} from '../../types/slack';
|
|
8
9
|
|
|
9
|
-
export class SlackClient {
|
|
10
|
+
export class SlackClient implements ServiceClient {
|
|
10
11
|
private credentials: SlackCredentials;
|
|
11
12
|
|
|
12
13
|
constructor(credentials: SlackCredentials) {
|
|
13
14
|
this.credentials = credentials;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
async validate(): Promise<ValidationResult> {
|
|
18
|
+
// Webhooks cannot be validated without sending a message
|
|
19
|
+
const webhookCreds = this.credentials as SlackWebhookCredentials;
|
|
20
|
+
const info = webhookCreds.channelName ? `#${webhookCreds.channelName}` : 'webhook';
|
|
21
|
+
return { valid: true, info };
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
async send(options: SlackSendOptions): Promise<SlackSendResult> {
|
|
17
25
|
if (this.credentials.type === 'webhook') {
|
|
18
26
|
return this.sendViaWebhook(options);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TelegramBotInfo, TelegramChat, TelegramMessage, TelegramSendOptions } from '../../types/telegram';
|
|
2
|
+
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
2
3
|
import { CliError } from '../../utils/errors';
|
|
3
4
|
|
|
4
5
|
const TELEGRAM_API_BASE = 'https://api.telegram.org/bot';
|
|
@@ -10,7 +11,7 @@ interface TelegramApiResponse<T> {
|
|
|
10
11
|
error_code?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export class TelegramClient {
|
|
14
|
+
export class TelegramClient implements ServiceClient {
|
|
14
15
|
private baseUrl: string;
|
|
15
16
|
|
|
16
17
|
constructor(
|
|
@@ -20,6 +21,18 @@ export class TelegramClient {
|
|
|
20
21
|
this.baseUrl = `${TELEGRAM_API_BASE}${botToken}`;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
async validate(): Promise<ValidationResult> {
|
|
25
|
+
try {
|
|
26
|
+
const botInfo = await this.getMe();
|
|
27
|
+
return { valid: true, info: `@${botInfo.username}` };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
private async request<T>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
24
37
|
const url = `${this.baseUrl}/${method}`;
|
|
25
38
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of validating service credentials.
|
|
3
|
+
*/
|
|
4
|
+
export interface ValidationResult {
|
|
5
|
+
valid: boolean;
|
|
6
|
+
info?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Common interface for all service clients.
|
|
12
|
+
* Implementations should handle token refresh internally if needed.
|
|
13
|
+
*/
|
|
14
|
+
export interface ServiceClient {
|
|
15
|
+
/**
|
|
16
|
+
* Validate credentials by making an API call.
|
|
17
|
+
* Should refresh tokens internally if needed before validating.
|
|
18
|
+
* @returns ValidationResult with status and optional info/error
|
|
19
|
+
*/
|
|
20
|
+
validate(): Promise<ValidationResult>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { listProfiles, removeProfile, setDefault } from '../config/config-manager';
|
|
3
|
+
import { removeCredentials, getCredentials } from '../auth/token-store';
|
|
4
|
+
import { handleError, CliError } from './errors';
|
|
5
|
+
import type { ServiceName } from '../types/config';
|
|
6
|
+
|
|
7
|
+
export interface ProfileCommandsOptions<T> {
|
|
8
|
+
service: ServiceName;
|
|
9
|
+
displayName: string;
|
|
10
|
+
getExtraInfo?: (credentials: T | null) => string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createProfileCommands<T>(
|
|
14
|
+
parent: Command,
|
|
15
|
+
options: ProfileCommandsOptions<T>
|
|
16
|
+
): Command {
|
|
17
|
+
const { service, displayName, getExtraInfo } = options;
|
|
18
|
+
|
|
19
|
+
const profile = parent
|
|
20
|
+
.command('profile')
|
|
21
|
+
.description(`Manage ${displayName} profiles`);
|
|
22
|
+
|
|
23
|
+
profile
|
|
24
|
+
.command('list')
|
|
25
|
+
.description(`List ${displayName} profiles`)
|
|
26
|
+
.action(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await listProfiles(service);
|
|
29
|
+
const { profiles, default: defaultProfile } = result[0];
|
|
30
|
+
|
|
31
|
+
if (profiles.length === 0) {
|
|
32
|
+
console.log('No profiles configured');
|
|
33
|
+
} else {
|
|
34
|
+
for (const name of profiles) {
|
|
35
|
+
const marker = name === defaultProfile ? ' (default)' : '';
|
|
36
|
+
const credentials = await getCredentials<T>(service, name);
|
|
37
|
+
const extraInfo = getExtraInfo ? getExtraInfo(credentials) : '';
|
|
38
|
+
console.log(`${name}${marker}${extraInfo}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
handleError(error);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
profile
|
|
47
|
+
.command('remove')
|
|
48
|
+
.description(`Remove a ${displayName} profile`)
|
|
49
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
try {
|
|
52
|
+
const profileName = opts.profile;
|
|
53
|
+
|
|
54
|
+
const removed = await removeProfile(service, profileName);
|
|
55
|
+
await removeCredentials(service, profileName);
|
|
56
|
+
|
|
57
|
+
if (removed) {
|
|
58
|
+
console.log(`Removed profile "${profileName}"`);
|
|
59
|
+
} else {
|
|
60
|
+
console.error(`Profile "${profileName}" not found`);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
handleError(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
profile
|
|
68
|
+
.command('default')
|
|
69
|
+
.description(`Set the default ${displayName} profile`)
|
|
70
|
+
.argument('<name>', 'Profile name to set as default')
|
|
71
|
+
.action(async (name) => {
|
|
72
|
+
try {
|
|
73
|
+
const success = await setDefault(service, name);
|
|
74
|
+
|
|
75
|
+
if (success) {
|
|
76
|
+
console.log(`Default profile set to "${name}"`);
|
|
77
|
+
} else {
|
|
78
|
+
throw new CliError(
|
|
79
|
+
'PROFILE_NOT_FOUND',
|
|
80
|
+
`Profile "${name}" not found`,
|
|
81
|
+
`Run: agentio ${service} profile list`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
handleError(error);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return profile;
|
|
90
|
+
}
|