@plosson/agentio 0.3.0 → 0.3.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.
@@ -6,6 +6,7 @@ import { join } from 'path';
6
6
  import { loadConfig, saveConfig, setEnv, unsetEnv, listEnv } from '../config/config-manager';
7
7
  import { getAllCredentials, setAllCredentials } from '../auth/token-store';
8
8
  import { CliError, handleError } from '../utils/errors';
9
+ import { confirm } from '../utils/stdin';
9
10
  import type { Config } from '../types/config';
10
11
  import type { StoredCredentials } from '../types/tokens';
11
12
 
@@ -308,4 +309,32 @@ export function registerConfigCommands(program: Command): void {
308
309
  handleError(error);
309
310
  }
310
311
  });
312
+
313
+ config
314
+ .command('clear')
315
+ .description('Clear all configuration and credentials')
316
+ .option('--force', 'Skip confirmation prompt')
317
+ .action(async (options) => {
318
+ try {
319
+ if (!options.force) {
320
+ const confirmed = await confirm(
321
+ 'This will delete all profiles and credentials. Are you sure?'
322
+ );
323
+ if (!confirmed) {
324
+ console.error('Aborted');
325
+ return;
326
+ }
327
+ }
328
+
329
+ // Reset config to default (empty profiles)
330
+ await saveConfig({ profiles: {} });
331
+
332
+ // Clear all credentials
333
+ await setAllCredentials({});
334
+
335
+ console.log('Configuration cleared');
336
+ } catch (error) {
337
+ handleError(error);
338
+ }
339
+ });
311
340
  }
@@ -1,7 +1,8 @@
1
1
  import { Command } from 'commander';
2
- import { setCredentials, getCredentials } from '../auth/token-store';
3
- import { setProfile, getProfile } from '../config/config-manager';
2
+ import { setCredentials } from '../auth/token-store';
3
+ import { setProfile } from '../config/config-manager';
4
4
  import { createProfileCommands } from '../utils/profile-commands';
5
+ import { createClientGetter } from '../utils/client-factory';
5
6
  import { DiscourseClient } from '../services/discourse/client';
6
7
  import { CliError, handleError } from '../utils/errors';
7
8
  import { prompt } from '../utils/stdin';
@@ -12,34 +13,10 @@ import {
12
13
  printDiscourseCategoryList,
13
14
  } from '../utils/output';
14
15
 
15
- async function getDiscourseClient(
16
- profileName: string
17
- ): Promise<{ client: DiscourseClient; profile: string }> {
18
- const profile = await getProfile('discourse', profileName);
19
-
20
- if (!profile) {
21
- throw new CliError(
22
- 'PROFILE_NOT_FOUND',
23
- `Profile "${profileName}" not found for discourse`,
24
- 'Run: agentio discourse profile add'
25
- );
26
- }
27
-
28
- const credentials = await getCredentials<DiscourseCredentials>('discourse', profile);
29
-
30
- if (!credentials) {
31
- throw new CliError(
32
- 'AUTH_FAILED',
33
- `No credentials found for discourse profile "${profile}"`,
34
- `Run: agentio discourse profile add --profile ${profile}`
35
- );
36
- }
37
-
38
- return {
39
- client: new DiscourseClient(credentials),
40
- profile,
41
- };
42
- }
16
+ const getDiscourseClient = createClientGetter<DiscourseCredentials, DiscourseClient>({
17
+ service: 'discourse',
18
+ createClient: (credentials) => new DiscourseClient(credentials),
19
+ });
43
20
 
44
21
  export function registerDiscourseCommands(program: Command): void {
45
22
  const discourse = program.command('discourse').description('Discourse forum operations');
@@ -1,9 +1,10 @@
1
1
  import { Command } from 'commander';
2
2
  import { google } from 'googleapis';
3
3
  import { readFile } from 'fs/promises';
4
- import { setCredentials, getCredentials } from '../auth/token-store';
5
- import { setProfile, getProfile } from '../config/config-manager';
4
+ import { setCredentials } from '../auth/token-store';
5
+ import { setProfile } from '../config/config-manager';
6
6
  import { createProfileCommands } from '../utils/profile-commands';
7
+ import { createClientGetter } from '../utils/client-factory';
7
8
  import { performOAuthFlow } from '../auth/oauth';
8
9
  import { createGoogleAuth } from '../auth/token-manager';
9
10
  import { GChatClient } from '../services/gchat/client';
@@ -12,32 +13,10 @@ import { readStdin, prompt } from '../utils/stdin';
12
13
  import { printGChatSendResult, printGChatMessageList, printGChatMessage, printGChatSpaceList } from '../utils/output';
13
14
  import type { GChatCredentials, GChatWebhookCredentials, GChatOAuthCredentials } from '../types/gchat';
14
15
 
15
- async function getGChatClient(profileName: string): Promise<{ client: GChatClient; profile: string }> {
16
- const profile = await getProfile('gchat', profileName);
17
-
18
- if (!profile) {
19
- throw new CliError(
20
- 'PROFILE_NOT_FOUND',
21
- `Profile "${profileName}" not found for gchat`,
22
- 'Run: agentio gchat profile add'
23
- );
24
- }
25
-
26
- const credentials = await getCredentials<GChatCredentials>('gchat', profile);
27
-
28
- if (!credentials) {
29
- throw new CliError(
30
- 'AUTH_FAILED',
31
- `No credentials found for gchat profile "${profile}"`,
32
- `Run: agentio gchat profile add --profile ${profile}`
33
- );
34
- }
35
-
36
- return {
37
- client: new GChatClient(credentials),
38
- profile,
39
- };
40
- }
16
+ const getGChatClient = createClientGetter<GChatCredentials, GChatClient>({
17
+ service: 'gchat',
18
+ createClient: (credentials) => new GChatClient(credentials),
19
+ });
41
20
 
42
21
  export function registerGChatCommands(program: Command): void {
43
22
  const gchat = program
@@ -1,39 +1,18 @@
1
1
  import { Command } from 'commander';
2
- import { setCredentials, getCredentials } from '../auth/token-store';
3
- import { setProfile, getProfile } from '../config/config-manager';
2
+ import { setCredentials } from '../auth/token-store';
3
+ import { setProfile } from '../config/config-manager';
4
4
  import { createProfileCommands } from '../utils/profile-commands';
5
+ import { createClientGetter } from '../utils/client-factory';
5
6
  import { GitHubClient } from '../services/github/client';
6
7
  import { performGitHubOAuthFlow } from '../auth/github-oauth';
7
8
  import { generateExportData } from './config';
8
9
  import { CliError, handleError } from '../utils/errors';
9
10
  import type { GitHubCredentials } from '../types/github';
10
11
 
11
- async function getGitHubClient(profileName: string): Promise<{ client: GitHubClient; profile: string }> {
12
- const profile = await getProfile('github', profileName);
13
-
14
- if (!profile) {
15
- throw new CliError(
16
- 'PROFILE_NOT_FOUND',
17
- `Profile "${profileName}" not found for github`,
18
- 'Run: agentio github profile add'
19
- );
20
- }
21
-
22
- const credentials = await getCredentials<GitHubCredentials>('github', profile);
23
-
24
- if (!credentials) {
25
- throw new CliError(
26
- 'AUTH_FAILED',
27
- `No credentials found for github profile "${profile}"`,
28
- `Run: agentio github profile add --profile ${profile}`
29
- );
30
- }
31
-
32
- return {
33
- client: new GitHubClient(credentials),
34
- profile,
35
- };
36
- }
12
+ const getGitHubClient = createClientGetter<GitHubCredentials, GitHubClient>({
13
+ service: 'github',
14
+ createClient: (credentials) => new GitHubClient(credentials),
15
+ });
37
16
 
38
17
  function parseRepo(repo: string): { owner: string; name: string } {
39
18
  const parts = repo.split('/');
@@ -1,40 +1,19 @@
1
1
  import { Command } from 'commander';
2
2
  import { readFile } from 'fs/promises';
3
- import { setCredentials, getCredentials } from '../auth/token-store';
4
- import { setProfile, getProfile } from '../config/config-manager';
3
+ import { setCredentials } from '../auth/token-store';
4
+ import { setProfile } from '../config/config-manager';
5
5
  import { createProfileCommands } from '../utils/profile-commands';
6
+ import { createClientGetter } from '../utils/client-factory';
6
7
  import { SlackClient } from '../services/slack/client';
7
8
  import { CliError, handleError } from '../utils/errors';
8
9
  import { readStdin, prompt } from '../utils/stdin';
9
10
  import { printSlackSendResult } from '../utils/output';
10
11
  import type { SlackCredentials, SlackWebhookCredentials } from '../types/slack';
11
12
 
12
- async function getSlackClient(profileName: string): Promise<{ client: SlackClient; profile: string }> {
13
- const profile = await getProfile('slack', profileName);
14
-
15
- if (!profile) {
16
- throw new CliError(
17
- 'PROFILE_NOT_FOUND',
18
- `Profile "${profileName}" not found for slack`,
19
- 'Run: agentio slack profile add'
20
- );
21
- }
22
-
23
- const credentials = await getCredentials<SlackCredentials>('slack', profile);
24
-
25
- if (!credentials) {
26
- throw new CliError(
27
- 'AUTH_FAILED',
28
- `No credentials found for slack profile "${profile}"`,
29
- `Run: agentio slack profile add --profile ${profile}`
30
- );
31
- }
32
-
33
- return {
34
- client: new SlackClient(credentials),
35
- profile,
36
- };
37
- }
13
+ const getSlackClient = createClientGetter<SlackCredentials, SlackClient>({
14
+ service: 'slack',
15
+ createClient: (credentials) => new SlackClient(credentials),
16
+ });
38
17
 
39
18
  export function registerSlackCommands(program: Command): void {
40
19
  const slack = program
@@ -1,38 +1,17 @@
1
1
  import { Command } from 'commander';
2
- import { setCredentials, getCredentials } from '../auth/token-store';
3
- import { setProfile, getProfile } from '../config/config-manager';
2
+ import { setCredentials } from '../auth/token-store';
3
+ import { setProfile } from '../config/config-manager';
4
4
  import { createProfileCommands } from '../utils/profile-commands';
5
+ import { createClientGetter } from '../utils/client-factory';
5
6
  import { SqlClient } from '../services/sql/client';
6
7
  import { CliError, handleError } from '../utils/errors';
7
8
  import { readStdin, prompt } from '../utils/stdin';
8
9
  import type { SqlCredentials } from '../types/sql';
9
10
 
10
- async function getSqlClient(profileName: string): Promise<{ client: SqlClient; profile: string }> {
11
- const profile = await getProfile('sql', profileName);
12
-
13
- if (!profile) {
14
- throw new CliError(
15
- 'PROFILE_NOT_FOUND',
16
- `Profile "${profileName}" not found for sql`,
17
- 'Run: agentio sql profile add'
18
- );
19
- }
20
-
21
- const credentials = await getCredentials<SqlCredentials>('sql', profile);
22
-
23
- if (!credentials) {
24
- throw new CliError(
25
- 'AUTH_FAILED',
26
- `No credentials found for sql profile "${profile}"`,
27
- `Run: agentio sql profile add --profile ${profile}`
28
- );
29
- }
30
-
31
- return {
32
- client: new SqlClient(credentials),
33
- profile,
34
- };
35
- }
11
+ const getSqlClient = createClientGetter<SqlCredentials, SqlClient>({
12
+ service: 'sql',
13
+ createClient: (credentials) => new SqlClient(credentials),
14
+ });
36
15
 
37
16
  function extractDisplayName(url: string): string {
38
17
  try {
@@ -48,7 +48,7 @@ async function createServiceClient(
48
48
 
49
49
  case 'telegram': {
50
50
  const creds = credentials as TelegramCredentials;
51
- return new TelegramClient(creds.bot_token, creds.channel_id);
51
+ return new TelegramClient(creds.botToken, creds.channelId);
52
52
  }
53
53
 
54
54
  case 'github': {
@@ -1,38 +1,17 @@
1
1
  import { Command } from 'commander';
2
- import { setCredentials, getCredentials } from '../auth/token-store';
3
- import { setProfile, getProfile } from '../config/config-manager';
2
+ import { setCredentials } from '../auth/token-store';
3
+ import { setProfile } from '../config/config-manager';
4
4
  import { createProfileCommands } from '../utils/profile-commands';
5
+ import { createClientGetter } from '../utils/client-factory';
5
6
  import { TelegramClient } from '../services/telegram/client';
6
7
  import { CliError, handleError } from '../utils/errors';
7
8
  import { readStdin, prompt } from '../utils/stdin';
8
9
  import type { TelegramCredentials, TelegramSendOptions } from '../types/telegram';
9
10
 
10
- async function getTelegramClient(profileName: string): Promise<{ client: TelegramClient; profile: string }> {
11
- const profile = await getProfile('telegram', profileName);
12
-
13
- if (!profile) {
14
- throw new CliError(
15
- 'PROFILE_NOT_FOUND',
16
- `Profile "${profileName}" not found for telegram`,
17
- 'Run: agentio telegram profile add'
18
- );
19
- }
20
-
21
- const credentials = await getCredentials<TelegramCredentials>('telegram', profile);
22
-
23
- if (!credentials) {
24
- throw new CliError(
25
- 'AUTH_FAILED',
26
- `No credentials found for telegram profile "${profile}"`,
27
- `Run: agentio telegram profile add --profile ${profile}`
28
- );
29
- }
30
-
31
- return {
32
- client: new TelegramClient(credentials.bot_token, credentials.channel_id),
33
- profile,
34
- };
35
- }
11
+ const getTelegramClient = createClientGetter<TelegramCredentials, TelegramClient>({
12
+ service: 'telegram',
13
+ createClient: (credentials) => new TelegramClient(credentials.botToken, credentials.channelId),
14
+ });
36
15
 
37
16
  export function registerTelegramCommands(program: Command): void {
38
17
  const telegram = program
@@ -84,7 +63,7 @@ export function registerTelegramCommands(program: Command): void {
84
63
  const profile = createProfileCommands<TelegramCredentials>(telegram, {
85
64
  service: 'telegram',
86
65
  displayName: 'Telegram',
87
- getExtraInfo: (credentials) => credentials?.channel_name ? ` - ${credentials.channel_name}` : '',
66
+ getExtraInfo: (credentials) => credentials?.channelName ? ` - ${credentials.channelName}` : '',
88
67
  });
89
68
 
90
69
  profile
@@ -175,10 +154,10 @@ export function registerTelegramCommands(program: Command): void {
175
154
 
176
155
  // Save credentials
177
156
  const credentials: TelegramCredentials = {
178
- bot_token: botToken,
179
- channel_id: channelId,
180
- bot_username: botInfo.username,
181
- channel_name: channelName,
157
+ botToken: botToken,
158
+ channelId: channelId,
159
+ botUsername: botInfo.username,
160
+ channelName: channelName,
182
161
  };
183
162
 
184
163
  await setProfile('telegram', profileName);
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import { createInterface } from 'readline';
3
2
  import { CliError, handleError } from '../utils/errors';
3
+ import { prompt } from '../utils/stdin';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import * as os from 'os';
@@ -17,20 +17,6 @@ interface GitHubRelease {
17
17
  }>;
18
18
  }
19
19
 
20
- function prompt(question: string): Promise<string> {
21
- const rl = createInterface({
22
- input: process.stdin,
23
- output: process.stderr,
24
- });
25
-
26
- return new Promise((resolve) => {
27
- rl.question(question, (answer) => {
28
- rl.close();
29
- resolve(answer.trim());
30
- });
31
- });
32
- }
33
-
34
20
  function getCurrentVersion(): string {
35
21
  return pkg.version;
36
22
  }
@@ -7,7 +7,7 @@ import type { Config, ServiceName } from '../types/config';
7
7
  const CONFIG_DIR = join(homedir(), '.config', 'agentio');
8
8
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
9
9
 
10
- const ALL_SERVICES: ServiceName[] = ['gmail', 'gchat', 'jira', 'slack', 'telegram', 'discourse', 'sql'];
10
+ const ALL_SERVICES: ServiceName[] = ['gmail', 'gchat', 'github', 'jira', 'slack', 'telegram', 'discourse', 'sql'];
11
11
 
12
12
  const DEFAULT_CONFIG: Config = {
13
13
  profiles: {},
@@ -7,7 +7,7 @@ import type {
7
7
  DiscourseListOptions,
8
8
  } from '../../types/discourse';
9
9
  import type { ServiceClient, ValidationResult } from '../../types/service';
10
- import { CliError, type ErrorCode } from '../../utils/errors';
10
+ import { CliError, httpStatusToErrorCode } from '../../utils/errors';
11
11
 
12
12
  interface DiscourseApiResponse {
13
13
  errors?: string[];
@@ -129,7 +129,7 @@ export class DiscourseClient implements ServiceClient {
129
129
  });
130
130
 
131
131
  if (!response.ok) {
132
- const errorCode = this.getErrorCode(response.status);
132
+ const errorCode = httpStatusToErrorCode(response.status);
133
133
  const text = await response.text();
134
134
  let message = `Discourse API error: ${response.status}`;
135
135
  try {
@@ -152,14 +152,6 @@ export class DiscourseClient implements ServiceClient {
152
152
  }
153
153
  }
154
154
 
155
- private getErrorCode(status: number): ErrorCode {
156
- if (status === 401) return 'AUTH_FAILED';
157
- if (status === 403) return 'PERMISSION_DENIED';
158
- if (status === 404) return 'NOT_FOUND';
159
- if (status === 429) return 'RATE_LIMITED';
160
- return 'API_ERROR';
161
- }
162
-
163
155
  async getCategories(): Promise<DiscourseCategory[]> {
164
156
  const data = await this.request<CategoryListResponse>('GET', '/categories.json');
165
157
 
@@ -1,7 +1,6 @@
1
1
  import { google } from 'googleapis';
2
2
  import type { chat_v1 } from 'googleapis';
3
- import { CliError } from '../../utils/errors';
4
- import type { ErrorCode } from '../../utils/errors';
3
+ import { CliError, httpStatusToErrorCode, type ErrorCode } from '../../utils/errors';
5
4
  import type { ServiceClient, ValidationResult } from '../../types/service';
6
5
  import { GOOGLE_OAUTH_CONFIG } from '../../config/credentials';
7
6
  import type {
@@ -331,10 +330,9 @@ export class GChatClient implements ServiceClient {
331
330
  if (err && typeof err === 'object') {
332
331
  const error = err as Record<string, unknown>;
333
332
  const code = error.code || error.status;
334
- if (code === 401) return 'AUTH_FAILED';
335
- if (code === 403) return 'PERMISSION_DENIED';
336
- if (code === 404) return 'NOT_FOUND';
337
- if (code === 429) return 'RATE_LIMITED';
333
+ if (typeof code === 'number') {
334
+ return httpStatusToErrorCode(code);
335
+ }
338
336
  }
339
337
  return 'API_ERROR';
340
338
  }
@@ -186,8 +186,9 @@ export class GmailClient implements ServiceClient {
186
186
  messages,
187
187
  total: response.data.resultSizeEstimate || messages.length,
188
188
  };
189
- } catch (error: any) {
190
- throw new CliError('API_ERROR', `Gmail API error: ${error.message}`);
189
+ } catch (error) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ throw new CliError('API_ERROR', `Gmail API error: ${message}`);
191
192
  }
192
193
  }
193
194
 
@@ -209,11 +210,12 @@ export class GmailClient implements ServiceClient {
209
210
  }
210
211
 
211
212
  return { ...message, body };
212
- } catch (error: any) {
213
- if (error.code === 404) {
213
+ } catch (error) {
214
+ if (this.isNotFoundError(error)) {
214
215
  throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
215
216
  }
216
- throw new CliError('API_ERROR', `Gmail API error: ${error.message}`);
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ throw new CliError('API_ERROR', `Gmail API error: ${message}`);
217
219
  }
218
220
  }
219
221
 
@@ -249,12 +251,13 @@ export class GmailClient implements ServiceClient {
249
251
  }
250
252
 
251
253
  return results;
252
- } catch (error: any) {
254
+ } catch (error) {
253
255
  if (error instanceof CliError) throw error;
254
- if (error.code === 404) {
256
+ if (this.isNotFoundError(error)) {
255
257
  throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
256
258
  }
257
- throw new CliError('API_ERROR', `Gmail API error: ${error.message}`);
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ throw new CliError('API_ERROR', `Gmail API error: ${message}`);
258
261
  }
259
262
  }
260
263
 
@@ -307,8 +310,9 @@ export class GmailClient implements ServiceClient {
307
310
  threadId: response.data.threadId!,
308
311
  labelIds: response.data.labelIds || ['SENT'],
309
312
  };
310
- } catch (error: any) {
311
- throw new CliError('API_ERROR', `Failed to send email: ${error.message}`);
313
+ } catch (error) {
314
+ const message = error instanceof Error ? error.message : String(error);
315
+ throw new CliError('API_ERROR', `Failed to send email: ${message}`);
312
316
  }
313
317
  }
314
318
 
@@ -453,9 +457,10 @@ export class GmailClient implements ServiceClient {
453
457
  mimeType,
454
458
  base64: Buffer.from(content).toString('base64'),
455
459
  };
456
- } catch (error: any) {
460
+ } catch (error) {
457
461
  if (error instanceof CliError) throw error;
458
- throw new CliError('API_ERROR', `Failed to read attachment ${attachment.path}: ${error.message}`);
462
+ const message = error instanceof Error ? error.message : String(error);
463
+ throw new CliError('API_ERROR', `Failed to read attachment ${attachment.path}: ${message}`);
459
464
  }
460
465
  }
461
466
 
@@ -510,9 +515,10 @@ export class GmailClient implements ServiceClient {
510
515
  threadId: response.data.threadId!,
511
516
  labelIds: response.data.labelIds || ['SENT'],
512
517
  };
513
- } catch (error: any) {
518
+ } catch (error) {
514
519
  if (error instanceof CliError) throw error;
515
- throw new CliError('API_ERROR', `Failed to send reply: ${error.message}`);
520
+ const message = error instanceof Error ? error.message : String(error);
521
+ throw new CliError('API_ERROR', `Failed to send reply: ${message}`);
516
522
  }
517
523
  }
518
524
 
@@ -525,11 +531,12 @@ export class GmailClient implements ServiceClient {
525
531
  removeLabelIds: ['INBOX'],
526
532
  },
527
533
  });
528
- } catch (error: any) {
529
- if (error.code === 404) {
534
+ } catch (error) {
535
+ if (this.isNotFoundError(error)) {
530
536
  throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
531
537
  }
532
- throw new CliError('API_ERROR', `Failed to archive: ${error.message}`);
538
+ const message = error instanceof Error ? error.message : String(error);
539
+ throw new CliError('API_ERROR', `Failed to archive: ${message}`);
533
540
  }
534
541
  }
535
542
 
@@ -542,11 +549,19 @@ export class GmailClient implements ServiceClient {
542
549
  ? { removeLabelIds: ['UNREAD'] }
543
550
  : { addLabelIds: ['UNREAD'] },
544
551
  });
545
- } catch (error: any) {
546
- if (error.code === 404) {
552
+ } catch (error) {
553
+ if (this.isNotFoundError(error)) {
547
554
  throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
548
555
  }
549
- throw new CliError('API_ERROR', `Failed to update message: ${error.message}`);
556
+ const message = error instanceof Error ? error.message : String(error);
557
+ throw new CliError('API_ERROR', `Failed to update message: ${message}`);
558
+ }
559
+ }
560
+
561
+ private isNotFoundError(error: unknown): boolean {
562
+ if (error && typeof error === 'object' && 'code' in error) {
563
+ return (error as { code: unknown }).code === 404;
550
564
  }
565
+ return false;
551
566
  }
552
567
  }
@@ -1,4 +1,4 @@
1
- import { CliError, type ErrorCode } from '../../utils/errors';
1
+ import { CliError, httpStatusToErrorCode } from '../../utils/errors';
2
2
  import type { ServiceClient, ValidationResult } from '../../types/service';
3
3
  import type {
4
4
  JiraCredentials,
@@ -86,7 +86,7 @@ export class JiraClient implements ServiceClient {
86
86
 
87
87
  if (!response.ok) {
88
88
  const errorText = await response.text();
89
- const code = this.getErrorCode(response.status);
89
+ const code = httpStatusToErrorCode(response.status);
90
90
  throw new CliError(code, `JIRA API error: ${errorText}`);
91
91
  }
92
92
 
@@ -98,14 +98,6 @@ export class JiraClient implements ServiceClient {
98
98
  return response.json();
99
99
  }
100
100
 
101
- private getErrorCode(status: number): ErrorCode {
102
- if (status === 401) return 'AUTH_FAILED';
103
- if (status === 403) return 'PERMISSION_DENIED';
104
- if (status === 404) return 'NOT_FOUND';
105
- if (status === 429) return 'RATE_LIMITED';
106
- return 'API_ERROR';
107
- }
108
-
109
101
  async listProjects(options: JiraProjectListOptions = {}): Promise<JiraProject[]> {
110
102
  const params = new URLSearchParams();
111
103
  if (options.maxResults) {
@@ -1,4 +1,4 @@
1
- import { CliError } from '../../utils/errors';
1
+ import { CliError, httpStatusToErrorCode } from '../../utils/errors';
2
2
  import type { ServiceClient, ValidationResult } from '../../types/service';
3
3
  import type {
4
4
  SlackCredentials,
@@ -54,7 +54,7 @@ export class SlackClient implements ServiceClient {
54
54
  if (!response.ok) {
55
55
  const error = await response.text();
56
56
  throw new CliError(
57
- this.getErrorCode(response.status),
57
+ httpStatusToErrorCode(response.status),
58
58
  `Failed to send message via webhook: ${response.status} ${error}`,
59
59
  'Check that the webhook URL is valid and the app has permission to post'
60
60
  );
@@ -74,11 +74,4 @@ export class SlackClient implements ServiceClient {
74
74
  }
75
75
  }
76
76
 
77
- private getErrorCode(status: number): 'AUTH_FAILED' | 'PERMISSION_DENIED' | 'NOT_FOUND' | 'RATE_LIMITED' | 'API_ERROR' {
78
- if (status === 401) return 'AUTH_FAILED';
79
- if (status === 403) return 'PERMISSION_DENIED';
80
- if (status === 404) return 'NOT_FOUND';
81
- if (status === 429) return 'RATE_LIMITED';
82
- return 'API_ERROR';
83
- }
84
77
  }
@@ -1,8 +1,8 @@
1
1
  export interface TelegramCredentials {
2
- bot_token: string;
3
- channel_id: string;
4
- bot_username?: string;
5
- channel_name?: string;
2
+ botToken: string;
3
+ channelId: string;
4
+ botUsername?: string;
5
+ channelName?: string;
6
6
  }
7
7
 
8
8
  export interface TelegramBotInfo {