@plosson/agentio 0.3.2 → 0.4.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/package.json +1 -1
- package/src/auth/oauth.ts +24 -2
- package/src/auth/token-manager.ts +10 -8
- package/src/commands/discourse.ts +3 -3
- package/src/commands/gchat.ts +4 -4
- package/src/commands/gdocs.ts +186 -0
- package/src/commands/gdrive.ts +285 -0
- package/src/commands/github.ts +2 -2
- package/src/commands/gmail.ts +10 -10
- package/src/commands/jira.ts +16 -14
- package/src/commands/slack.ts +1 -1
- package/src/commands/sql.ts +3 -2
- package/src/commands/status.ts +14 -0
- package/src/commands/telegram.ts +1 -1
- package/src/config/config-manager.ts +35 -1
- package/src/index.ts +4 -0
- package/src/services/gdocs/client.ts +165 -0
- package/src/services/gdrive/client.ts +452 -0
- package/src/types/config.ts +3 -1
- package/src/types/gdocs.ts +28 -0
- package/src/types/gdrive.ts +81 -0
- package/src/utils/client-factory.ts +12 -9
- package/src/utils/output.ts +109 -0
package/package.json
CHANGED
package/src/auth/oauth.ts
CHANGED
|
@@ -17,13 +17,35 @@ const GCHAT_SCOPES = [
|
|
|
17
17
|
'https://www.googleapis.com/auth/userinfo.email', // get user email for profile naming
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const GDOCS_SCOPES = [
|
|
21
|
+
'https://www.googleapis.com/auth/documents', // read/write docs
|
|
22
|
+
'https://www.googleapis.com/auth/drive.file', // create/access files created by this app
|
|
23
|
+
'https://www.googleapis.com/auth/drive.readonly', // read all drive files (list, metadata, export)
|
|
24
|
+
'https://www.googleapis.com/auth/userinfo.email', // get email for profile naming
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const GDRIVE_READONLY_SCOPES = [
|
|
28
|
+
'https://www.googleapis.com/auth/drive.readonly', // read all drive files (list, metadata, download)
|
|
29
|
+
'https://www.googleapis.com/auth/userinfo.email', // get email for profile naming
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const GDRIVE_FULL_SCOPES = [
|
|
33
|
+
'https://www.googleapis.com/auth/drive', // full access to all drive files
|
|
34
|
+
'https://www.googleapis.com/auth/userinfo.email', // get email for profile naming
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const SCOPES: Record<'gmail' | 'gchat' | 'gdocs' | 'gdrive-readonly' | 'gdrive-full', string[]> = {
|
|
21
38
|
gmail: GMAIL_SCOPES,
|
|
22
39
|
gchat: GCHAT_SCOPES,
|
|
40
|
+
gdocs: GDOCS_SCOPES,
|
|
41
|
+
'gdrive-readonly': GDRIVE_READONLY_SCOPES,
|
|
42
|
+
'gdrive-full': GDRIVE_FULL_SCOPES,
|
|
23
43
|
};
|
|
24
44
|
|
|
45
|
+
export type OAuthService = 'gmail' | 'gchat' | 'gdocs' | 'gdrive-readonly' | 'gdrive-full';
|
|
46
|
+
|
|
25
47
|
export async function performOAuthFlow(
|
|
26
|
-
service:
|
|
48
|
+
service: OAuthService
|
|
27
49
|
): Promise<OAuthTokens> {
|
|
28
50
|
const port = await findAvailablePort();
|
|
29
51
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { google } from 'googleapis';
|
|
2
2
|
import { getCredentials, setCredentials } from './token-store';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveProfile } from '../config/config-manager';
|
|
4
4
|
import { GOOGLE_OAUTH_CONFIG } from '../config/credentials';
|
|
5
5
|
import { CliError } from '../utils/errors';
|
|
6
6
|
import type { ServiceName } from '../types/config';
|
|
@@ -10,16 +10,18 @@ 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
|
-
const profile = await
|
|
15
|
+
const { profile, error } = await resolveProfile(service, profileName);
|
|
16
16
|
|
|
17
17
|
if (!profile) {
|
|
18
|
-
|
|
19
|
-
'PROFILE_NOT_FOUND',
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
if (error === 'none') {
|
|
19
|
+
throw new CliError('PROFILE_NOT_FOUND', `No ${service} profile configured`, `Run: agentio ${service} profile add`);
|
|
20
|
+
}
|
|
21
|
+
if (error === 'multiple') {
|
|
22
|
+
throw new CliError('PROFILE_NOT_FOUND', `Multiple ${service} profiles exist`, 'Specify --profile <name>');
|
|
23
|
+
}
|
|
24
|
+
throw new CliError('PROFILE_NOT_FOUND', `Profile "${profileName}" not found for ${service}`, `Run: agentio ${service} profile add`);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const tokens = await getCredentials<OAuthTokens>(service, profile);
|
|
@@ -25,7 +25,7 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
25
25
|
discourse
|
|
26
26
|
.command('list')
|
|
27
27
|
.description('List latest topics')
|
|
28
|
-
.
|
|
28
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
29
29
|
.option('--category <slug>', 'Filter by category slug or name')
|
|
30
30
|
.option('--page <number>', 'Page number (0-indexed)', '0')
|
|
31
31
|
.action(async (options) => {
|
|
@@ -46,7 +46,7 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
46
46
|
.command('get')
|
|
47
47
|
.description('Get a topic with its posts')
|
|
48
48
|
.argument('<topic-id>', 'Topic ID')
|
|
49
|
-
.
|
|
49
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
50
50
|
.action(async (topicId: string, options) => {
|
|
51
51
|
try {
|
|
52
52
|
const id = parseInt(topicId, 10);
|
|
@@ -66,7 +66,7 @@ export function registerDiscourseCommands(program: Command): void {
|
|
|
66
66
|
discourse
|
|
67
67
|
.command('categories')
|
|
68
68
|
.description('List all categories')
|
|
69
|
-
.
|
|
69
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
70
70
|
.action(async (options) => {
|
|
71
71
|
try {
|
|
72
72
|
const { client } = await getDiscourseClient(options.profile);
|
package/src/commands/gchat.ts
CHANGED
|
@@ -26,7 +26,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
26
26
|
gchat
|
|
27
27
|
.command('send')
|
|
28
28
|
.description('Send a message to Google Chat')
|
|
29
|
-
.
|
|
29
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
30
30
|
.option('--space <id>', 'Space ID (required for OAuth profiles)')
|
|
31
31
|
.option('--thread <id>', 'Thread ID (optional)')
|
|
32
32
|
.option('--json [file]', 'Send rich message from JSON file (or stdin if no file specified)')
|
|
@@ -111,7 +111,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
111
111
|
gchat
|
|
112
112
|
.command('list')
|
|
113
113
|
.description('List messages from a Google Chat space (OAuth profiles only)')
|
|
114
|
-
.
|
|
114
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
115
115
|
.requiredOption('--space <id>', 'Space ID')
|
|
116
116
|
.option('--limit <n>', 'Number of messages', '10')
|
|
117
117
|
.option('--thread <id>', 'Filter by thread ID')
|
|
@@ -135,7 +135,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
135
135
|
gchat
|
|
136
136
|
.command('get <message-id>')
|
|
137
137
|
.description('Get a message from a Google Chat space (OAuth profiles only)')
|
|
138
|
-
.
|
|
138
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
139
139
|
.requiredOption('--space <id>', 'Space ID')
|
|
140
140
|
.action(async (messageId: string, options) => {
|
|
141
141
|
try {
|
|
@@ -154,7 +154,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
154
154
|
gchat
|
|
155
155
|
.command('spaces')
|
|
156
156
|
.description('List available Google Chat spaces (OAuth profiles only)')
|
|
157
|
-
.
|
|
157
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
158
158
|
.option('--filter <text>', 'Filter spaces by name (case-insensitive)')
|
|
159
159
|
.action(async (options) => {
|
|
160
160
|
try {
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { createGoogleAuth } from '../auth/token-manager';
|
|
5
|
+
import { setCredentials } from '../auth/token-store';
|
|
6
|
+
import { setProfile } from '../config/config-manager';
|
|
7
|
+
import { createProfileCommands } from '../utils/profile-commands';
|
|
8
|
+
import { createClientGetter } from '../utils/client-factory';
|
|
9
|
+
import { performOAuthFlow } from '../auth/oauth';
|
|
10
|
+
import { GDocsClient } from '../services/gdocs/client';
|
|
11
|
+
import { printGDocsList, printGDocCreated, raw } from '../utils/output';
|
|
12
|
+
import { CliError, handleError } from '../utils/errors';
|
|
13
|
+
import { readStdin } from '../utils/stdin';
|
|
14
|
+
import type { GDocsCredentials } from '../types/gdocs';
|
|
15
|
+
|
|
16
|
+
const getGDocsClient = createClientGetter<GDocsCredentials, GDocsClient>({
|
|
17
|
+
service: 'gdocs',
|
|
18
|
+
createClient: (credentials) => new GDocsClient(credentials),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function registerGDocsCommands(program: Command): void {
|
|
22
|
+
const gdocs = program
|
|
23
|
+
.command('gdocs')
|
|
24
|
+
.description('Google Docs operations');
|
|
25
|
+
|
|
26
|
+
gdocs
|
|
27
|
+
.command('get <doc-id-or-url>')
|
|
28
|
+
.description('Export a document')
|
|
29
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
30
|
+
.option('--format <format>', 'Export format: markdown or docx', 'markdown')
|
|
31
|
+
.option('--output <file>', 'Output file path (required for docx, optional for markdown)')
|
|
32
|
+
.action(async (docIdOrUrl: string, options) => {
|
|
33
|
+
try {
|
|
34
|
+
const { client } = await getGDocsClient(options.profile);
|
|
35
|
+
const format = options.format.toLowerCase();
|
|
36
|
+
|
|
37
|
+
if (format !== 'markdown' && format !== 'docx') {
|
|
38
|
+
throw new CliError('INVALID_PARAMS', `Unknown format: ${format}`, 'Use --format markdown or --format docx');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (format === 'docx' && !options.output) {
|
|
42
|
+
throw new CliError('INVALID_PARAMS', 'Output file required for docx format', 'Use --output <file.docx>');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const content = format === 'docx'
|
|
46
|
+
? await client.getAsDocx(docIdOrUrl)
|
|
47
|
+
: await client.getAsMarkdown(docIdOrUrl);
|
|
48
|
+
|
|
49
|
+
if (options.output) {
|
|
50
|
+
await writeFile(options.output, content);
|
|
51
|
+
console.log(`Exported to ${options.output}`);
|
|
52
|
+
} else {
|
|
53
|
+
raw(content as string);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
handleError(error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
gdocs
|
|
61
|
+
.command('create')
|
|
62
|
+
.description('Create a new document from Markdown')
|
|
63
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
64
|
+
.requiredOption('--title <title>', 'Document title')
|
|
65
|
+
.option('--content <text>', 'Markdown content (or pipe via stdin)')
|
|
66
|
+
.option('--folder <folder-id>', 'Folder ID to create the document in')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
try {
|
|
69
|
+
let content = options.content;
|
|
70
|
+
|
|
71
|
+
if (!content) {
|
|
72
|
+
content = await readStdin();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!content) {
|
|
76
|
+
throw new CliError('INVALID_PARAMS', 'No content provided', 'Provide --content or pipe markdown via stdin');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { client } = await getGDocsClient(options.profile);
|
|
80
|
+
const result = await client.create(options.title, content, options.folder);
|
|
81
|
+
|
|
82
|
+
printGDocCreated(result);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
handleError(error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
gdocs
|
|
89
|
+
.command('list')
|
|
90
|
+
.description('List recent documents')
|
|
91
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
92
|
+
.option('--limit <n>', 'Number of documents', '10')
|
|
93
|
+
.option('--query <query>', 'Drive search query filter')
|
|
94
|
+
.addHelpText('after', `
|
|
95
|
+
Query Syntax Examples:
|
|
96
|
+
|
|
97
|
+
Name search:
|
|
98
|
+
--query "name contains 'project'" Documents with "project" in name
|
|
99
|
+
--query "name = 'Meeting Notes'" Exact name match
|
|
100
|
+
|
|
101
|
+
Ownership:
|
|
102
|
+
--query "'me' in owners" Documents you own
|
|
103
|
+
--query "'user@example.com' in owners" Documents owned by specific user
|
|
104
|
+
|
|
105
|
+
Date filters:
|
|
106
|
+
--query "modifiedTime > '2024-01-01'" Modified after date
|
|
107
|
+
--query "createdTime > '2024-01-01'" Created after date
|
|
108
|
+
|
|
109
|
+
Starred/Trashed:
|
|
110
|
+
--query "starred = true" Starred documents
|
|
111
|
+
--query "trashed = false" Not in trash (default)
|
|
112
|
+
|
|
113
|
+
Combined:
|
|
114
|
+
--query "name contains 'report' and modifiedTime > '2024-01-01'"
|
|
115
|
+
`)
|
|
116
|
+
.action(async (options) => {
|
|
117
|
+
try {
|
|
118
|
+
const { client } = await getGDocsClient(options.profile);
|
|
119
|
+
const docs = await client.list({
|
|
120
|
+
limit: parseInt(options.limit, 10),
|
|
121
|
+
query: options.query,
|
|
122
|
+
});
|
|
123
|
+
printGDocsList(docs);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
handleError(error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Profile management
|
|
130
|
+
const profile = createProfileCommands<GDocsCredentials>(gdocs, {
|
|
131
|
+
service: 'gdocs',
|
|
132
|
+
displayName: 'Google Docs',
|
|
133
|
+
getExtraInfo: (credentials) => credentials?.email ? ` - ${credentials.email}` : '',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
profile
|
|
137
|
+
.command('add')
|
|
138
|
+
.description('Add a new Google Docs profile')
|
|
139
|
+
.option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
|
|
140
|
+
.action(async (options) => {
|
|
141
|
+
try {
|
|
142
|
+
console.error('Starting OAuth flow for Google Docs...\n');
|
|
143
|
+
|
|
144
|
+
const tokens = await performOAuthFlow('gdocs');
|
|
145
|
+
const auth = createGoogleAuth(tokens);
|
|
146
|
+
|
|
147
|
+
// Fetch user email for profile naming
|
|
148
|
+
let userEmail: string;
|
|
149
|
+
try {
|
|
150
|
+
const oauth2 = google.oauth2({ version: 'v2', auth });
|
|
151
|
+
const userInfo = await oauth2.userinfo.get();
|
|
152
|
+
userEmail = userInfo.data.email || '';
|
|
153
|
+
if (!userEmail) {
|
|
154
|
+
throw new Error('No email returned');
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
158
|
+
throw new CliError(
|
|
159
|
+
'AUTH_FAILED',
|
|
160
|
+
`Failed to fetch user email: ${errorMessage}`,
|
|
161
|
+
'Ensure the account has an email address'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const profileName = options.profile || userEmail;
|
|
166
|
+
|
|
167
|
+
const credentials: GDocsCredentials = {
|
|
168
|
+
accessToken: tokens.access_token,
|
|
169
|
+
refreshToken: tokens.refresh_token,
|
|
170
|
+
expiryDate: tokens.expiry_date,
|
|
171
|
+
tokenType: tokens.token_type,
|
|
172
|
+
scope: tokens.scope,
|
|
173
|
+
email: userEmail,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
await setProfile('gdocs', profileName);
|
|
177
|
+
await setCredentials('gdocs', profileName, credentials);
|
|
178
|
+
|
|
179
|
+
console.log(`\nSuccess! Profile "${profileName}" configured.`);
|
|
180
|
+
console.log(` Email: ${userEmail}`);
|
|
181
|
+
console.log(` Test with: agentio gdocs list --profile ${profileName}`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
handleError(error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { createGoogleAuth } from '../auth/token-manager';
|
|
4
|
+
import { setCredentials } from '../auth/token-store';
|
|
5
|
+
import { setProfile } from '../config/config-manager';
|
|
6
|
+
import { createProfileCommands } from '../utils/profile-commands';
|
|
7
|
+
import { createClientGetter } from '../utils/client-factory';
|
|
8
|
+
import { performOAuthFlow } from '../auth/oauth';
|
|
9
|
+
import { GDriveClient } from '../services/gdrive/client';
|
|
10
|
+
import { printGDriveFileList, printGDriveFile, printGDriveDownloaded, printGDriveUploaded } from '../utils/output';
|
|
11
|
+
import { CliError, handleError } from '../utils/errors';
|
|
12
|
+
import { prompt } from '../utils/stdin';
|
|
13
|
+
import type { GDriveCredentials, GDriveAccessLevel } from '../types/gdrive';
|
|
14
|
+
|
|
15
|
+
const getGDriveClient = createClientGetter<GDriveCredentials, GDriveClient>({
|
|
16
|
+
service: 'gdrive',
|
|
17
|
+
createClient: (credentials) => new GDriveClient(credentials),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function registerGDriveCommands(program: Command): void {
|
|
21
|
+
const gdrive = program
|
|
22
|
+
.command('gdrive')
|
|
23
|
+
.description('Google Drive operations');
|
|
24
|
+
|
|
25
|
+
gdrive
|
|
26
|
+
.command('list')
|
|
27
|
+
.description('List files')
|
|
28
|
+
.option('--profile <name>', 'Profile name')
|
|
29
|
+
.option('--limit <n>', 'Number of files', '20')
|
|
30
|
+
.option('--folder <id>', 'Folder ID to list (use "root" for root folder)')
|
|
31
|
+
.option('--query <query>', 'Drive API query filter')
|
|
32
|
+
.option('--order <field>', 'Order by field', 'modifiedTime desc')
|
|
33
|
+
.option('--trash', 'Include trashed files')
|
|
34
|
+
.addHelpText('after', `
|
|
35
|
+
Query Syntax Examples:
|
|
36
|
+
|
|
37
|
+
Name search:
|
|
38
|
+
--query "name contains 'report'" Files with "report" in name
|
|
39
|
+
--query "name = 'Budget 2024.xlsx'" Exact name match
|
|
40
|
+
|
|
41
|
+
File type:
|
|
42
|
+
--query "mimeType = 'application/pdf'"
|
|
43
|
+
--query "mimeType contains 'image/'"
|
|
44
|
+
|
|
45
|
+
Ownership:
|
|
46
|
+
--query "'me' in owners" Files you own
|
|
47
|
+
--query "not 'me' in owners" Shared with you
|
|
48
|
+
|
|
49
|
+
Date filters:
|
|
50
|
+
--query "modifiedTime > '2024-01-01'"
|
|
51
|
+
--query "createdTime > '2024-01-01'"
|
|
52
|
+
|
|
53
|
+
Properties:
|
|
54
|
+
--query "starred = true" Starred files
|
|
55
|
+
--query "shared = true" Shared files
|
|
56
|
+
|
|
57
|
+
Combined:
|
|
58
|
+
--query "name contains 'report' and modifiedTime > '2024-01-01'"
|
|
59
|
+
`)
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
try {
|
|
62
|
+
const { client } = await getGDriveClient(options.profile);
|
|
63
|
+
const files = await client.list({
|
|
64
|
+
limit: parseInt(options.limit, 10),
|
|
65
|
+
folderId: options.folder,
|
|
66
|
+
query: options.query,
|
|
67
|
+
orderBy: options.order,
|
|
68
|
+
includeTrash: options.trash,
|
|
69
|
+
});
|
|
70
|
+
printGDriveFileList(files);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
handleError(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
gdrive
|
|
77
|
+
.command('folders')
|
|
78
|
+
.description('List folders')
|
|
79
|
+
.option('--profile <name>', 'Profile name')
|
|
80
|
+
.option('--limit <n>', 'Number of folders', '20')
|
|
81
|
+
.option('--parent <id>', 'Parent folder ID (use "root" for root folder)')
|
|
82
|
+
.option('--query <query>', 'Additional query filter')
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
try {
|
|
85
|
+
const { client } = await getGDriveClient(options.profile);
|
|
86
|
+
const folders = await client.listFolders({
|
|
87
|
+
limit: parseInt(options.limit, 10),
|
|
88
|
+
parentId: options.parent,
|
|
89
|
+
query: options.query,
|
|
90
|
+
});
|
|
91
|
+
printGDriveFileList(folders, 'Folders');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
handleError(error);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
gdrive
|
|
98
|
+
.command('get <file-id-or-url>')
|
|
99
|
+
.description('Get file metadata')
|
|
100
|
+
.option('--profile <name>', 'Profile name')
|
|
101
|
+
.action(async (fileIdOrUrl: string, options) => {
|
|
102
|
+
try {
|
|
103
|
+
const { client } = await getGDriveClient(options.profile);
|
|
104
|
+
const file = await client.get(fileIdOrUrl);
|
|
105
|
+
printGDriveFile(file);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
handleError(error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
gdrive
|
|
112
|
+
.command('search')
|
|
113
|
+
.description('Search for files')
|
|
114
|
+
.requiredOption('--query <text>', 'Search text (searches name and content)')
|
|
115
|
+
.option('--profile <name>', 'Profile name')
|
|
116
|
+
.option('--limit <n>', 'Number of results', '20')
|
|
117
|
+
.option('--type <mime>', 'Filter by MIME type')
|
|
118
|
+
.option('--folder <id>', 'Search within folder')
|
|
119
|
+
.action(async (options) => {
|
|
120
|
+
try {
|
|
121
|
+
const { client } = await getGDriveClient(options.profile);
|
|
122
|
+
const files = await client.search({
|
|
123
|
+
query: options.query,
|
|
124
|
+
mimeType: options.type,
|
|
125
|
+
limit: parseInt(options.limit, 10),
|
|
126
|
+
folderId: options.folder,
|
|
127
|
+
});
|
|
128
|
+
printGDriveFileList(files, 'Search Results');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
handleError(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
gdrive
|
|
135
|
+
.command('download <file-id-or-url>')
|
|
136
|
+
.description('Download a file (or export Google Workspace files)')
|
|
137
|
+
.option('--profile <name>', 'Profile name')
|
|
138
|
+
.requiredOption('--output <path>', 'Output file path')
|
|
139
|
+
.option('--export <format>', 'Export format for Google Workspace files (pdf, docx, xlsx, csv, pptx, txt, etc.)')
|
|
140
|
+
.addHelpText('after', `
|
|
141
|
+
Export Formats:
|
|
142
|
+
|
|
143
|
+
Google Docs: pdf, docx, odt, txt, html, rtf
|
|
144
|
+
Google Sheets: xlsx, csv, pdf, ods, tsv
|
|
145
|
+
Google Slides: pptx, pdf, odp, txt
|
|
146
|
+
Google Drawing: pdf, png, jpeg, svg
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
agentio gdrive download <doc-id> --output report.pdf --export pdf
|
|
150
|
+
agentio gdrive download <sheet-id> --output data.csv --export csv
|
|
151
|
+
`)
|
|
152
|
+
.action(async (fileIdOrUrl: string, options) => {
|
|
153
|
+
try {
|
|
154
|
+
const { client } = await getGDriveClient(options.profile);
|
|
155
|
+
const result = await client.download({
|
|
156
|
+
fileIdOrUrl,
|
|
157
|
+
outputPath: options.output,
|
|
158
|
+
exportFormat: options.export,
|
|
159
|
+
});
|
|
160
|
+
printGDriveDownloaded(result);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
handleError(error);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
gdrive
|
|
167
|
+
.command('put <file-path>')
|
|
168
|
+
.description('Upload a file to Google Drive')
|
|
169
|
+
.option('--profile <name>', 'Profile name')
|
|
170
|
+
.option('--name <name>', 'Name for the file in Drive (defaults to local filename)')
|
|
171
|
+
.option('--folder <id>', 'Folder ID to upload to')
|
|
172
|
+
.option('--type <mime>', 'MIME type (auto-detected if not specified)')
|
|
173
|
+
.option('--convert', 'Convert to Google Workspace format (Doc, Sheet, or Slides)')
|
|
174
|
+
.addHelpText('after', `
|
|
175
|
+
Conversion:
|
|
176
|
+
|
|
177
|
+
With --convert, supported files are converted to Google Workspace format:
|
|
178
|
+
docx, doc, odt, txt, html, rtf → Google Doc
|
|
179
|
+
xlsx, xls, ods, csv, tsv → Google Sheet
|
|
180
|
+
pptx, ppt, odp → Google Slides
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
agentio gdrive put report.docx --convert # Creates Google Doc
|
|
184
|
+
agentio gdrive put data.xlsx --convert # Creates Google Sheet
|
|
185
|
+
agentio gdrive put slides.pptx --convert # Creates Google Slides
|
|
186
|
+
`)
|
|
187
|
+
.action(async (filePath: string, options) => {
|
|
188
|
+
try {
|
|
189
|
+
const { client } = await getGDriveClient(options.profile);
|
|
190
|
+
const result = await client.upload({
|
|
191
|
+
filePath,
|
|
192
|
+
name: options.name,
|
|
193
|
+
folderId: options.folder,
|
|
194
|
+
mimeType: options.type,
|
|
195
|
+
convert: options.convert,
|
|
196
|
+
});
|
|
197
|
+
printGDriveUploaded(result);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
handleError(error);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Profile management
|
|
204
|
+
const profile = createProfileCommands<GDriveCredentials>(gdrive, {
|
|
205
|
+
service: 'gdrive',
|
|
206
|
+
displayName: 'Google Drive',
|
|
207
|
+
getExtraInfo: (credentials) => {
|
|
208
|
+
if (!credentials) return '';
|
|
209
|
+
const access = credentials.accessLevel === 'full' ? 'full' : 'read-only';
|
|
210
|
+
return ` - ${credentials.email} (${access})`;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
profile
|
|
215
|
+
.command('add')
|
|
216
|
+
.description('Add a new Google Drive profile')
|
|
217
|
+
.option('--profile <name>', 'Profile name (auto-detected from email if not provided)')
|
|
218
|
+
.option('--readonly', 'Create a read-only profile (skip access level prompt)')
|
|
219
|
+
.option('--full', 'Create a full access profile (skip access level prompt)')
|
|
220
|
+
.action(async (options) => {
|
|
221
|
+
try {
|
|
222
|
+
console.error('Google Drive Setup\n');
|
|
223
|
+
|
|
224
|
+
let accessLevel: GDriveAccessLevel;
|
|
225
|
+
|
|
226
|
+
if (options.readonly) {
|
|
227
|
+
accessLevel = 'readonly';
|
|
228
|
+
} else if (options.full) {
|
|
229
|
+
accessLevel = 'full';
|
|
230
|
+
} else {
|
|
231
|
+
console.error('Access level options:');
|
|
232
|
+
console.error(' 1. Read-only - List, search, download files');
|
|
233
|
+
console.error(' 2. Full - Read-only + upload, create folders, modify files\n');
|
|
234
|
+
|
|
235
|
+
const choice = await prompt('? Select access level (1 or 2): ');
|
|
236
|
+
accessLevel = choice.trim() === '2' ? 'full' : 'readonly';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const oauthService = accessLevel === 'full' ? 'gdrive-full' : 'gdrive-readonly';
|
|
240
|
+
console.error(`\nStarting OAuth flow (${accessLevel} access)...\n`);
|
|
241
|
+
|
|
242
|
+
const tokens = await performOAuthFlow(oauthService);
|
|
243
|
+
const auth = createGoogleAuth(tokens);
|
|
244
|
+
|
|
245
|
+
let userEmail: string;
|
|
246
|
+
try {
|
|
247
|
+
const oauth2 = google.oauth2({ version: 'v2', auth });
|
|
248
|
+
const userInfo = await oauth2.userinfo.get();
|
|
249
|
+
userEmail = userInfo.data.email || '';
|
|
250
|
+
if (!userEmail) {
|
|
251
|
+
throw new Error('No email returned');
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
255
|
+
throw new CliError(
|
|
256
|
+
'AUTH_FAILED',
|
|
257
|
+
`Failed to fetch user email: ${errorMessage}`,
|
|
258
|
+
'Ensure the account has an email address'
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const profileName = options.profile || userEmail;
|
|
263
|
+
|
|
264
|
+
const credentials: GDriveCredentials = {
|
|
265
|
+
accessToken: tokens.access_token,
|
|
266
|
+
refreshToken: tokens.refresh_token,
|
|
267
|
+
expiryDate: tokens.expiry_date,
|
|
268
|
+
tokenType: tokens.token_type,
|
|
269
|
+
scope: tokens.scope,
|
|
270
|
+
email: userEmail,
|
|
271
|
+
accessLevel,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
await setProfile('gdrive', profileName);
|
|
275
|
+
await setCredentials('gdrive', profileName, credentials);
|
|
276
|
+
|
|
277
|
+
console.log(`\nSuccess! Profile "${profileName}" configured.`);
|
|
278
|
+
console.log(` Email: ${userEmail}`);
|
|
279
|
+
console.log(` Access: ${accessLevel === 'full' ? 'Full (read & write)' : 'Read-only'}`);
|
|
280
|
+
console.log(` Test with: agentio gdrive list --profile ${profileName}`);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
handleError(error);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
package/src/commands/github.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function registerGitHubCommands(program: Command): void {
|
|
|
35
35
|
.command('install')
|
|
36
36
|
.description('Install AGENTIO_KEY and AGENTIO_CONFIG as GitHub Actions secrets')
|
|
37
37
|
.argument('<repo>', 'Repository in owner/repo format')
|
|
38
|
-
.
|
|
38
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
39
39
|
.action(async (repo: string, options) => {
|
|
40
40
|
try {
|
|
41
41
|
// Validate repo format
|
|
@@ -70,7 +70,7 @@ export function registerGitHubCommands(program: Command): void {
|
|
|
70
70
|
.command('uninstall')
|
|
71
71
|
.description('Remove AGENTIO_KEY and AGENTIO_CONFIG secrets from a repository')
|
|
72
72
|
.argument('<repo>', 'Repository in owner/repo format')
|
|
73
|
-
.
|
|
73
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
74
74
|
.action(async (repo: string, options) => {
|
|
75
75
|
try {
|
|
76
76
|
// Validate repo format
|