@plosson/agentio 0.1.0

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.
@@ -0,0 +1,303 @@
1
+ import { Command } from 'commander';
2
+ import { basename } from 'path';
3
+ import { getValidTokens, createGoogleAuth } from '../auth/token-manager';
4
+ import { setCredentials, removeCredentials } from '../auth/token-store';
5
+ import { setProfile, removeProfile, listProfiles } from '../config/config-manager';
6
+ import { performOAuthFlow } from '../auth/oauth';
7
+ import { GmailClient } from '../services/gmail/client';
8
+ import { printMessageList, printMessage, printSendResult, printArchived, printMarked, raw } from '../utils/output';
9
+ import { CliError, handleError } from '../utils/errors';
10
+ import { readStdin } from '../utils/stdin';
11
+ import type { GmailAttachment } from '../types/gmail';
12
+
13
+ async function getGmailClient(profileName?: string): Promise<{ client: GmailClient; profile: string }> {
14
+ const { tokens, profile } = await getValidTokens('gmail', profileName);
15
+ const auth = createGoogleAuth(tokens);
16
+ return { client: new GmailClient(auth), profile };
17
+ }
18
+
19
+ export function registerGmailCommands(program: Command): void {
20
+ const gmail = program
21
+ .command('gmail')
22
+ .description('Gmail operations');
23
+
24
+ gmail
25
+ .command('list')
26
+ .description('List messages')
27
+ .option('--profile <name>', 'Profile name')
28
+ .option('--limit <n>', 'Number of messages', '10')
29
+ .option('--query <query>', 'Gmail search query (see "gmail search --help" for syntax)')
30
+ .option('--label <label>', 'Filter by label (repeatable)', (val, acc: string[]) => [...acc, val], [])
31
+ .action(async (options) => {
32
+ try {
33
+ const { client } = await getGmailClient(options.profile);
34
+ const result = await client.list({
35
+ limit: parseInt(options.limit, 10),
36
+ query: options.query,
37
+ labels: options.label.length ? options.label : undefined,
38
+ });
39
+ printMessageList(result.messages, result.total);
40
+ } catch (error) {
41
+ handleError(error);
42
+ }
43
+ });
44
+
45
+ gmail
46
+ .command('get <message-id>')
47
+ .description('Get a message')
48
+ .option('--profile <name>', 'Profile name')
49
+ .option('--format <format>', 'Body format: text, html, or raw', 'text')
50
+ .option('--body-only', 'Output only the message body')
51
+ .action(async (messageId: string, options) => {
52
+ try {
53
+ const { client } = await getGmailClient(options.profile);
54
+ const result = await client.get(messageId, options.format);
55
+ if (options.bodyOnly) {
56
+ raw(result.body);
57
+ } else {
58
+ printMessage(result);
59
+ }
60
+ } catch (error) {
61
+ handleError(error);
62
+ }
63
+ });
64
+
65
+ gmail
66
+ .command('search')
67
+ .description('Search messages using Gmail query syntax')
68
+ .requiredOption('--query <query>', 'Search query')
69
+ .option('--profile <name>', 'Profile name')
70
+ .option('--limit <n>', 'Max results', '10')
71
+ .addHelpText('after', `
72
+ Query Syntax Examples:
73
+
74
+ Keywords:
75
+ --query "meeting agenda" Messages containing both words
76
+ --query "exact phrase" Messages with exact phrase
77
+
78
+ From/To:
79
+ --query "from:john@example.com" Messages from specific sender
80
+ --query "to:me" Messages sent to you
81
+ --query "from:john to:jane" Combine sender and recipient
82
+ --query "cc:team@example.com" Messages where someone was CC'd
83
+
84
+ Date Ranges:
85
+ --query "after:2024/01/01" Messages after date (YYYY/MM/DD)
86
+ --query "before:2024/12/31" Messages before date
87
+ --query "after:2024/01/01 before:2024/06/30" Date range
88
+ --query "newer_than:7d" Last 7 days (d=days, m=months, y=years)
89
+ --query "older_than:1m" Older than 1 month
90
+
91
+ Labels:
92
+ --query "label:inbox" Messages in inbox
93
+ --query "label:important" Important messages
94
+ --query "label:work" Custom label (use exact label name)
95
+ --query "-label:spam" Exclude spam (- negates)
96
+
97
+ Status:
98
+ --query "is:unread" Unread messages
99
+ --query "is:starred" Starred messages
100
+ --query "is:important" Marked as important
101
+ --query "has:attachment" Messages with attachments
102
+
103
+ Subject:
104
+ --query "subject:invoice" Search in subject only
105
+
106
+ Combined:
107
+ --query "from:boss@work.com is:unread newer_than:7d"
108
+ --query "has:attachment from:client after:2024/01/01"
109
+ `)
110
+ .action(async (options) => {
111
+ try {
112
+ const { client } = await getGmailClient(options.profile);
113
+ const result = await client.search(options.query, parseInt(options.limit, 10));
114
+ printMessageList(result.messages, result.total);
115
+ } catch (error) {
116
+ handleError(error);
117
+ }
118
+ });
119
+
120
+ gmail
121
+ .command('send')
122
+ .description('Send an email')
123
+ .option('--profile <name>', 'Profile name')
124
+ .requiredOption('--to <email>', 'Recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
125
+ .option('--cc <email>', 'CC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
126
+ .option('--bcc <email>', 'BCC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
127
+ .requiredOption('--subject <subject>', 'Email subject')
128
+ .option('--body <body>', 'Email body (or pipe via stdin)')
129
+ .option('--html', 'Treat body as HTML')
130
+ .option('--attachment <path>', 'File to attach (repeatable)', (val, acc: string[]) => [...acc, val], [])
131
+ .action(async (options) => {
132
+ try {
133
+ let body = options.body;
134
+
135
+ // Check for stdin if no body provided
136
+ if (!body) {
137
+ body = await readStdin();
138
+ }
139
+
140
+ if (!body) {
141
+ throw new CliError('INVALID_PARAMS', 'Body is required. Use --body or pipe via stdin.');
142
+ }
143
+
144
+ // Process attachments
145
+ const attachments: GmailAttachment[] | undefined = options.attachment.length
146
+ ? options.attachment.map((path: string) => ({
147
+ path,
148
+ filename: basename(path),
149
+ }))
150
+ : undefined;
151
+
152
+ const { client } = await getGmailClient(options.profile);
153
+ const result = await client.send({
154
+ to: options.to,
155
+ cc: options.cc.length ? options.cc : undefined,
156
+ bcc: options.bcc.length ? options.bcc : undefined,
157
+ subject: options.subject,
158
+ body,
159
+ isHtml: options.html,
160
+ attachments,
161
+ });
162
+ printSendResult(result);
163
+ } catch (error) {
164
+ handleError(error);
165
+ }
166
+ });
167
+
168
+ gmail
169
+ .command('reply')
170
+ .description('Reply to a thread')
171
+ .option('--profile <name>', 'Profile name')
172
+ .requiredOption('--thread-id <id>', 'Thread ID')
173
+ .option('--body <body>', 'Reply body (or pipe via stdin)')
174
+ .option('--html', 'Treat body as HTML')
175
+ .action(async (options) => {
176
+ try {
177
+ let body = options.body;
178
+
179
+ if (!body) {
180
+ body = await readStdin();
181
+ }
182
+
183
+ if (!body) {
184
+ throw new CliError('INVALID_PARAMS', 'Body is required. Use --body or pipe via stdin.');
185
+ }
186
+
187
+ const { client } = await getGmailClient(options.profile);
188
+ const result = await client.reply({
189
+ threadId: options.threadId,
190
+ body,
191
+ isHtml: options.html,
192
+ });
193
+ printSendResult(result);
194
+ } catch (error) {
195
+ handleError(error);
196
+ }
197
+ });
198
+
199
+ gmail
200
+ .command('archive <message-id>')
201
+ .description('Archive a message')
202
+ .option('--profile <name>', 'Profile name')
203
+ .action(async (messageId: string, options) => {
204
+ try {
205
+ const { client } = await getGmailClient(options.profile);
206
+ await client.archive(messageId);
207
+ printArchived(messageId);
208
+ } catch (error) {
209
+ handleError(error);
210
+ }
211
+ });
212
+
213
+ gmail
214
+ .command('mark <message-id>')
215
+ .description('Mark message as read or unread')
216
+ .option('--profile <name>', 'Profile name')
217
+ .option('--read', 'Mark as read')
218
+ .option('--unread', 'Mark as unread')
219
+ .action(async (messageId: string, options) => {
220
+ try {
221
+ if (!options.read && !options.unread) {
222
+ throw new CliError('INVALID_PARAMS', 'Specify --read or --unread');
223
+ }
224
+ if (options.read && options.unread) {
225
+ throw new CliError('INVALID_PARAMS', 'Cannot specify both --read and --unread');
226
+ }
227
+
228
+ const { client } = await getGmailClient(options.profile);
229
+ await client.mark(messageId, options.read);
230
+ printMarked(messageId, options.read);
231
+ } catch (error) {
232
+ handleError(error);
233
+ }
234
+ });
235
+
236
+ // Profile management
237
+ const profile = gmail
238
+ .command('profile')
239
+ .description('Manage Gmail profiles');
240
+
241
+ profile
242
+ .command('add')
243
+ .description('Add a new Gmail profile')
244
+ .option('--profile <name>', 'Profile name', 'default')
245
+ .action(async (options) => {
246
+ try {
247
+ const profileName = options.profile;
248
+
249
+ console.error(`Starting OAuth flow for Gmail profile "${profileName}"...`);
250
+
251
+ const tokens = await performOAuthFlow('gmail');
252
+
253
+ await setProfile('gmail', profileName);
254
+ await setCredentials('gmail', profileName, tokens);
255
+
256
+ console.error(`\nSuccess! Profile "${profileName}" for Gmail is now configured.`);
257
+ } catch (error) {
258
+ handleError(error);
259
+ }
260
+ });
261
+
262
+ profile
263
+ .command('list')
264
+ .description('List Gmail profiles')
265
+ .action(async () => {
266
+ try {
267
+ const result = await listProfiles('gmail');
268
+ const { profiles, default: defaultProfile } = result[0];
269
+
270
+ if (profiles.length === 0) {
271
+ console.log('No profiles configured');
272
+ } else {
273
+ for (const name of profiles) {
274
+ const marker = name === defaultProfile ? ' (default)' : '';
275
+ console.log(`${name}${marker}`);
276
+ }
277
+ }
278
+ } catch (error) {
279
+ handleError(error);
280
+ }
281
+ });
282
+
283
+ profile
284
+ .command('remove')
285
+ .description('Remove a Gmail profile')
286
+ .requiredOption('--profile <name>', 'Profile name')
287
+ .action(async (options) => {
288
+ try {
289
+ const profileName = options.profile;
290
+
291
+ const removed = await removeProfile('gmail', profileName);
292
+ await removeCredentials('gmail', profileName);
293
+
294
+ if (removed) {
295
+ console.error(`Removed profile "${profileName}"`);
296
+ } else {
297
+ console.error(`Profile "${profileName}" not found`);
298
+ }
299
+ } catch (error) {
300
+ handleError(error);
301
+ }
302
+ });
303
+ }
@@ -0,0 +1,247 @@
1
+ import { Command } from 'commander';
2
+ import { createInterface } from 'readline';
3
+ import { setCredentials, removeCredentials, getCredentials } from '../auth/token-store';
4
+ import { setProfile, removeProfile, listProfiles, getProfile } from '../config/config-manager';
5
+ import { TelegramClient } from '../services/telegram/client';
6
+ import { CliError, handleError } from '../utils/errors';
7
+ import { readStdin } from '../utils/stdin';
8
+ import type { TelegramCredentials, TelegramSendOptions } from '../types/telegram';
9
+
10
+ function prompt(question: string): Promise<string> {
11
+ const rl = createInterface({
12
+ input: process.stdin,
13
+ output: process.stderr,
14
+ });
15
+
16
+ return new Promise((resolve) => {
17
+ rl.question(question, (answer) => {
18
+ rl.close();
19
+ resolve(answer.trim());
20
+ });
21
+ });
22
+ }
23
+
24
+ async function getTelegramClient(profileName?: string): Promise<{ client: TelegramClient; profile: string }> {
25
+ const profile = await getProfile('telegram', profileName);
26
+
27
+ if (!profile) {
28
+ throw new CliError(
29
+ 'PROFILE_NOT_FOUND',
30
+ profileName
31
+ ? `Profile "${profileName}" not found for telegram`
32
+ : 'No default profile configured for telegram',
33
+ 'Run: agentio telegram profile add'
34
+ );
35
+ }
36
+
37
+ const credentials = await getCredentials<TelegramCredentials>('telegram', profile);
38
+
39
+ if (!credentials) {
40
+ throw new CliError(
41
+ 'AUTH_FAILED',
42
+ `No credentials found for telegram profile "${profile}"`,
43
+ `Run: agentio telegram profile add --profile ${profile}`
44
+ );
45
+ }
46
+
47
+ return {
48
+ client: new TelegramClient(credentials.bot_token, credentials.channel_id),
49
+ profile,
50
+ };
51
+ }
52
+
53
+ export function registerTelegramCommands(program: Command): void {
54
+ const telegram = program
55
+ .command('telegram')
56
+ .description('Telegram operations');
57
+
58
+ telegram
59
+ .command('send')
60
+ .description('Send a message to the channel')
61
+ .option('--profile <name>', 'Profile name')
62
+ .option('--parse-mode <mode>', 'Message format: html or markdown')
63
+ .option('--silent', 'Send without notification')
64
+ .argument('[message]', 'Message text (or pipe via stdin)')
65
+ .action(async (message: string | undefined, options) => {
66
+ try {
67
+ let text = message;
68
+
69
+ if (!text) {
70
+ text = await readStdin() || undefined;
71
+ }
72
+
73
+ if (!text) {
74
+ throw new CliError('INVALID_PARAMS', 'Message is required. Provide as argument or pipe via stdin.');
75
+ }
76
+
77
+ const sendOptions: TelegramSendOptions = {};
78
+ if (options.parseMode) {
79
+ const mode = options.parseMode.toLowerCase();
80
+ if (mode === 'html') sendOptions.parse_mode = 'HTML';
81
+ else if (mode === 'markdown') sendOptions.parse_mode = 'MarkdownV2';
82
+ else throw new CliError('INVALID_PARAMS', 'parse-mode must be "html" or "markdown"');
83
+ }
84
+ if (options.silent) {
85
+ sendOptions.disable_notification = true;
86
+ }
87
+
88
+ const { client } = await getTelegramClient(options.profile);
89
+ const result = await client.sendMessage(text, sendOptions);
90
+
91
+ console.log('Message sent');
92
+ console.log(`ID: ${result.message_id}`);
93
+ console.log(`Chat: ${result.chat.title || result.chat.id}`);
94
+ } catch (error) {
95
+ handleError(error);
96
+ }
97
+ });
98
+
99
+ // Profile management
100
+ const profile = telegram
101
+ .command('profile')
102
+ .description('Manage Telegram profiles');
103
+
104
+ profile
105
+ .command('add')
106
+ .description('Add a new Telegram bot profile')
107
+ .option('--profile <name>', 'Profile name', 'default')
108
+ .action(async (options) => {
109
+ try {
110
+ const profileName = options.profile;
111
+
112
+ console.error('\n📱 Telegram Bot Setup\n');
113
+
114
+ // Step 1: Create bot
115
+ console.error('Step 1: Create your bot');
116
+ console.error(' Open Telegram and message @BotFather');
117
+ console.error(' → https://t.me/BotFather\n');
118
+ console.error(' Send these commands:');
119
+ console.error(' /newbot');
120
+ console.error(' → Enter a display name (e.g., "My Announcements Bot")');
121
+ console.error(' → Enter a username ending in "bot" (e.g., "my_announce_bot")\n');
122
+ console.error(' BotFather will give you a token like:');
123
+ console.error(' 123456789:ABCdefGHIjklMNOpqrsTUVwxyz\n');
124
+
125
+ const botToken = await prompt('? Paste your bot token: ');
126
+
127
+ if (!botToken) {
128
+ throw new CliError('INVALID_PARAMS', 'Bot token is required');
129
+ }
130
+
131
+ // Validate token
132
+ const tempClient = new TelegramClient(botToken, '');
133
+ let botInfo;
134
+ try {
135
+ botInfo = await tempClient.getMe();
136
+ } catch (error) {
137
+ if (error instanceof CliError && error.code === 'AUTH_FAILED') {
138
+ throw new CliError('AUTH_FAILED', 'Invalid bot token. Please check and try again.');
139
+ }
140
+ throw error;
141
+ }
142
+
143
+ console.error(`\n✓ Bot verified: @${botInfo.username}\n`);
144
+
145
+ // Step 2: Add bot to channel
146
+ console.error('Step 2: Add bot to your channel');
147
+ console.error(' 1. Open your Telegram channel');
148
+ console.error(' 2. Go to Channel Settings → Administrators');
149
+ console.error(` 3. Add @${botInfo.username} as admin with "Post Messages" permission\n`);
150
+
151
+ console.error(' How to find your channel ID:');
152
+ console.error(' • Public channel: Use @username (e.g., @mychannel)');
153
+ console.error(' • Private channel: Forward any message from the channel to @userinfobot');
154
+ console.error(' The bot will reply with the channel ID (starts with -100)\n');
155
+
156
+ const channelId = await prompt('? Enter channel ID: ');
157
+
158
+ if (!channelId) {
159
+ throw new CliError('INVALID_PARAMS', 'Channel ID is required');
160
+ }
161
+
162
+ // Validate channel access
163
+ const client = new TelegramClient(botToken, channelId);
164
+ let chatInfo;
165
+ try {
166
+ chatInfo = await client.getChat();
167
+ } catch (error) {
168
+ if (error instanceof CliError) {
169
+ if (error.code === 'NOT_FOUND') {
170
+ throw new CliError('NOT_FOUND', `Channel "${channelId}" not found. Check the channel ID or username.`);
171
+ }
172
+ if (error.code === 'PERMISSION_DENIED') {
173
+ throw new CliError('PERMISSION_DENIED', `Bot cannot access "${channelId}". Make sure it's added as an admin.`);
174
+ }
175
+ }
176
+ throw error;
177
+ }
178
+
179
+ const channelName = chatInfo.title || chatInfo.username || channelId;
180
+ console.error(`\n✓ Channel verified: ${channelName}`);
181
+ console.error('✓ Bot can post to this channel\n');
182
+
183
+ // Step 3: Optional customization tips
184
+ console.error('Step 3: Customize your bot (optional)');
185
+ console.error(' You can set a profile photo and description in @BotFather:');
186
+ console.error(' /setuserpic - Set bot photo');
187
+ console.error(' /setdescription - Set bot description\n');
188
+
189
+ // Save credentials
190
+ const credentials: TelegramCredentials = {
191
+ bot_token: botToken,
192
+ channel_id: channelId,
193
+ bot_username: botInfo.username,
194
+ };
195
+
196
+ await setProfile('telegram', profileName);
197
+ await setCredentials('telegram', profileName, credentials);
198
+
199
+ console.error(`✅ Profile "${profileName}" configured!`);
200
+ console.error(` Test with: agentio telegram send --profile ${profileName} "Hello world"`);
201
+ } catch (error) {
202
+ handleError(error);
203
+ }
204
+ });
205
+
206
+ profile
207
+ .command('list')
208
+ .description('List Telegram profiles')
209
+ .action(async () => {
210
+ try {
211
+ const result = await listProfiles('telegram');
212
+ const { profiles, default: defaultProfile } = result[0];
213
+
214
+ if (profiles.length === 0) {
215
+ console.log('No profiles configured');
216
+ } else {
217
+ for (const name of profiles) {
218
+ const marker = name === defaultProfile ? ' (default)' : '';
219
+ console.log(`${name}${marker}`);
220
+ }
221
+ }
222
+ } catch (error) {
223
+ handleError(error);
224
+ }
225
+ });
226
+
227
+ profile
228
+ .command('remove')
229
+ .description('Remove a Telegram profile')
230
+ .requiredOption('--profile <name>', 'Profile name')
231
+ .action(async (options) => {
232
+ try {
233
+ const profileName = options.profile;
234
+
235
+ const removed = await removeProfile('telegram', profileName);
236
+ await removeCredentials('telegram', profileName);
237
+
238
+ if (removed) {
239
+ console.error(`Removed profile "${profileName}"`);
240
+ } else {
241
+ console.error(`Profile "${profileName}" not found`);
242
+ }
243
+ } catch (error) {
244
+ handleError(error);
245
+ }
246
+ });
247
+ }
@@ -0,0 +1,127 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { mkdir, readFile, writeFile } from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import type { Config, ServiceName } from '../types/config';
6
+
7
+ const CONFIG_DIR = join(homedir(), '.config', 'agentio');
8
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
9
+
10
+ const DEFAULT_CONFIG: Config = {
11
+ profiles: {},
12
+ defaults: {},
13
+ };
14
+
15
+ export async function ensureConfigDir(): Promise<void> {
16
+ if (!existsSync(CONFIG_DIR)) {
17
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
18
+ }
19
+ }
20
+
21
+ export async function loadConfig(): Promise<Config> {
22
+ await ensureConfigDir();
23
+
24
+ if (!existsSync(CONFIG_FILE)) {
25
+ await saveConfig(DEFAULT_CONFIG);
26
+ return DEFAULT_CONFIG;
27
+ }
28
+
29
+ try {
30
+ const content = await readFile(CONFIG_FILE, 'utf-8');
31
+ return JSON.parse(content) as Config;
32
+ } catch {
33
+ // Config file corrupted, back it up and return default
34
+ const backupPath = `${CONFIG_FILE}.backup`;
35
+ const content = await readFile(CONFIG_FILE, 'utf-8').catch(() => '');
36
+ if (content) {
37
+ await writeFile(backupPath, content).catch(() => {});
38
+ }
39
+ await saveConfig(DEFAULT_CONFIG);
40
+ return DEFAULT_CONFIG;
41
+ }
42
+ }
43
+
44
+ export async function saveConfig(config: Config): Promise<void> {
45
+ await ensureConfigDir();
46
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
47
+ }
48
+
49
+ export async function getProfile(
50
+ service: ServiceName,
51
+ profileName?: string
52
+ ): Promise<string | null> {
53
+ const config = await loadConfig();
54
+ const name = profileName || config.defaults[service];
55
+
56
+ if (!name) {
57
+ return null;
58
+ }
59
+
60
+ const serviceProfiles = config.profiles[service] || [];
61
+ if (!serviceProfiles.includes(name)) {
62
+ return null;
63
+ }
64
+
65
+ return name;
66
+ }
67
+
68
+ export async function setProfile(
69
+ service: ServiceName,
70
+ profileName: string
71
+ ): Promise<void> {
72
+ const config = await loadConfig();
73
+
74
+ if (!config.profiles[service]) {
75
+ config.profiles[service] = [];
76
+ }
77
+
78
+ if (!config.profiles[service]!.includes(profileName)) {
79
+ config.profiles[service]!.push(profileName);
80
+ }
81
+
82
+ // Set as default if it's the first profile for this service
83
+ if (!config.defaults[service]) {
84
+ config.defaults[service] = profileName;
85
+ }
86
+
87
+ await saveConfig(config);
88
+ }
89
+
90
+ export async function removeProfile(
91
+ service: ServiceName,
92
+ profileName: string
93
+ ): Promise<boolean> {
94
+ const config = await loadConfig();
95
+
96
+ const serviceProfiles = config.profiles[service];
97
+ if (!serviceProfiles || !serviceProfiles.includes(profileName)) {
98
+ return false;
99
+ }
100
+
101
+ config.profiles[service] = serviceProfiles.filter((p) => p !== profileName);
102
+
103
+ // Clear default if it was the removed profile
104
+ if (config.defaults[service] === profileName) {
105
+ config.defaults[service] = config.profiles[service]![0];
106
+ }
107
+
108
+ await saveConfig(config);
109
+ return true;
110
+ }
111
+
112
+ export async function listProfiles(service?: ServiceName): Promise<{
113
+ service: ServiceName;
114
+ profiles: string[];
115
+ default?: string;
116
+ }[]> {
117
+ const config = await loadConfig();
118
+ const services: ServiceName[] = service ? [service] : ['gmail', 'gchat', 'jira', 'telegram'];
119
+
120
+ return services.map((svc) => ({
121
+ service: svc,
122
+ profiles: config.profiles[svc] || [],
123
+ default: config.defaults[svc],
124
+ }));
125
+ }
126
+
127
+ export { CONFIG_DIR, CONFIG_FILE };
@@ -0,0 +1,7 @@
1
+ // Embedded OAuth credentials for agentio
2
+ // These are "public" credentials for a desktop/CLI app - this is standard practice
3
+
4
+ export const GOOGLE_OAUTH_CONFIG = {
5
+ clientId: '125936797748-gju6s8niabdqtp3bnmoapsp5gou1vekb.apps.googleusercontent.com',
6
+ clientSecret: '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',
7
+ };
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from 'commander';
3
+ import { registerGmailCommands } from './commands/gmail';
4
+ import { registerTelegramCommands } from './commands/telegram';
5
+
6
+ const program = new Command();
7
+
8
+ program
9
+ .name('agentio')
10
+ .description('CLI for LLM agents to interact with communication and tracking services')
11
+ .version('0.1.0');
12
+
13
+ registerGmailCommands(program);
14
+ registerTelegramCommands(program);
15
+
16
+ program.parse();