@plosson/agentio 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/auth/oauth.ts +2 -0
- package/src/auth/token-manager.ts +2 -4
- package/src/commands/config.ts +43 -7
- package/src/commands/discourse.ts +10 -11
- package/src/commands/gchat.ts +69 -21
- package/src/commands/github.ts +7 -10
- package/src/commands/gmail.ts +21 -19
- package/src/commands/jira.ts +14 -14
- package/src/commands/slack.ts +6 -9
- package/src/commands/sql.ts +8 -9
- package/src/commands/status.ts +3 -8
- package/src/commands/telegram.ts +8 -9
- package/src/config/config-manager.ts +3 -37
- package/src/services/gchat/client.ts +50 -0
- package/src/types/config.ts +0 -10
- package/src/types/gchat.ts +3 -0
- package/src/utils/output.ts +20 -1
- package/src/utils/profile-commands.ts +4 -27
- package/src/utils/stdin.ts +0 -36
package/package.json
CHANGED
package/src/auth/oauth.ts
CHANGED
|
@@ -14,6 +14,8 @@ const GCHAT_SCOPES = [
|
|
|
14
14
|
'https://www.googleapis.com/auth/chat.messages.create', // send messages
|
|
15
15
|
'https://www.googleapis.com/auth/chat.messages.readonly', // read messages (get operations)
|
|
16
16
|
'https://www.googleapis.com/auth/chat.spaces.readonly', // read space info and list
|
|
17
|
+
'https://www.googleapis.com/auth/chat.memberships.readonly', // read space members
|
|
18
|
+
'https://www.googleapis.com/auth/userinfo.email', // get user email for profile naming
|
|
17
19
|
];
|
|
18
20
|
|
|
19
21
|
const PORT_RANGE_START = 3000;
|
|
@@ -10,16 +10,14 @@ const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
10
10
|
|
|
11
11
|
export async function getValidTokens(
|
|
12
12
|
service: ServiceName,
|
|
13
|
-
profileName
|
|
13
|
+
profileName: string
|
|
14
14
|
): Promise<{ tokens: OAuthTokens; profile: string }> {
|
|
15
15
|
const profile = await getProfile(service, profileName);
|
|
16
16
|
|
|
17
17
|
if (!profile) {
|
|
18
18
|
throw new CliError(
|
|
19
19
|
'PROFILE_NOT_FOUND',
|
|
20
|
-
profileName
|
|
21
|
-
? `Profile "${profileName}" not found for ${service}`
|
|
22
|
-
: `No default profile configured for ${service}`,
|
|
20
|
+
`Profile "${profileName}" not found for ${service}`,
|
|
23
21
|
`Run: agentio ${service} profile add`
|
|
24
22
|
);
|
|
25
23
|
}
|
package/src/commands/config.ts
CHANGED
|
@@ -3,12 +3,27 @@ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypt
|
|
|
3
3
|
import { readFile, writeFile } from 'fs/promises';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
6
7
|
import { loadConfig, saveConfig, setEnv, unsetEnv, listEnv } from '../config/config-manager';
|
|
7
8
|
import { getAllCredentials, setAllCredentials } from '../auth/token-store';
|
|
8
9
|
import { CliError, handleError } from '../utils/errors';
|
|
9
10
|
import type { Config } from '../types/config';
|
|
10
11
|
import type { StoredCredentials } from '../types/tokens';
|
|
11
12
|
|
|
13
|
+
async function confirm(message: string): Promise<boolean> {
|
|
14
|
+
const rl = createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stderr,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
21
|
+
rl.close();
|
|
22
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
const ALGORITHM = 'aes-256-gcm';
|
|
13
28
|
|
|
14
29
|
interface ExportedData {
|
|
@@ -231,13 +246,6 @@ export function registerConfigCommands(program: Command): void {
|
|
|
231
246
|
}
|
|
232
247
|
}
|
|
233
248
|
|
|
234
|
-
// Merge defaults (only if not set)
|
|
235
|
-
for (const [service, defaultProfile] of Object.entries(exportData.config.defaults)) {
|
|
236
|
-
if (defaultProfile && !(currentConfig.defaults as Record<string, string | undefined>)[service]) {
|
|
237
|
-
(currentConfig.defaults as Record<string, string>)[service] = defaultProfile;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
249
|
// Merge credentials
|
|
242
250
|
for (const [service, profiles] of Object.entries(exportData.credentials)) {
|
|
243
251
|
if (!currentCredentials[service]) {
|
|
@@ -315,4 +323,32 @@ export function registerConfigCommands(program: Command): void {
|
|
|
315
323
|
handleError(error);
|
|
316
324
|
}
|
|
317
325
|
});
|
|
326
|
+
|
|
327
|
+
config
|
|
328
|
+
.command('clear')
|
|
329
|
+
.description('Clear all configuration and credentials')
|
|
330
|
+
.option('--force', 'Skip confirmation prompt')
|
|
331
|
+
.action(async (options) => {
|
|
332
|
+
try {
|
|
333
|
+
if (!options.force) {
|
|
334
|
+
const confirmed = await confirm(
|
|
335
|
+
'This will delete all profiles and credentials. Are you sure?'
|
|
336
|
+
);
|
|
337
|
+
if (!confirmed) {
|
|
338
|
+
console.error('Aborted');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Reset config to default (empty profiles)
|
|
344
|
+
await saveConfig({ profiles: {} });
|
|
345
|
+
|
|
346
|
+
// Clear all credentials
|
|
347
|
+
await setAllCredentials({});
|
|
348
|
+
|
|
349
|
+
console.log('Configuration cleared');
|
|
350
|
+
} catch (error) {
|
|
351
|
+
handleError(error);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
318
354
|
}
|
|
@@ -4,7 +4,7 @@ import { setProfile, getProfile } from '../config/config-manager';
|
|
|
4
4
|
import { createProfileCommands } from '../utils/profile-commands';
|
|
5
5
|
import { DiscourseClient } from '../services/discourse/client';
|
|
6
6
|
import { CliError, handleError } from '../utils/errors';
|
|
7
|
-
import { prompt
|
|
7
|
+
import { prompt } from '../utils/stdin';
|
|
8
8
|
import type { DiscourseCredentials } from '../types/discourse';
|
|
9
9
|
import {
|
|
10
10
|
printDiscourseTopicList,
|
|
@@ -13,16 +13,14 @@ import {
|
|
|
13
13
|
} from '../utils/output';
|
|
14
14
|
|
|
15
15
|
async function getDiscourseClient(
|
|
16
|
-
profileName
|
|
16
|
+
profileName: string
|
|
17
17
|
): Promise<{ client: DiscourseClient; profile: string }> {
|
|
18
18
|
const profile = await getProfile('discourse', profileName);
|
|
19
19
|
|
|
20
20
|
if (!profile) {
|
|
21
21
|
throw new CliError(
|
|
22
22
|
'PROFILE_NOT_FOUND',
|
|
23
|
-
profileName
|
|
24
|
-
? `Profile "${profileName}" not found for discourse`
|
|
25
|
-
: 'No default profile configured for discourse',
|
|
23
|
+
`Profile "${profileName}" not found for discourse`,
|
|
26
24
|
'Run: agentio discourse profile add'
|
|
27
25
|
);
|
|
28
26
|
}
|
|
@@ -50,7 +48,7 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
50
48
|
discourse
|
|
51
49
|
.command('list')
|
|
52
50
|
.description('List latest topics')
|
|
53
|
-
.
|
|
51
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
54
52
|
.option('--category <slug>', 'Filter by category slug or name')
|
|
55
53
|
.option('--page <number>', 'Page number (0-indexed)', '0')
|
|
56
54
|
.action(async (options) => {
|
|
@@ -71,7 +69,7 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
71
69
|
.command('get')
|
|
72
70
|
.description('Get a topic with its posts')
|
|
73
71
|
.argument('<topic-id>', 'Topic ID')
|
|
74
|
-
.
|
|
72
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
75
73
|
.action(async (topicId: string, options) => {
|
|
76
74
|
try {
|
|
77
75
|
const id = parseInt(topicId, 10);
|
|
@@ -91,7 +89,7 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
91
89
|
discourse
|
|
92
90
|
.command('categories')
|
|
93
91
|
.description('List all categories')
|
|
94
|
-
.
|
|
92
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
95
93
|
.action(async (options) => {
|
|
96
94
|
try {
|
|
97
95
|
const { client } = await getDiscourseClient(options.profile);
|
|
@@ -112,11 +110,9 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
112
110
|
profile
|
|
113
111
|
.command('add')
|
|
114
112
|
.description('Add a new Discourse profile')
|
|
115
|
-
.option('--profile <name>', 'Profile name
|
|
113
|
+
.option('--profile <name>', 'Profile name (auto-detected from username if not provided)')
|
|
116
114
|
.action(async (options) => {
|
|
117
115
|
try {
|
|
118
|
-
const profileName = await resolveProfileName('discourse', options.profile);
|
|
119
|
-
|
|
120
116
|
console.error('\nDiscourse Setup\n');
|
|
121
117
|
|
|
122
118
|
// Step 1: Get base URL
|
|
@@ -196,6 +192,9 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
196
192
|
console.error(`\nConnected to ${normalizedUrl}`);
|
|
197
193
|
console.error(`Authenticated as ${username}\n`);
|
|
198
194
|
|
|
195
|
+
// Auto-name based on username
|
|
196
|
+
const profileName = options.profile || username.trim();
|
|
197
|
+
|
|
199
198
|
// Save credentials
|
|
200
199
|
await setProfile('discourse', profileName);
|
|
201
200
|
await setCredentials('discourse', profileName, credentials);
|
package/src/commands/gchat.ts
CHANGED
|
@@ -8,19 +8,17 @@ import { performOAuthFlow } from '../auth/oauth';
|
|
|
8
8
|
import { createGoogleAuth } from '../auth/token-manager';
|
|
9
9
|
import { GChatClient } from '../services/gchat/client';
|
|
10
10
|
import { CliError, handleError } from '../utils/errors';
|
|
11
|
-
import { readStdin, prompt
|
|
12
|
-
import { printGChatSendResult, printGChatMessageList, printGChatMessage } from '../utils/output';
|
|
11
|
+
import { readStdin, prompt } from '../utils/stdin';
|
|
12
|
+
import { printGChatSendResult, printGChatMessageList, printGChatMessage, printGChatSpaceList } from '../utils/output';
|
|
13
13
|
import type { GChatCredentials, GChatWebhookCredentials, GChatOAuthCredentials } from '../types/gchat';
|
|
14
14
|
|
|
15
|
-
async function getGChatClient(profileName
|
|
15
|
+
async function getGChatClient(profileName: string): Promise<{ client: GChatClient; profile: string }> {
|
|
16
16
|
const profile = await getProfile('gchat', profileName);
|
|
17
17
|
|
|
18
18
|
if (!profile) {
|
|
19
19
|
throw new CliError(
|
|
20
20
|
'PROFILE_NOT_FOUND',
|
|
21
|
-
profileName
|
|
22
|
-
? `Profile "${profileName}" not found for gchat`
|
|
23
|
-
: 'No default profile configured for gchat',
|
|
21
|
+
`Profile "${profileName}" not found for gchat`,
|
|
24
22
|
'Run: agentio gchat profile add'
|
|
25
23
|
);
|
|
26
24
|
}
|
|
@@ -49,7 +47,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
49
47
|
gchat
|
|
50
48
|
.command('send')
|
|
51
49
|
.description('Send a message to Google Chat')
|
|
52
|
-
.
|
|
50
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
53
51
|
.option('--space <id>', 'Space ID (required for OAuth profiles)')
|
|
54
52
|
.option('--thread <id>', 'Thread ID (optional)')
|
|
55
53
|
.option('--json [file]', 'Send rich message from JSON file (or stdin if no file specified)')
|
|
@@ -134,15 +132,19 @@ export function registerGChatCommands(program: Command): void {
|
|
|
134
132
|
gchat
|
|
135
133
|
.command('list')
|
|
136
134
|
.description('List messages from a Google Chat space (OAuth profiles only)')
|
|
137
|
-
.
|
|
135
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
138
136
|
.requiredOption('--space <id>', 'Space ID')
|
|
139
137
|
.option('--limit <n>', 'Number of messages', '10')
|
|
138
|
+
.option('--thread <id>', 'Filter by thread ID')
|
|
139
|
+
.option('--since <date>', 'Only messages after this date (YYYY-MM-DD)')
|
|
140
140
|
.action(async (options) => {
|
|
141
141
|
try {
|
|
142
142
|
const { client } = await getGChatClient(options.profile);
|
|
143
143
|
const messages = await client.list({
|
|
144
144
|
spaceId: options.space,
|
|
145
145
|
limit: parseInt(options.limit, 10),
|
|
146
|
+
threadId: options.thread,
|
|
147
|
+
since: options.since ? new Date(options.since) : undefined,
|
|
146
148
|
});
|
|
147
149
|
|
|
148
150
|
printGChatMessageList(messages);
|
|
@@ -154,7 +156,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
154
156
|
gchat
|
|
155
157
|
.command('get <message-id>')
|
|
156
158
|
.description('Get a message from a Google Chat space (OAuth profiles only)')
|
|
157
|
-
.
|
|
159
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
158
160
|
.requiredOption('--space <id>', 'Space ID')
|
|
159
161
|
.action(async (messageId: string, options) => {
|
|
160
162
|
try {
|
|
@@ -170,6 +172,27 @@ export function registerGChatCommands(program: Command): void {
|
|
|
170
172
|
}
|
|
171
173
|
});
|
|
172
174
|
|
|
175
|
+
gchat
|
|
176
|
+
.command('spaces')
|
|
177
|
+
.description('List available Google Chat spaces (OAuth profiles only)')
|
|
178
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
179
|
+
.option('--filter <text>', 'Filter spaces by name (case-insensitive)')
|
|
180
|
+
.action(async (options) => {
|
|
181
|
+
try {
|
|
182
|
+
const { client } = await getGChatClient(options.profile);
|
|
183
|
+
let spaces = await client.listSpaces();
|
|
184
|
+
|
|
185
|
+
if (options.filter) {
|
|
186
|
+
const filterLower = options.filter.toLowerCase();
|
|
187
|
+
spaces = spaces.filter(s => s.displayName.toLowerCase().includes(filterLower));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
printGChatSpaceList(spaces);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
handleError(error);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
173
196
|
// Profile management
|
|
174
197
|
const profile = createProfileCommands<GChatCredentials>(gchat, {
|
|
175
198
|
service: 'gchat',
|
|
@@ -180,19 +203,24 @@ export function registerGChatCommands(program: Command): void {
|
|
|
180
203
|
profile
|
|
181
204
|
.command('add')
|
|
182
205
|
.description('Add a new Google Chat profile (webhook or OAuth)')
|
|
183
|
-
.option('--profile <name>', 'Profile name
|
|
206
|
+
.option('--profile <name>', 'Profile name (required for webhook, auto-detected for OAuth)')
|
|
184
207
|
.action(async (options) => {
|
|
185
208
|
try {
|
|
186
|
-
const profileName = await resolveProfileName('gchat', options.profile);
|
|
187
|
-
|
|
188
209
|
console.error('\nGoogle Chat Setup\n');
|
|
189
210
|
|
|
190
211
|
const profileType = await prompt('Choose profile type (webhook/oauth): ');
|
|
191
212
|
|
|
192
213
|
if (profileType.toLowerCase() === 'webhook') {
|
|
193
|
-
|
|
214
|
+
if (!options.profile) {
|
|
215
|
+
throw new CliError(
|
|
216
|
+
'INVALID_PARAMS',
|
|
217
|
+
'Profile name is required for webhook profiles',
|
|
218
|
+
'Run: agentio gchat profile add --profile <name>'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
await setupWebhookProfile(options.profile);
|
|
194
222
|
} else if (profileType.toLowerCase() === 'oauth') {
|
|
195
|
-
await setupOAuthProfile(
|
|
223
|
+
await setupOAuthProfile(options.profile);
|
|
196
224
|
} else {
|
|
197
225
|
throw new CliError('INVALID_PARAMS', 'Profile type must be "webhook" or "oauth"');
|
|
198
226
|
}
|
|
@@ -257,27 +285,46 @@ async function setupWebhookProfile(profileName: string): Promise<void> {
|
|
|
257
285
|
printProfileSetupSuccess(profileName, 'webhook');
|
|
258
286
|
}
|
|
259
287
|
|
|
260
|
-
async function setupOAuthProfile(
|
|
288
|
+
async function setupOAuthProfile(profileNameOverride?: string): Promise<void> {
|
|
261
289
|
console.error('OAuth Setup\n');
|
|
262
290
|
console.error('Starting OAuth flow for Google Chat profile...\n');
|
|
263
291
|
|
|
264
292
|
const tokens = await performOAuthFlow('gchat');
|
|
293
|
+
const auth = createGoogleAuth(tokens);
|
|
265
294
|
|
|
266
|
-
//
|
|
267
|
-
|
|
295
|
+
// Fetch user email for profile naming
|
|
296
|
+
let userEmail: string;
|
|
297
|
+
try {
|
|
298
|
+
const oauth2 = google.oauth2({ version: 'v2', auth });
|
|
299
|
+
const userInfo = await oauth2.userinfo.get();
|
|
300
|
+
userEmail = userInfo.data.email || '';
|
|
301
|
+
if (!userEmail) {
|
|
302
|
+
throw new Error('No email returned');
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
306
|
+
throw new CliError(
|
|
307
|
+
'AUTH_FAILED',
|
|
308
|
+
`Failed to fetch user email: ${errorMessage}`,
|
|
309
|
+
'Ensure the account has an email address'
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Validate the token works with Chat API
|
|
268
314
|
try {
|
|
269
|
-
const auth = createGoogleAuth(tokens);
|
|
270
315
|
const chat = google.chat({ version: 'v1', auth });
|
|
271
|
-
// Simple validation: list spaces
|
|
272
316
|
await chat.spaces.list({ pageSize: 1 });
|
|
273
317
|
} catch (error) {
|
|
318
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
274
319
|
throw new CliError(
|
|
275
320
|
'AUTH_FAILED',
|
|
276
|
-
|
|
277
|
-
'
|
|
321
|
+
`Failed to validate Google Chat access: ${errorMessage}`,
|
|
322
|
+
'Google Chat API requires a Google Workspace account. Personal Gmail accounts cannot use the Chat API.'
|
|
278
323
|
);
|
|
279
324
|
}
|
|
280
325
|
|
|
326
|
+
const profileName = profileNameOverride || userEmail;
|
|
327
|
+
|
|
281
328
|
const credentials: GChatOAuthCredentials = {
|
|
282
329
|
type: 'oauth',
|
|
283
330
|
accessToken: tokens.access_token,
|
|
@@ -285,6 +332,7 @@ async function setupOAuthProfile(profileName: string): Promise<void> {
|
|
|
285
332
|
expiryDate: tokens.expiry_date,
|
|
286
333
|
tokenType: tokens.token_type,
|
|
287
334
|
scope: tokens.scope,
|
|
335
|
+
email: userEmail,
|
|
288
336
|
};
|
|
289
337
|
|
|
290
338
|
await setProfile('gchat', profileName);
|
package/src/commands/github.ts
CHANGED
|
@@ -6,18 +6,15 @@ import { GitHubClient } from '../services/github/client';
|
|
|
6
6
|
import { performGitHubOAuthFlow } from '../auth/github-oauth';
|
|
7
7
|
import { generateExportData } from './config';
|
|
8
8
|
import { CliError, handleError } from '../utils/errors';
|
|
9
|
-
import { resolveProfileName } from '../utils/stdin';
|
|
10
9
|
import type { GitHubCredentials } from '../types/github';
|
|
11
10
|
|
|
12
|
-
async function getGitHubClient(profileName
|
|
11
|
+
async function getGitHubClient(profileName: string): Promise<{ client: GitHubClient; profile: string }> {
|
|
13
12
|
const profile = await getProfile('github', profileName);
|
|
14
13
|
|
|
15
14
|
if (!profile) {
|
|
16
15
|
throw new CliError(
|
|
17
16
|
'PROFILE_NOT_FOUND',
|
|
18
|
-
profileName
|
|
19
|
-
? `Profile "${profileName}" not found for github`
|
|
20
|
-
: 'No default profile configured for github',
|
|
17
|
+
`Profile "${profileName}" not found for github`,
|
|
21
18
|
'Run: agentio github profile add'
|
|
22
19
|
);
|
|
23
20
|
}
|
|
@@ -59,7 +56,7 @@ export function registerGitHubCommands(program: Command): void {
|
|
|
59
56
|
.command('install')
|
|
60
57
|
.description('Install AGENTIO_KEY and AGENTIO_CONFIG as GitHub Actions secrets')
|
|
61
58
|
.argument('<repo>', 'Repository in owner/repo format')
|
|
62
|
-
.
|
|
59
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
63
60
|
.action(async (repo: string, options) => {
|
|
64
61
|
try {
|
|
65
62
|
// Validate repo format
|
|
@@ -94,7 +91,7 @@ export function registerGitHubCommands(program: Command): void {
|
|
|
94
91
|
.command('uninstall')
|
|
95
92
|
.description('Remove AGENTIO_KEY and AGENTIO_CONFIG secrets from a repository')
|
|
96
93
|
.argument('<repo>', 'Repository in owner/repo format')
|
|
97
|
-
.
|
|
94
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
98
95
|
.action(async (repo: string, options) => {
|
|
99
96
|
try {
|
|
100
97
|
// Validate repo format
|
|
@@ -128,11 +125,9 @@ export function registerGitHubCommands(program: Command): void {
|
|
|
128
125
|
profile
|
|
129
126
|
.command('add')
|
|
130
127
|
.description('Add a new GitHub profile')
|
|
131
|
-
.option('--profile <name>', 'Profile name
|
|
128
|
+
.option('--profile <name>', 'Profile name (auto-detected from username if not provided)')
|
|
132
129
|
.action(async (options) => {
|
|
133
130
|
try {
|
|
134
|
-
const profileName = await resolveProfileName('github', options.profile);
|
|
135
|
-
|
|
136
131
|
console.error('\nGitHub Setup\n');
|
|
137
132
|
console.error('This will open your browser to authorize agentio with GitHub.');
|
|
138
133
|
console.error('You will need to grant access to repositories where you want to set secrets.\n');
|
|
@@ -154,6 +149,8 @@ export function registerGitHubCommands(program: Command): void {
|
|
|
154
149
|
credentials.username = user.login;
|
|
155
150
|
credentials.email = user.email;
|
|
156
151
|
|
|
152
|
+
const profileName = options.profile || user.login;
|
|
153
|
+
|
|
157
154
|
console.error(`\nAuthenticated as: ${user.login}${user.email ? ` (${user.email})` : ''}`);
|
|
158
155
|
|
|
159
156
|
// Save credentials
|
package/src/commands/gmail.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { performOAuthFlow } from '../auth/oauth';
|
|
|
10
10
|
import { GmailClient } from '../services/gmail/client';
|
|
11
11
|
import { printMessageList, printMessage, printSendResult, printArchived, printMarked, printAttachmentList, printAttachmentDownloaded, raw } from '../utils/output';
|
|
12
12
|
import { CliError, handleError } from '../utils/errors';
|
|
13
|
-
import { readStdin
|
|
13
|
+
import { readStdin } from '../utils/stdin';
|
|
14
14
|
import type { GmailAttachment } from '../types/gmail';
|
|
15
15
|
|
|
16
16
|
function escapeHtml(text: string): string {
|
|
@@ -68,7 +68,7 @@ function findChromePath(): string | null {
|
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
async function getGmailClient(profileName
|
|
71
|
+
async function getGmailClient(profileName: string): Promise<{ client: GmailClient; profile: string }> {
|
|
72
72
|
const { tokens, profile } = await getValidTokens('gmail', profileName);
|
|
73
73
|
const auth = createGoogleAuth(tokens);
|
|
74
74
|
return { client: new GmailClient(auth), profile };
|
|
@@ -82,7 +82,7 @@ export function registerGmailCommands(program: Command): void {
|
|
|
82
82
|
gmail
|
|
83
83
|
.command('list')
|
|
84
84
|
.description('List messages')
|
|
85
|
-
.
|
|
85
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
86
86
|
.option('--limit <n>', 'Number of messages', '10')
|
|
87
87
|
.option('--query <query>', 'Gmail search query (see "gmail search --help" for syntax)')
|
|
88
88
|
.option('--label <label>', 'Filter by label (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
@@ -103,7 +103,7 @@ export function registerGmailCommands(program: Command): void {
|
|
|
103
103
|
gmail
|
|
104
104
|
.command('get <message-id>')
|
|
105
105
|
.description('Get a message')
|
|
106
|
-
.
|
|
106
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
107
107
|
.option('--format <format>', 'Body format: text, html, or raw', 'text')
|
|
108
108
|
.option('--body-only', 'Output only the message body')
|
|
109
109
|
.action(async (messageId: string, options) => {
|
|
@@ -124,7 +124,7 @@ export function registerGmailCommands(program: Command): void {
|
|
|
124
124
|
.command('search')
|
|
125
125
|
.description('Search messages using Gmail query syntax')
|
|
126
126
|
.requiredOption('--query <query>', 'Search query')
|
|
127
|
-
.
|
|
127
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
128
128
|
.option('--limit <n>', 'Max results', '10')
|
|
129
129
|
.addHelpText('after', `
|
|
130
130
|
Query Syntax Examples:
|
|
@@ -178,7 +178,7 @@ Query Syntax Examples:
|
|
|
178
178
|
gmail
|
|
179
179
|
.command('send')
|
|
180
180
|
.description('Send an email')
|
|
181
|
-
.
|
|
181
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
182
182
|
.requiredOption('--to <email>', 'Recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
183
183
|
.option('--cc <email>', 'CC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
184
184
|
.option('--bcc <email>', 'BCC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
@@ -246,7 +246,7 @@ Query Syntax Examples:
|
|
|
246
246
|
gmail
|
|
247
247
|
.command('reply')
|
|
248
248
|
.description('Reply to a thread')
|
|
249
|
-
.
|
|
249
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
250
250
|
.requiredOption('--thread-id <id>', 'Thread ID')
|
|
251
251
|
.option('--body <body>', 'Reply body (or pipe via stdin)')
|
|
252
252
|
.option('--html', 'Treat body as HTML')
|
|
@@ -277,7 +277,7 @@ Query Syntax Examples:
|
|
|
277
277
|
gmail
|
|
278
278
|
.command('archive <message-id...>')
|
|
279
279
|
.description('Archive one or more messages')
|
|
280
|
-
.
|
|
280
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
281
281
|
.action(async (messageIds: string[], options) => {
|
|
282
282
|
try {
|
|
283
283
|
const { client } = await getGmailClient(options.profile);
|
|
@@ -293,7 +293,7 @@ Query Syntax Examples:
|
|
|
293
293
|
gmail
|
|
294
294
|
.command('mark <message-id...>')
|
|
295
295
|
.description('Mark one or more messages as read or unread')
|
|
296
|
-
.
|
|
296
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
297
297
|
.option('--read', 'Mark as read')
|
|
298
298
|
.option('--unread', 'Mark as unread')
|
|
299
299
|
.action(async (messageIds: string[], options) => {
|
|
@@ -318,7 +318,7 @@ Query Syntax Examples:
|
|
|
318
318
|
gmail
|
|
319
319
|
.command('attachment <message-id>')
|
|
320
320
|
.description('Download attachments from a message')
|
|
321
|
-
.
|
|
321
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
322
322
|
.option('--name <filename>', 'Download specific attachment by filename (downloads all if not specified)')
|
|
323
323
|
.option('--output <dir>', 'Output directory', '.')
|
|
324
324
|
.action(async (messageId: string, options) => {
|
|
@@ -361,7 +361,7 @@ Query Syntax Examples:
|
|
|
361
361
|
gmail
|
|
362
362
|
.command('export <message-id>')
|
|
363
363
|
.description('Export a message as PDF')
|
|
364
|
-
.
|
|
364
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
365
365
|
.option('--output <path>', 'Output file path', 'message.pdf')
|
|
366
366
|
.action(async (messageId: string, options) => {
|
|
367
367
|
try {
|
|
@@ -452,12 +452,10 @@ ${emailHeader}
|
|
|
452
452
|
profile
|
|
453
453
|
.command('add')
|
|
454
454
|
.description('Add a new Gmail profile')
|
|
455
|
-
.option('--profile <name>', 'Profile name
|
|
455
|
+
.option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
|
|
456
456
|
.action(async (options) => {
|
|
457
457
|
try {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
console.error(`Starting OAuth flow for Gmail profile "${profileName}"...`);
|
|
458
|
+
console.error('Starting OAuth flow for Gmail...\n');
|
|
461
459
|
|
|
462
460
|
const tokens = await performOAuthFlow('gmail');
|
|
463
461
|
|
|
@@ -467,13 +465,17 @@ ${emailHeader}
|
|
|
467
465
|
const userProfile = await gmailApi.users.getProfile({ userId: 'me' });
|
|
468
466
|
const email = userProfile.data.emailAddress;
|
|
469
467
|
|
|
468
|
+
if (!email) {
|
|
469
|
+
throw new CliError('AUTH_FAILED', 'Could not fetch email from Gmail', 'Try again or specify --profile manually');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const profileName = options.profile || email;
|
|
473
|
+
|
|
470
474
|
await setProfile('gmail', profileName);
|
|
471
475
|
await setCredentials('gmail', profileName, { ...tokens, email });
|
|
472
476
|
|
|
473
|
-
console.log(`\nSuccess! Profile "${profileName}"
|
|
474
|
-
|
|
475
|
-
console.log(` Email: ${email}`);
|
|
476
|
-
}
|
|
477
|
+
console.log(`\nSuccess! Profile "${profileName}" configured.`);
|
|
478
|
+
console.log(` Email: ${email}`);
|
|
477
479
|
} catch (error) {
|
|
478
480
|
handleError(error);
|
|
479
481
|
}
|
package/src/commands/jira.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { createProfileCommands } from '../utils/profile-commands';
|
|
|
5
5
|
import { performJiraOAuthFlow, refreshJiraToken, type AtlassianSite } from '../auth/jira-oauth';
|
|
6
6
|
import { JiraClient } from '../services/jira/client';
|
|
7
7
|
import { CliError, handleError } from '../utils/errors';
|
|
8
|
-
import { readStdin, prompt
|
|
8
|
+
import { readStdin, prompt } from '../utils/stdin';
|
|
9
9
|
import {
|
|
10
10
|
printJiraProjectList,
|
|
11
11
|
printJiraIssueList,
|
|
@@ -46,15 +46,13 @@ async function ensureValidToken(credentials: JiraCredentials, profile: string):
|
|
|
46
46
|
return credentials;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function getJiraClient(profileName
|
|
49
|
+
async function getJiraClient(profileName: string): Promise<{ client: JiraClient; profile: string }> {
|
|
50
50
|
const profile = await getProfile('jira', profileName);
|
|
51
51
|
|
|
52
52
|
if (!profile) {
|
|
53
53
|
throw new CliError(
|
|
54
54
|
'PROFILE_NOT_FOUND',
|
|
55
|
-
profileName
|
|
56
|
-
? `Profile "${profileName}" not found for jira`
|
|
57
|
-
: 'No default profile configured for jira',
|
|
55
|
+
`Profile "${profileName}" not found for jira`,
|
|
58
56
|
'Run: agentio jira profile add'
|
|
59
57
|
);
|
|
60
58
|
}
|
|
@@ -87,7 +85,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
87
85
|
jira
|
|
88
86
|
.command('projects')
|
|
89
87
|
.description('List JIRA projects')
|
|
90
|
-
.
|
|
88
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
91
89
|
.option('--limit <number>', 'Maximum number of projects', '50')
|
|
92
90
|
.action(async (options) => {
|
|
93
91
|
try {
|
|
@@ -105,7 +103,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
105
103
|
jira
|
|
106
104
|
.command('search')
|
|
107
105
|
.description('Search JIRA issues')
|
|
108
|
-
.
|
|
106
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
109
107
|
.option('--jql <query>', 'JQL query')
|
|
110
108
|
.option('--project <key>', 'Project key')
|
|
111
109
|
.option('--status <status>', 'Issue status')
|
|
@@ -132,7 +130,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
132
130
|
.command('get')
|
|
133
131
|
.description('Get JIRA issue details')
|
|
134
132
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
135
|
-
.
|
|
133
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
136
134
|
.action(async (issueKey: string, options) => {
|
|
137
135
|
try {
|
|
138
136
|
const { client } = await getJiraClient(options.profile);
|
|
@@ -149,7 +147,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
149
147
|
.description('Add a comment to an issue')
|
|
150
148
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
151
149
|
.argument('[body]', 'Comment body (or pipe via stdin)')
|
|
152
|
-
.
|
|
150
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
153
151
|
.action(async (issueKey: string, body: string | undefined, options) => {
|
|
154
152
|
try {
|
|
155
153
|
let text = body;
|
|
@@ -175,7 +173,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
175
173
|
.command('transitions')
|
|
176
174
|
.description('List available transitions for an issue')
|
|
177
175
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
178
|
-
.
|
|
176
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
179
177
|
.action(async (issueKey: string, options) => {
|
|
180
178
|
try {
|
|
181
179
|
const { client } = await getJiraClient(options.profile);
|
|
@@ -192,7 +190,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
192
190
|
.description('Transition an issue to a new status')
|
|
193
191
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
194
192
|
.argument('<transition-id>', 'Transition ID (use "transitions" command to see available)')
|
|
195
|
-
.
|
|
193
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
196
194
|
.action(async (issueKey: string, transitionId: string, options) => {
|
|
197
195
|
try {
|
|
198
196
|
const { client } = await getJiraClient(options.profile);
|
|
@@ -213,11 +211,9 @@ export function registerJiraCommands(program: Command): void {
|
|
|
213
211
|
profile
|
|
214
212
|
.command('add')
|
|
215
213
|
.description('Add a new JIRA profile with OAuth authentication')
|
|
216
|
-
.option('--profile <name>', 'Profile name
|
|
214
|
+
.option('--profile <name>', 'Profile name (auto-detected from site URL if not provided)')
|
|
217
215
|
.action(async (options) => {
|
|
218
216
|
try {
|
|
219
|
-
const profileName = await resolveProfileName('jira', options.profile);
|
|
220
|
-
|
|
221
217
|
console.error('\nJIRA OAuth Setup\n');
|
|
222
218
|
|
|
223
219
|
// Site selection callback
|
|
@@ -242,6 +238,10 @@ export function registerJiraCommands(program: Command): void {
|
|
|
242
238
|
|
|
243
239
|
console.error(`\nAuthorized for site: ${result.siteUrl}\n`);
|
|
244
240
|
|
|
241
|
+
// Auto-name based on site hostname
|
|
242
|
+
const siteHostname = new URL(result.siteUrl).hostname;
|
|
243
|
+
const profileName = options.profile || siteHostname;
|
|
244
|
+
|
|
245
245
|
// Save credentials
|
|
246
246
|
const credentials: JiraCredentials = {
|
|
247
247
|
accessToken: result.accessToken,
|
package/src/commands/slack.ts
CHANGED
|
@@ -5,19 +5,17 @@ import { setProfile, getProfile } from '../config/config-manager';
|
|
|
5
5
|
import { createProfileCommands } from '../utils/profile-commands';
|
|
6
6
|
import { SlackClient } from '../services/slack/client';
|
|
7
7
|
import { CliError, handleError } from '../utils/errors';
|
|
8
|
-
import { readStdin, prompt
|
|
8
|
+
import { readStdin, prompt } from '../utils/stdin';
|
|
9
9
|
import { printSlackSendResult } from '../utils/output';
|
|
10
10
|
import type { SlackCredentials, SlackWebhookCredentials } from '../types/slack';
|
|
11
11
|
|
|
12
|
-
async function getSlackClient(profileName
|
|
12
|
+
async function getSlackClient(profileName: string): Promise<{ client: SlackClient; profile: string }> {
|
|
13
13
|
const profile = await getProfile('slack', profileName);
|
|
14
14
|
|
|
15
15
|
if (!profile) {
|
|
16
16
|
throw new CliError(
|
|
17
17
|
'PROFILE_NOT_FOUND',
|
|
18
|
-
profileName
|
|
19
|
-
? `Profile "${profileName}" not found for slack`
|
|
20
|
-
: 'No default profile configured for slack',
|
|
18
|
+
`Profile "${profileName}" not found for slack`,
|
|
21
19
|
'Run: agentio slack profile add'
|
|
22
20
|
);
|
|
23
21
|
}
|
|
@@ -46,7 +44,7 @@ export function registerSlackCommands(program: Command): void {
|
|
|
46
44
|
slack
|
|
47
45
|
.command('send')
|
|
48
46
|
.description('Send a message to Slack')
|
|
49
|
-
.
|
|
47
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
50
48
|
.option('--json [file]', 'Send Block Kit message from JSON file (or stdin if no file specified)')
|
|
51
49
|
.argument('[message]', 'Message text (or pipe via stdin)')
|
|
52
50
|
.action(async (message: string | undefined, options) => {
|
|
@@ -134,11 +132,10 @@ export function registerSlackCommands(program: Command): void {
|
|
|
134
132
|
profile
|
|
135
133
|
.command('add')
|
|
136
134
|
.description('Add a new Slack profile (webhook)')
|
|
137
|
-
.
|
|
135
|
+
.requiredOption('--profile <name>', 'Profile name (required)')
|
|
138
136
|
.action(async (options) => {
|
|
139
137
|
try {
|
|
140
|
-
|
|
141
|
-
await setupWebhookProfile(profileName);
|
|
138
|
+
await setupWebhookProfile(options.profile);
|
|
142
139
|
} catch (error) {
|
|
143
140
|
handleError(error);
|
|
144
141
|
}
|
package/src/commands/sql.ts
CHANGED
|
@@ -4,18 +4,16 @@ import { setProfile, getProfile } from '../config/config-manager';
|
|
|
4
4
|
import { createProfileCommands } from '../utils/profile-commands';
|
|
5
5
|
import { SqlClient } from '../services/sql/client';
|
|
6
6
|
import { CliError, handleError } from '../utils/errors';
|
|
7
|
-
import { readStdin, prompt
|
|
7
|
+
import { readStdin, prompt } from '../utils/stdin';
|
|
8
8
|
import type { SqlCredentials } from '../types/sql';
|
|
9
9
|
|
|
10
|
-
async function getSqlClient(profileName
|
|
10
|
+
async function getSqlClient(profileName: string): Promise<{ client: SqlClient; profile: string }> {
|
|
11
11
|
const profile = await getProfile('sql', profileName);
|
|
12
12
|
|
|
13
13
|
if (!profile) {
|
|
14
14
|
throw new CliError(
|
|
15
15
|
'PROFILE_NOT_FOUND',
|
|
16
|
-
profileName
|
|
17
|
-
? `Profile "${profileName}" not found for sql`
|
|
18
|
-
: 'No default profile configured for sql',
|
|
16
|
+
`Profile "${profileName}" not found for sql`,
|
|
19
17
|
'Run: agentio sql profile add'
|
|
20
18
|
);
|
|
21
19
|
}
|
|
@@ -55,7 +53,7 @@ export function registerSqlCommands(program: Command): void {
|
|
|
55
53
|
sql
|
|
56
54
|
.command('query')
|
|
57
55
|
.description('Execute a SQL query')
|
|
58
|
-
.
|
|
56
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
59
57
|
.option('--limit <n>', 'Maximum rows to return', '100')
|
|
60
58
|
.argument('[query]', 'SQL query (or pipe via stdin)')
|
|
61
59
|
.action(async (query: string | undefined, options) => {
|
|
@@ -94,12 +92,10 @@ export function registerSqlCommands(program: Command): void {
|
|
|
94
92
|
profile
|
|
95
93
|
.command('add')
|
|
96
94
|
.description('Add a new SQL database profile')
|
|
97
|
-
.option('--profile <name>', 'Profile name
|
|
95
|
+
.option('--profile <name>', 'Profile name (auto-detected from connection if not provided)')
|
|
98
96
|
.option('--interactive', 'Interactive mode: prompt for individual connection components')
|
|
99
97
|
.action(async (options) => {
|
|
100
98
|
try {
|
|
101
|
-
const profileName = await resolveProfileName('sql', options.profile);
|
|
102
|
-
|
|
103
99
|
let url: string;
|
|
104
100
|
|
|
105
101
|
if (options.interactive) {
|
|
@@ -138,6 +134,9 @@ export function registerSqlCommands(program: Command): void {
|
|
|
138
134
|
const displayName = extractDisplayName(url);
|
|
139
135
|
console.error(`\nConnected to: ${displayName}\n`);
|
|
140
136
|
|
|
137
|
+
// Auto-name based on connection display name
|
|
138
|
+
const profileName = options.profile || displayName;
|
|
139
|
+
|
|
141
140
|
// Save credentials
|
|
142
141
|
const credentials: SqlCredentials = {
|
|
143
142
|
url,
|
package/src/commands/status.ts
CHANGED
|
@@ -144,7 +144,6 @@ async function createServiceClient(
|
|
|
144
144
|
interface ProfileStatus {
|
|
145
145
|
service: string;
|
|
146
146
|
profile: string;
|
|
147
|
-
isDefault: boolean;
|
|
148
147
|
status: 'ok' | 'invalid' | 'no-creds' | 'skipped';
|
|
149
148
|
info?: string;
|
|
150
149
|
error?: string;
|
|
@@ -166,7 +165,7 @@ export function registerStatusCommand(program: Command): void {
|
|
|
166
165
|
// Collect all profile statuses
|
|
167
166
|
const statuses: ProfileStatus[] = [];
|
|
168
167
|
|
|
169
|
-
for (const { service, profiles
|
|
168
|
+
for (const { service, profiles } of allProfiles) {
|
|
170
169
|
for (const name of profiles) {
|
|
171
170
|
const credentials = await getCredentials(service, name);
|
|
172
171
|
|
|
@@ -174,7 +173,6 @@ export function registerStatusCommand(program: Command): void {
|
|
|
174
173
|
statuses.push({
|
|
175
174
|
service,
|
|
176
175
|
profile: name,
|
|
177
|
-
isDefault: name === defaultProfile,
|
|
178
176
|
status: 'no-creds',
|
|
179
177
|
});
|
|
180
178
|
continue;
|
|
@@ -184,7 +182,6 @@ export function registerStatusCommand(program: Command): void {
|
|
|
184
182
|
statuses.push({
|
|
185
183
|
service,
|
|
186
184
|
profile: name,
|
|
187
|
-
isDefault: name === defaultProfile,
|
|
188
185
|
status: 'skipped',
|
|
189
186
|
});
|
|
190
187
|
continue;
|
|
@@ -202,7 +199,6 @@ export function registerStatusCommand(program: Command): void {
|
|
|
202
199
|
statuses.push({
|
|
203
200
|
service,
|
|
204
201
|
profile: name,
|
|
205
|
-
isDefault: name === defaultProfile,
|
|
206
202
|
status: result.valid ? 'ok' : 'invalid',
|
|
207
203
|
info: result.info,
|
|
208
204
|
error: result.error,
|
|
@@ -244,13 +240,12 @@ export function registerStatusCommand(program: Command): void {
|
|
|
244
240
|
|
|
245
241
|
// Calculate column widths
|
|
246
242
|
const serviceWidth = Math.max(...statuses.map((s) => s.service.length));
|
|
247
|
-
const profileWidth = Math.max(...statuses.map((s) => s.profile.length
|
|
243
|
+
const profileWidth = Math.max(...statuses.map((s) => s.profile.length));
|
|
248
244
|
|
|
249
245
|
// Print each profile on one line
|
|
250
246
|
for (const s of statuses) {
|
|
251
247
|
const servicePad = s.service.padEnd(serviceWidth);
|
|
252
|
-
const
|
|
253
|
-
const profilePad = profileName.padEnd(profileWidth);
|
|
248
|
+
const profilePad = s.profile.padEnd(profileWidth);
|
|
254
249
|
|
|
255
250
|
let statusStr: string;
|
|
256
251
|
let details: string;
|
package/src/commands/telegram.ts
CHANGED
|
@@ -4,18 +4,16 @@ import { setProfile, getProfile } from '../config/config-manager';
|
|
|
4
4
|
import { createProfileCommands } from '../utils/profile-commands';
|
|
5
5
|
import { TelegramClient } from '../services/telegram/client';
|
|
6
6
|
import { CliError, handleError } from '../utils/errors';
|
|
7
|
-
import { readStdin, prompt
|
|
7
|
+
import { readStdin, prompt } from '../utils/stdin';
|
|
8
8
|
import type { TelegramCredentials, TelegramSendOptions } from '../types/telegram';
|
|
9
9
|
|
|
10
|
-
async function getTelegramClient(profileName
|
|
10
|
+
async function getTelegramClient(profileName: string): Promise<{ client: TelegramClient; profile: string }> {
|
|
11
11
|
const profile = await getProfile('telegram', profileName);
|
|
12
12
|
|
|
13
13
|
if (!profile) {
|
|
14
14
|
throw new CliError(
|
|
15
15
|
'PROFILE_NOT_FOUND',
|
|
16
|
-
profileName
|
|
17
|
-
? `Profile "${profileName}" not found for telegram`
|
|
18
|
-
: 'No default profile configured for telegram',
|
|
16
|
+
`Profile "${profileName}" not found for telegram`,
|
|
19
17
|
'Run: agentio telegram profile add'
|
|
20
18
|
);
|
|
21
19
|
}
|
|
@@ -44,7 +42,7 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
44
42
|
telegram
|
|
45
43
|
.command('send')
|
|
46
44
|
.description('Send a message to the channel')
|
|
47
|
-
.
|
|
45
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
48
46
|
.option('--parse-mode <mode>', 'Message format: html or markdown')
|
|
49
47
|
.option('--silent', 'Send without notification')
|
|
50
48
|
.argument('[message]', 'Message text (or pipe via stdin)')
|
|
@@ -92,11 +90,9 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
92
90
|
profile
|
|
93
91
|
.command('add')
|
|
94
92
|
.description('Add a new Telegram bot profile')
|
|
95
|
-
.option('--profile <name>', 'Profile name
|
|
93
|
+
.option('--profile <name>', 'Profile name (auto-detected from bot username if not provided)')
|
|
96
94
|
.action(async (options) => {
|
|
97
95
|
try {
|
|
98
|
-
const profileName = await resolveProfileName('telegram', options.profile);
|
|
99
|
-
|
|
100
96
|
console.error('\nTelegram Bot Setup\n');
|
|
101
97
|
|
|
102
98
|
// Step 1: Create bot
|
|
@@ -174,6 +170,9 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
174
170
|
console.error(' /setuserpic - Set bot photo');
|
|
175
171
|
console.error(' /setdescription - Set bot description\n');
|
|
176
172
|
|
|
173
|
+
// Auto-name based on bot username
|
|
174
|
+
const profileName = options.profile || botInfo.username;
|
|
175
|
+
|
|
177
176
|
// Save credentials
|
|
178
177
|
const credentials: TelegramCredentials = {
|
|
179
178
|
bot_token: botToken,
|
|
@@ -11,7 +11,6 @@ const ALL_SERVICES: ServiceName[] = ['gmail', 'gchat', 'jira', 'slack', 'telegra
|
|
|
11
11
|
|
|
12
12
|
const DEFAULT_CONFIG: Config = {
|
|
13
13
|
profiles: {},
|
|
14
|
-
defaults: {},
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
export async function ensureConfigDir(): Promise<void> {
|
|
@@ -50,21 +49,16 @@ export async function saveConfig(config: Config): Promise<void> {
|
|
|
50
49
|
|
|
51
50
|
export async function getProfile(
|
|
52
51
|
service: ServiceName,
|
|
53
|
-
profileName
|
|
52
|
+
profileName: string
|
|
54
53
|
): Promise<string | null> {
|
|
55
54
|
const config = await loadConfig();
|
|
56
|
-
const name = profileName || config.defaults[service];
|
|
57
|
-
|
|
58
|
-
if (!name) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
55
|
|
|
62
56
|
const serviceProfiles = config.profiles[service] || [];
|
|
63
|
-
if (!serviceProfiles.includes(
|
|
57
|
+
if (!serviceProfiles.includes(profileName)) {
|
|
64
58
|
return null;
|
|
65
59
|
}
|
|
66
60
|
|
|
67
|
-
return
|
|
61
|
+
return profileName;
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
export async function setProfile(
|
|
@@ -81,11 +75,6 @@ export async function setProfile(
|
|
|
81
75
|
config.profiles[service]!.push(profileName);
|
|
82
76
|
}
|
|
83
77
|
|
|
84
|
-
// Set as default if it's the first profile for this service
|
|
85
|
-
if (!config.defaults[service]) {
|
|
86
|
-
config.defaults[service] = profileName;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
78
|
await saveConfig(config);
|
|
90
79
|
}
|
|
91
80
|
|
|
@@ -102,27 +91,6 @@ export async function removeProfile(
|
|
|
102
91
|
|
|
103
92
|
config.profiles[service] = serviceProfiles.filter((p) => p !== profileName);
|
|
104
93
|
|
|
105
|
-
// Clear default if it was the removed profile
|
|
106
|
-
if (config.defaults[service] === profileName) {
|
|
107
|
-
config.defaults[service] = config.profiles[service]![0];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
await saveConfig(config);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function setDefault(
|
|
115
|
-
service: ServiceName,
|
|
116
|
-
profileName: string
|
|
117
|
-
): Promise<boolean> {
|
|
118
|
-
const config = await loadConfig();
|
|
119
|
-
|
|
120
|
-
const serviceProfiles = config.profiles[service] || [];
|
|
121
|
-
if (!serviceProfiles.includes(profileName)) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
config.defaults[service] = profileName;
|
|
126
94
|
await saveConfig(config);
|
|
127
95
|
return true;
|
|
128
96
|
}
|
|
@@ -130,7 +98,6 @@ export async function setDefault(
|
|
|
130
98
|
export async function listProfiles(service?: ServiceName): Promise<{
|
|
131
99
|
service: ServiceName;
|
|
132
100
|
profiles: string[];
|
|
133
|
-
default?: string;
|
|
134
101
|
}[]> {
|
|
135
102
|
const config = await loadConfig();
|
|
136
103
|
const services = service ? [service] : ALL_SERVICES;
|
|
@@ -138,7 +105,6 @@ export async function listProfiles(service?: ServiceName): Promise<{
|
|
|
138
105
|
return services.map((svc) => ({
|
|
139
106
|
service: svc,
|
|
140
107
|
profiles: config.profiles[svc] || [],
|
|
141
|
-
default: config.defaults[svc],
|
|
142
108
|
}));
|
|
143
109
|
}
|
|
144
110
|
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
GChatWebhookCredentials,
|
|
14
14
|
GChatOAuthCredentials,
|
|
15
15
|
GChatMessage,
|
|
16
|
+
GChatSpace,
|
|
16
17
|
} from '../../types/gchat';
|
|
17
18
|
|
|
18
19
|
export class GChatClient implements ServiceClient {
|
|
@@ -87,6 +88,17 @@ export class GChatClient implements ServiceClient {
|
|
|
87
88
|
return this.getViaOAuth(options);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
async listSpaces(): Promise<GChatSpace[]> {
|
|
92
|
+
if (this.credentials.type === 'webhook') {
|
|
93
|
+
throw new CliError(
|
|
94
|
+
'PERMISSION_DENIED',
|
|
95
|
+
'Listing spaces is not supported for webhook profiles',
|
|
96
|
+
'Use an OAuth profile to list spaces'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return this.listSpacesViaOAuth();
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
private async sendViaWebhook(options: GChatSendOptions): Promise<GChatSendResult> {
|
|
91
103
|
const webhookUrl = (this.credentials as GChatWebhookCredentials).webhookUrl;
|
|
92
104
|
|
|
@@ -194,9 +206,21 @@ export class GChatClient implements ServiceClient {
|
|
|
194
206
|
const chat = google.chat({ version: 'v1', auth });
|
|
195
207
|
|
|
196
208
|
try {
|
|
209
|
+
// Build filter from options
|
|
210
|
+
const filters: string[] = [];
|
|
211
|
+
if (options.threadId) {
|
|
212
|
+
filters.push(`thread.name = "spaces/${options.spaceId}/threads/${options.threadId}"`);
|
|
213
|
+
}
|
|
214
|
+
if (options.since) {
|
|
215
|
+
filters.push(`createTime > "${options.since.toISOString()}"`);
|
|
216
|
+
}
|
|
217
|
+
const filter = filters.length > 0 ? filters.join(' AND ') : undefined;
|
|
218
|
+
|
|
197
219
|
const response = await chat.spaces.messages.list({
|
|
198
220
|
parent: `spaces/${options.spaceId}`,
|
|
199
221
|
pageSize: options.limit || 10,
|
|
222
|
+
orderBy: 'createTime desc',
|
|
223
|
+
filter,
|
|
200
224
|
});
|
|
201
225
|
|
|
202
226
|
const messages = response.data.messages || [];
|
|
@@ -277,6 +301,32 @@ export class GChatClient implements ServiceClient {
|
|
|
277
301
|
}
|
|
278
302
|
}
|
|
279
303
|
|
|
304
|
+
private async listSpacesViaOAuth(): Promise<GChatSpace[]> {
|
|
305
|
+
const oauthCreds = this.credentials as GChatOAuthCredentials;
|
|
306
|
+
const auth = this.createOAuthClient(oauthCreds);
|
|
307
|
+
const chat = google.chat({ version: 'v1', auth });
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const response = await chat.spaces.list({});
|
|
311
|
+
|
|
312
|
+
const spaces = response.data.spaces || [];
|
|
313
|
+
return spaces.map((space: chat_v1.Schema$Space) => ({
|
|
314
|
+
name: space.name || '',
|
|
315
|
+
displayName: space.displayName || 'Unnamed',
|
|
316
|
+
type: (space.type as 'ROOM' | 'DM') || 'ROOM',
|
|
317
|
+
description: space.spaceDetails?.description || undefined,
|
|
318
|
+
}));
|
|
319
|
+
} catch (err) {
|
|
320
|
+
const code = this.getErrorCode(err);
|
|
321
|
+
const message = this.getErrorMessage(err);
|
|
322
|
+
throw new CliError(
|
|
323
|
+
code,
|
|
324
|
+
`Failed to list spaces: ${message}`,
|
|
325
|
+
'Check that OAuth token is valid and has Chat scope'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
280
330
|
private getErrorCode(err: unknown): ErrorCode {
|
|
281
331
|
if (err && typeof err === 'object') {
|
|
282
332
|
const error = err as Record<string, unknown>;
|
package/src/types/config.ts
CHANGED
|
@@ -9,16 +9,6 @@ export interface Config {
|
|
|
9
9
|
discourse?: string[];
|
|
10
10
|
sql?: string[];
|
|
11
11
|
};
|
|
12
|
-
defaults: {
|
|
13
|
-
gmail?: string;
|
|
14
|
-
gchat?: string;
|
|
15
|
-
github?: string;
|
|
16
|
-
jira?: string;
|
|
17
|
-
slack?: string;
|
|
18
|
-
telegram?: string;
|
|
19
|
-
discourse?: string;
|
|
20
|
-
sql?: string;
|
|
21
|
-
};
|
|
22
12
|
env?: Record<string, string>;
|
|
23
13
|
}
|
|
24
14
|
|
package/src/types/gchat.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface GChatOAuthCredentials {
|
|
|
42
42
|
expiryDate?: number;
|
|
43
43
|
tokenType: string;
|
|
44
44
|
scope?: string;
|
|
45
|
+
email: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export type GChatCredentials = GChatWebhookCredentials | GChatOAuthCredentials;
|
|
@@ -55,6 +56,8 @@ export interface GChatSendOptions {
|
|
|
55
56
|
export interface GChatListOptions {
|
|
56
57
|
spaceId: string;
|
|
57
58
|
limit?: number;
|
|
59
|
+
threadId?: string;
|
|
60
|
+
since?: Date;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
export interface GChatGetOptions {
|
package/src/utils/output.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GmailMessage, GmailAttachmentInfo } from '../types/gmail';
|
|
2
|
-
import type { GChatMessage } from '../types/gchat';
|
|
2
|
+
import type { GChatMessage, GChatSpace } from '../types/gchat';
|
|
3
3
|
import type { JiraProject, JiraIssue, JiraTransition, JiraCommentResult, JiraTransitionResult } from '../types/jira';
|
|
4
4
|
import type { SlackSendResult } from '../types/slack';
|
|
5
5
|
import type { RssFeed, RssArticle } from '../types/rss';
|
|
@@ -147,6 +147,25 @@ export function printGChatMessage(msg: GChatMessage): void {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
export function printGChatSpaceList(spaces: GChatSpace[]): void {
|
|
151
|
+
if (spaces.length === 0) {
|
|
152
|
+
console.log('No spaces found');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`Spaces (${spaces.length})\n`);
|
|
157
|
+
|
|
158
|
+
for (const space of spaces) {
|
|
159
|
+
const spaceId = space.name.replace('spaces/', '');
|
|
160
|
+
console.log(`[${space.type}] ${space.displayName}`);
|
|
161
|
+
console.log(` ID: ${spaceId}`);
|
|
162
|
+
if (space.description) {
|
|
163
|
+
console.log(` Description: ${space.description}`);
|
|
164
|
+
}
|
|
165
|
+
console.log('');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
150
169
|
// JIRA specific formatters
|
|
151
170
|
export function printJiraProjectList(projects: JiraProject[]): void {
|
|
152
171
|
if (projects.length === 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { listProfiles, removeProfile
|
|
2
|
+
import { listProfiles, removeProfile } from '../config/config-manager';
|
|
3
3
|
import { removeCredentials, getCredentials } from '../auth/token-store';
|
|
4
|
-
import { handleError
|
|
4
|
+
import { handleError } from './errors';
|
|
5
5
|
import type { ServiceName } from '../types/config';
|
|
6
6
|
|
|
7
7
|
export interface ProfileCommandsOptions<T> {
|
|
@@ -26,16 +26,15 @@ export function createProfileCommands<T>(
|
|
|
26
26
|
.action(async () => {
|
|
27
27
|
try {
|
|
28
28
|
const result = await listProfiles(service);
|
|
29
|
-
const { profiles
|
|
29
|
+
const { profiles } = result[0];
|
|
30
30
|
|
|
31
31
|
if (profiles.length === 0) {
|
|
32
32
|
console.log('No profiles configured');
|
|
33
33
|
} else {
|
|
34
34
|
for (const name of profiles) {
|
|
35
|
-
const marker = name === defaultProfile ? ' (default)' : '';
|
|
36
35
|
const credentials = await getCredentials<T>(service, name);
|
|
37
36
|
const extraInfo = getExtraInfo ? getExtraInfo(credentials) : '';
|
|
38
|
-
console.log(`${name}${
|
|
37
|
+
console.log(`${name}${extraInfo}`);
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
} catch (error) {
|
|
@@ -64,27 +63,5 @@ export function createProfileCommands<T>(
|
|
|
64
63
|
}
|
|
65
64
|
});
|
|
66
65
|
|
|
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
66
|
return profile;
|
|
90
67
|
}
|
package/src/utils/stdin.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { createInterface } from 'readline';
|
|
2
|
-
import { getProfile } from '../config/config-manager';
|
|
3
|
-
import type { ServiceName } from '../types/config';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Prompt the user for input with a question.
|
|
@@ -27,40 +25,6 @@ export async function confirm(question: string): Promise<boolean> {
|
|
|
27
25
|
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
/**
|
|
31
|
-
* Resolve profile name, checking for existing profiles and prompting for override.
|
|
32
|
-
* If the profile already exists, asks the user whether to override or choose a new name.
|
|
33
|
-
*/
|
|
34
|
-
export async function resolveProfileName(
|
|
35
|
-
service: ServiceName,
|
|
36
|
-
requestedName: string
|
|
37
|
-
): Promise<string> {
|
|
38
|
-
const existingProfile = await getProfile(service, requestedName);
|
|
39
|
-
|
|
40
|
-
if (!existingProfile) {
|
|
41
|
-
// Profile doesn't exist, use the requested name
|
|
42
|
-
return requestedName;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Profile exists, ask if user wants to override
|
|
46
|
-
console.error(`\nProfile "${requestedName}" already exists for ${service}.`);
|
|
47
|
-
const shouldOverride = await confirm('Do you want to override it?');
|
|
48
|
-
|
|
49
|
-
if (shouldOverride) {
|
|
50
|
-
return requestedName;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Ask for a new profile name
|
|
54
|
-
const newName = await prompt('Enter a new profile name: ');
|
|
55
|
-
|
|
56
|
-
if (!newName) {
|
|
57
|
-
throw new Error('Profile name is required');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Recursively check if the new name also exists
|
|
61
|
-
return resolveProfileName(service, newName);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
28
|
export async function readStdin(): Promise<string | null> {
|
|
65
29
|
// Check if stdin is a TTY (interactive terminal)
|
|
66
30
|
if (process.stdin.isTTY) {
|