@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 agentio contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # agentio
2
+
3
+ CLI for LLM agents to interact with communication and tracking services.
4
+
5
+ ## Installation
6
+
7
+ ### Via npm/bun (recommended)
8
+
9
+ ```bash
10
+ # Using bun
11
+ bunx agentio --help
12
+
13
+ # Using npm
14
+ npx agentio --help
15
+
16
+ # Global install
17
+ bun add -g agentio
18
+ # or
19
+ npm install -g agentio
20
+ ```
21
+
22
+ ### Native binaries
23
+
24
+ Download from [GitHub Releases](https://github.com/plosson/agentio/releases):
25
+
26
+ | Platform | Binary |
27
+ |----------|--------|
28
+ | macOS Intel | `agentio-darwin-x64` |
29
+ | macOS Apple Silicon | `agentio-darwin-arm64` |
30
+ | Linux x64 | `agentio-linux-x64` |
31
+ | Linux ARM64 | `agentio-linux-arm64` |
32
+ | Windows x64 | `agentio-windows-x64.exe` |
33
+
34
+ ## Services
35
+
36
+ | Service | Status | Commands |
37
+ |---------|--------|----------|
38
+ | Gmail | Available | `list`, `get`, `search`, `send`, `reply`, `archive`, `mark` |
39
+ | Telegram | Available | `send` |
40
+ | Slack | Planned | - |
41
+ | JIRA | Planned | - |
42
+ | Linear | Planned | - |
43
+
44
+ ## Usage
45
+
46
+ ### Gmail
47
+
48
+ ```bash
49
+ # First, authenticate
50
+ agentio gmail profile add
51
+
52
+ # List recent emails
53
+ agentio gmail list --limit 10
54
+
55
+ # Search emails
56
+ agentio gmail search --query "from:boss@company.com is:unread"
57
+
58
+ # Get a specific email
59
+ agentio gmail get <message-id>
60
+
61
+ # Send an email
62
+ agentio gmail send --to user@example.com --subject "Hello" --body "Message body"
63
+
64
+ # Or pipe content
65
+ echo "Message body" | agentio gmail send --to user@example.com --subject "Hello"
66
+ ```
67
+
68
+ ### Telegram
69
+
70
+ ```bash
71
+ # Set up bot profile (interactive wizard)
72
+ agentio telegram profile add
73
+
74
+ # Send message to channel
75
+ agentio telegram send "Hello from agentio!"
76
+
77
+ # Send with formatting
78
+ agentio telegram send --parse-mode markdown "**Bold** and _italic_"
79
+ ```
80
+
81
+ ## Multi-Profile Support
82
+
83
+ Each service supports multiple named profiles:
84
+
85
+ ```bash
86
+ # Add profiles for different accounts
87
+ agentio gmail profile add --profile work
88
+ agentio gmail profile add --profile personal
89
+
90
+ # Use specific profile
91
+ agentio gmail list --profile work
92
+ ```
93
+
94
+ ## Design
95
+
96
+ agentio is designed for LLM consumption:
97
+
98
+ - **Structured output**: Human-readable text output optimized for LLM parsing
99
+ - **Clear errors**: Error messages written to stderr with suggestions
100
+ - **Stdin support**: Pipe content to commands that accept body text
101
+ - **Multi-profile**: Manage multiple accounts per service
102
+
103
+ ## Configuration
104
+
105
+ Configuration is stored in `~/.config/agentio/`:
106
+
107
+ - `config.json` - Profile names and defaults
108
+ - `tokens.enc` - Encrypted credentials (AES-256-GCM)
109
+
110
+ ## License
111
+
112
+ MIT
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@plosson/agentio",
3
+ "version": "0.1.0",
4
+ "description": "CLI for LLM agents to interact with communication and tracking services",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "plosson",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/plosson/agentio.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/plosson/agentio/issues"
14
+ },
15
+ "homepage": "https://github.com/plosson/agentio#readme",
16
+ "keywords": [
17
+ "cli",
18
+ "llm",
19
+ "agent",
20
+ "gmail",
21
+ "telegram",
22
+ "automation",
23
+ "ai",
24
+ "mcp"
25
+ ],
26
+ "bin": {
27
+ "agentio": "./src/index.ts"
28
+ },
29
+ "files": [
30
+ "src",
31
+ "LICENSE",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "dev": "bun run src/index.ts",
36
+ "build": "bun build src/index.ts --outdir dist --target node",
37
+ "build:native": "bun build src/index.ts --compile --outfile dist/agentio",
38
+ "typecheck": "tsc --noEmit"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "latest",
45
+ "@types/node": "^25.0.3",
46
+ "typescript": "^5.9.3"
47
+ },
48
+ "dependencies": {
49
+ "commander": "^14.0.2",
50
+ "googleapis": "^169.0.0"
51
+ }
52
+ }
@@ -0,0 +1,132 @@
1
+ import { createServer, type Server } from 'http';
2
+ import { URL } from 'url';
3
+ import { google } from 'googleapis';
4
+ import { GOOGLE_OAUTH_CONFIG } from '../config/credentials';
5
+ import type { OAuthTokens } from '../types/tokens';
6
+
7
+ const GMAIL_SCOPES = [
8
+ 'https://www.googleapis.com/auth/gmail.modify',
9
+ 'https://www.googleapis.com/auth/gmail.send',
10
+ ];
11
+
12
+ const PORT_RANGE_START = 3000;
13
+ const PORT_RANGE_END = 3010;
14
+
15
+ async function findAvailablePort(): Promise<number> {
16
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
17
+ try {
18
+ await new Promise<void>((resolve, reject) => {
19
+ const server = createServer();
20
+ server.listen(port, () => {
21
+ server.close(() => resolve());
22
+ });
23
+ server.on('error', reject);
24
+ });
25
+ return port;
26
+ } catch {
27
+ continue;
28
+ }
29
+ }
30
+ throw new Error(`No available port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
31
+ }
32
+
33
+ export async function performOAuthFlow(
34
+ service: 'gmail' | 'gchat'
35
+ ): Promise<OAuthTokens> {
36
+ const port = await findAvailablePort();
37
+ const redirectUri = `http://localhost:${port}/callback`;
38
+
39
+ const oauth2Client = new google.auth.OAuth2(
40
+ GOOGLE_OAUTH_CONFIG.clientId,
41
+ GOOGLE_OAUTH_CONFIG.clientSecret,
42
+ redirectUri
43
+ );
44
+
45
+ const scopes = service === 'gmail' ? GMAIL_SCOPES : [];
46
+
47
+ const authUrl = oauth2Client.generateAuthUrl({
48
+ access_type: 'offline',
49
+ scope: scopes,
50
+ prompt: 'consent',
51
+ });
52
+
53
+ return new Promise((resolve, reject) => {
54
+ let server: Server;
55
+
56
+ const timeout = setTimeout(() => {
57
+ server?.close();
58
+ reject(new Error('OAuth flow timed out after 5 minutes'));
59
+ }, 5 * 60 * 1000);
60
+
61
+ server = createServer(async (req, res) => {
62
+ const url = new URL(req.url || '', `http://localhost:${port}`);
63
+
64
+ if (url.pathname !== '/callback') {
65
+ res.writeHead(404);
66
+ res.end('Not found');
67
+ return;
68
+ }
69
+
70
+ const code = url.searchParams.get('code');
71
+ const error = url.searchParams.get('error');
72
+
73
+ if (error) {
74
+ res.writeHead(200, { 'Content-Type': 'text/html' });
75
+ res.end('<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>');
76
+ clearTimeout(timeout);
77
+ server.close();
78
+ reject(new Error(`OAuth error: ${error}`));
79
+ return;
80
+ }
81
+
82
+ if (!code) {
83
+ res.writeHead(400, { 'Content-Type': 'text/html' });
84
+ res.end('<html><body><h1>Missing Authorization Code</h1><p>You can close this window.</p></body></html>');
85
+ clearTimeout(timeout);
86
+ server.close();
87
+ reject(new Error('Missing authorization code in OAuth callback'));
88
+ return;
89
+ }
90
+
91
+ try {
92
+ const { tokens } = await oauth2Client.getToken(code);
93
+
94
+ res.writeHead(200, { 'Content-Type': 'text/html' });
95
+ res.end('<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
96
+
97
+ clearTimeout(timeout);
98
+ server.close();
99
+
100
+ resolve({
101
+ access_token: tokens.access_token!,
102
+ refresh_token: tokens.refresh_token || undefined,
103
+ expiry_date: tokens.expiry_date || undefined,
104
+ token_type: tokens.token_type || 'Bearer',
105
+ scope: tokens.scope || undefined,
106
+ });
107
+ } catch (err) {
108
+ res.writeHead(500);
109
+ res.end('Failed to exchange authorization code');
110
+ clearTimeout(timeout);
111
+ server.close();
112
+ reject(err);
113
+ }
114
+ });
115
+
116
+ server.listen(port, () => {
117
+ console.error(`\nOpening browser for authorization...`);
118
+ console.error(`If browser doesn't open, visit:\n${authUrl}\n`);
119
+
120
+ // Open browser
121
+ const open = process.platform === 'darwin' ? 'open' :
122
+ process.platform === 'win32' ? 'start' : 'xdg-open';
123
+ Bun.spawn([open, authUrl], { stdout: 'ignore', stderr: 'ignore' });
124
+ });
125
+
126
+ server.on('error', (err) => {
127
+ clearTimeout(timeout);
128
+ server?.close();
129
+ reject(err);
130
+ });
131
+ });
132
+ }
@@ -0,0 +1,104 @@
1
+ import { google } from 'googleapis';
2
+ import { getCredentials, setCredentials } from './token-store';
3
+ import { getProfile } from '../config/config-manager';
4
+ import { GOOGLE_OAUTH_CONFIG } from '../config/credentials';
5
+ import { CliError } from '../utils/errors';
6
+ import type { ServiceName } from '../types/config';
7
+ import type { OAuthTokens } from '../types/tokens';
8
+
9
+ const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
10
+
11
+ export async function getValidTokens(
12
+ service: ServiceName,
13
+ profileName?: string
14
+ ): Promise<{ tokens: OAuthTokens; profile: string }> {
15
+ const profile = await getProfile(service, profileName);
16
+
17
+ if (!profile) {
18
+ throw new CliError(
19
+ 'PROFILE_NOT_FOUND',
20
+ profileName
21
+ ? `Profile "${profileName}" not found for ${service}`
22
+ : `No default profile configured for ${service}`,
23
+ `Run: agentio ${service} profile add`
24
+ );
25
+ }
26
+
27
+ const tokens = await getCredentials<OAuthTokens>(service, profile);
28
+
29
+ if (!tokens) {
30
+ throw new CliError(
31
+ 'AUTH_FAILED',
32
+ `No tokens found for ${service} profile "${profile}"`,
33
+ `Run: agentio ${service} profile add --profile ${profile}`
34
+ );
35
+ }
36
+
37
+ // Check if token needs refresh
38
+ if (tokens.expiry_date && Date.now() > tokens.expiry_date - TOKEN_EXPIRY_BUFFER_MS) {
39
+ if (!tokens.refresh_token) {
40
+ throw new CliError(
41
+ 'TOKEN_EXPIRED',
42
+ 'Access token expired and no refresh token available',
43
+ `Run: agentio ${service} profile add --profile ${profile}`
44
+ );
45
+ }
46
+
47
+ const refreshed = await refreshTokens(service, profile, tokens);
48
+ return { tokens: refreshed, profile };
49
+ }
50
+
51
+ return { tokens, profile };
52
+ }
53
+
54
+ async function refreshTokens(
55
+ service: ServiceName,
56
+ profileName: string,
57
+ tokens: OAuthTokens
58
+ ): Promise<OAuthTokens> {
59
+ const oauth2Client = new google.auth.OAuth2(
60
+ GOOGLE_OAUTH_CONFIG.clientId,
61
+ GOOGLE_OAUTH_CONFIG.clientSecret
62
+ );
63
+
64
+ oauth2Client.setCredentials({
65
+ refresh_token: tokens.refresh_token,
66
+ });
67
+
68
+ try {
69
+ const { credentials } = await oauth2Client.refreshAccessToken();
70
+
71
+ const newTokens: OAuthTokens = {
72
+ access_token: credentials.access_token!,
73
+ refresh_token: credentials.refresh_token || tokens.refresh_token,
74
+ expiry_date: credentials.expiry_date || undefined,
75
+ token_type: credentials.token_type || 'Bearer',
76
+ scope: credentials.scope || tokens.scope,
77
+ };
78
+
79
+ await setCredentials(service, profileName, newTokens);
80
+ return newTokens;
81
+ } catch (error) {
82
+ const message = error instanceof Error ? error.message : 'Unknown error';
83
+ throw new CliError(
84
+ 'TOKEN_EXPIRED',
85
+ `Failed to refresh access token: ${message}`,
86
+ `Run: agentio ${service} profile add --profile ${profileName}`
87
+ );
88
+ }
89
+ }
90
+
91
+ export function createGoogleAuth(tokens: OAuthTokens) {
92
+ const oauth2Client = new google.auth.OAuth2(
93
+ GOOGLE_OAUTH_CONFIG.clientId,
94
+ GOOGLE_OAUTH_CONFIG.clientSecret
95
+ );
96
+
97
+ oauth2Client.setCredentials({
98
+ access_token: tokens.access_token,
99
+ refresh_token: tokens.refresh_token,
100
+ expiry_date: tokens.expiry_date,
101
+ });
102
+
103
+ return oauth2Client;
104
+ }
@@ -0,0 +1,114 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
2
+ import { readFile, writeFile } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { hostname, userInfo } from 'os';
6
+ import { CONFIG_DIR, ensureConfigDir } from '../config/config-manager';
7
+ import type { StoredCredentials } from '../types/tokens';
8
+ import type { ServiceName } from '../types/config';
9
+
10
+ const TOKENS_FILE = join(CONFIG_DIR, 'tokens.enc');
11
+ const ALGORITHM = 'aes-256-gcm';
12
+
13
+ // Derive a machine-specific key from hostname + username
14
+ function deriveKey(): Buffer {
15
+ const machineId = `${hostname()}-${userInfo().username}-agentio-v1`;
16
+ return scryptSync(machineId, 'agentio-salt', 32);
17
+ }
18
+
19
+ async function loadCredentials(): Promise<StoredCredentials> {
20
+ await ensureConfigDir();
21
+
22
+ if (!existsSync(TOKENS_FILE)) {
23
+ return {};
24
+ }
25
+
26
+ try {
27
+ const encrypted = await readFile(TOKENS_FILE, 'utf-8');
28
+ const { iv, tag, data } = JSON.parse(encrypted);
29
+
30
+ const key = deriveKey();
31
+ const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, 'hex'));
32
+ decipher.setAuthTag(Buffer.from(tag, 'hex'));
33
+
34
+ const decrypted = Buffer.concat([
35
+ decipher.update(Buffer.from(data, 'hex')),
36
+ decipher.final(),
37
+ ]);
38
+
39
+ return JSON.parse(decrypted.toString('utf-8'));
40
+ } catch {
41
+ // File corrupted, tampered, or key changed - return empty credentials
42
+ return {};
43
+ }
44
+ }
45
+
46
+ async function saveCredentials(credentials: StoredCredentials): Promise<void> {
47
+ await ensureConfigDir();
48
+
49
+ const key = deriveKey();
50
+ const iv = randomBytes(16);
51
+ const cipher = createCipheriv(ALGORITHM, key, iv);
52
+
53
+ const data = JSON.stringify(credentials);
54
+ const encrypted = Buffer.concat([
55
+ cipher.update(data, 'utf-8'),
56
+ cipher.final(),
57
+ ]);
58
+
59
+ const tag = cipher.getAuthTag();
60
+
61
+ const stored = JSON.stringify({
62
+ iv: iv.toString('hex'),
63
+ tag: tag.toString('hex'),
64
+ data: encrypted.toString('hex'),
65
+ });
66
+
67
+ await writeFile(TOKENS_FILE, stored, { mode: 0o600 });
68
+ }
69
+
70
+ export async function getCredentials<T = Record<string, unknown>>(
71
+ service: ServiceName,
72
+ profile: string
73
+ ): Promise<T | null> {
74
+ const credentials = await loadCredentials();
75
+ return (credentials[service]?.[profile] as T) || null;
76
+ }
77
+
78
+ export async function setCredentials(
79
+ service: ServiceName,
80
+ profile: string,
81
+ data: object
82
+ ): Promise<void> {
83
+ const credentials = await loadCredentials();
84
+
85
+ if (!credentials[service]) {
86
+ credentials[service] = {};
87
+ }
88
+
89
+ credentials[service][profile] = data as Record<string, unknown>;
90
+ await saveCredentials(credentials);
91
+ }
92
+
93
+ export async function removeCredentials(
94
+ service: ServiceName,
95
+ profile: string
96
+ ): Promise<boolean> {
97
+ const credentials = await loadCredentials();
98
+
99
+ if (!credentials[service]?.[profile]) {
100
+ return false;
101
+ }
102
+
103
+ delete credentials[service][profile];
104
+ await saveCredentials(credentials);
105
+ return true;
106
+ }
107
+
108
+ export async function hasCredentials(
109
+ service: ServiceName,
110
+ profile: string
111
+ ): Promise<boolean> {
112
+ const credentials = await loadCredentials();
113
+ return !!credentials[service]?.[profile];
114
+ }