@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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/package.json +52 -0
- package/src/auth/oauth.ts +132 -0
- package/src/auth/token-manager.ts +104 -0
- package/src/auth/token-store.ts +114 -0
- package/src/commands/gmail.ts +303 -0
- package/src/commands/telegram.ts +247 -0
- package/src/config/config-manager.ts +127 -0
- package/src/config/credentials.ts +7 -0
- package/src/index.ts +16 -0
- package/src/services/gmail/client.ts +377 -0
- package/src/services/telegram/client.ts +81 -0
- package/src/types/config.ts +16 -0
- package/src/types/gmail.ts +40 -0
- package/src/types/telegram.ts +34 -0
- package/src/types/tokens.ts +13 -0
- package/src/utils/errors.ts +60 -0
- package/src/utils/output.ts +54 -0
- package/src/utils/stdin.ts +18 -0
|
@@ -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();
|