@plosson/agentio 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/auth/oauth.ts +24 -2
- package/src/auth/token-manager.ts +10 -8
- package/src/commands/config.ts +90 -3
- package/src/commands/discourse.ts +3 -3
- package/src/commands/gchat.ts +14 -9
- 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 +25 -28
- package/src/commands/slack.ts +1 -1
- package/src/commands/sql.ts +14 -27
- 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/interactive.ts +145 -0
- package/src/utils/output.ts +109 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plosson/agentio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "CLI for LLM agents to interact with communication and tracking services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"typescript": "^5.9.3"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@inquirer/prompts": "^8.2.0",
|
|
49
50
|
"commander": "^14.0.2",
|
|
50
51
|
"googleapis": "^169.0.0",
|
|
51
52
|
"libsodium-wrappers": "^0.8.1",
|
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);
|
package/src/commands/config.ts
CHANGED
|
@@ -7,9 +7,15 @@ import { loadConfig, saveConfig, setEnv, unsetEnv, listEnv } from '../config/con
|
|
|
7
7
|
import { getAllCredentials, setAllCredentials } from '../auth/token-store';
|
|
8
8
|
import { CliError, handleError } from '../utils/errors';
|
|
9
9
|
import { confirm } from '../utils/stdin';
|
|
10
|
-
import
|
|
10
|
+
import { isInteractive, interactiveCheckbox, interactiveSelect } from '../utils/interactive';
|
|
11
|
+
import type { Config, ServiceName } from '../types/config';
|
|
11
12
|
import type { StoredCredentials } from '../types/tokens';
|
|
12
13
|
|
|
14
|
+
interface ProfileSelection {
|
|
15
|
+
service: ServiceName;
|
|
16
|
+
profile: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
const ALGORITHM = 'aes-256-gcm';
|
|
14
20
|
|
|
15
21
|
interface ExportedData {
|
|
@@ -94,6 +100,7 @@ export function registerConfigCommands(program: Command): void {
|
|
|
94
100
|
.description('Export configuration and credentials (as environment variables by default, or to a file)')
|
|
95
101
|
.option('--key <key>', 'Encryption key (64 hex characters). If not provided, a random key will be generated')
|
|
96
102
|
.option('--file <path>', 'Write encrypted config to file instead of outputting AGENTIO_CONFIG')
|
|
103
|
+
.option('--all', 'Export all profiles without prompting for selection')
|
|
97
104
|
.action(async (options) => {
|
|
98
105
|
try {
|
|
99
106
|
// Validate key if provided
|
|
@@ -115,23 +122,103 @@ export function registerConfigCommands(program: Command): void {
|
|
|
115
122
|
const configData = await loadConfig();
|
|
116
123
|
const credentials = await getAllCredentials();
|
|
117
124
|
|
|
125
|
+
// Build list of all available profiles
|
|
126
|
+
const allProfiles: ProfileSelection[] = [];
|
|
127
|
+
for (const [service, profiles] of Object.entries(configData.profiles)) {
|
|
128
|
+
if (profiles) {
|
|
129
|
+
for (const profile of profiles) {
|
|
130
|
+
allProfiles.push({ service: service as ServiceName, profile });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (allProfiles.length === 0) {
|
|
136
|
+
throw new CliError(
|
|
137
|
+
'NOT_FOUND',
|
|
138
|
+
'No profiles configured',
|
|
139
|
+
'Add profiles first with: agentio <service> profile add'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Determine which profiles to export
|
|
144
|
+
let selectedProfiles: ProfileSelection[];
|
|
145
|
+
|
|
146
|
+
if (options.all || !isInteractive()) {
|
|
147
|
+
// Export all profiles
|
|
148
|
+
selectedProfiles = allProfiles;
|
|
149
|
+
} else {
|
|
150
|
+
// Interactive: ask user to select profiles
|
|
151
|
+
const exportAll = await interactiveSelect({
|
|
152
|
+
message: 'What would you like to export?',
|
|
153
|
+
choices: [
|
|
154
|
+
{ name: `All profiles (${allProfiles.length})`, value: 'all' },
|
|
155
|
+
{ name: 'Select specific profiles', value: 'select' },
|
|
156
|
+
],
|
|
157
|
+
default: 'all',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (exportAll === 'all') {
|
|
161
|
+
selectedProfiles = allProfiles;
|
|
162
|
+
} else {
|
|
163
|
+
selectedProfiles = await interactiveCheckbox({
|
|
164
|
+
message: 'Select profiles to export:',
|
|
165
|
+
choices: allProfiles.map((p) => ({
|
|
166
|
+
name: `${p.service}: ${p.profile}`,
|
|
167
|
+
value: p,
|
|
168
|
+
checked: false,
|
|
169
|
+
})),
|
|
170
|
+
required: true,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Filter config and credentials based on selection
|
|
176
|
+
const filteredConfig: Config = { profiles: {} };
|
|
177
|
+
const filteredCredentials: StoredCredentials = {};
|
|
178
|
+
|
|
179
|
+
for (const { service, profile } of selectedProfiles) {
|
|
180
|
+
// Add to filtered config
|
|
181
|
+
if (!filteredConfig.profiles[service]) {
|
|
182
|
+
(filteredConfig.profiles as Record<string, string[]>)[service] = [];
|
|
183
|
+
}
|
|
184
|
+
(filteredConfig.profiles as Record<string, string[]>)[service].push(profile);
|
|
185
|
+
|
|
186
|
+
// Add credentials if they exist
|
|
187
|
+
if (credentials[service]?.[profile]) {
|
|
188
|
+
if (!filteredCredentials[service]) {
|
|
189
|
+
filteredCredentials[service] = {};
|
|
190
|
+
}
|
|
191
|
+
filteredCredentials[service][profile] = credentials[service][profile];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Include env vars if they exist
|
|
196
|
+
if (configData.env) {
|
|
197
|
+
filteredConfig.env = configData.env;
|
|
198
|
+
}
|
|
199
|
+
|
|
118
200
|
const exportData: ExportedData = {
|
|
119
201
|
version: 1,
|
|
120
|
-
config:
|
|
121
|
-
credentials,
|
|
202
|
+
config: filteredConfig,
|
|
203
|
+
credentials: filteredCredentials,
|
|
122
204
|
};
|
|
123
205
|
|
|
124
206
|
// Encrypt the data
|
|
125
207
|
const key = deriveKeyFromPassword(encryptionKey);
|
|
126
208
|
const encrypted = encrypt(JSON.stringify(exportData), key);
|
|
127
209
|
|
|
210
|
+
const profileCount = selectedProfiles.length;
|
|
211
|
+
const profileText = profileCount === 1 ? 'profile' : 'profiles';
|
|
212
|
+
|
|
128
213
|
if (options.file) {
|
|
129
214
|
// Write to file, output just the key
|
|
130
215
|
const filePath = options.file.startsWith('/') ? options.file : join(process.cwd(), options.file);
|
|
131
216
|
await writeFile(filePath, encrypted, { mode: 0o600 });
|
|
217
|
+
console.error(`Exported ${profileCount} ${profileText} to ${filePath}`);
|
|
132
218
|
console.log(`AGENTIO_KEY=${encryptionKey}`);
|
|
133
219
|
} else {
|
|
134
220
|
// Output as environment variables
|
|
221
|
+
console.error(`Exported ${profileCount} ${profileText}`);
|
|
135
222
|
console.log(`AGENTIO_KEY=${encryptionKey}`);
|
|
136
223
|
console.log(`AGENTIO_CONFIG=${encrypted}`);
|
|
137
224
|
}
|
|
@@ -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
|
@@ -10,6 +10,7 @@ import { createGoogleAuth } from '../auth/token-manager';
|
|
|
10
10
|
import { GChatClient } from '../services/gchat/client';
|
|
11
11
|
import { CliError, handleError } from '../utils/errors';
|
|
12
12
|
import { readStdin, prompt } from '../utils/stdin';
|
|
13
|
+
import { interactiveSelect } from '../utils/interactive';
|
|
13
14
|
import { printGChatSendResult, printGChatMessageList, printGChatMessage, printGChatSpaceList } from '../utils/output';
|
|
14
15
|
import type { GChatCredentials, GChatWebhookCredentials, GChatOAuthCredentials } from '../types/gchat';
|
|
15
16
|
|
|
@@ -26,7 +27,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
26
27
|
gchat
|
|
27
28
|
.command('send')
|
|
28
29
|
.description('Send a message to Google Chat')
|
|
29
|
-
.
|
|
30
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
30
31
|
.option('--space <id>', 'Space ID (required for OAuth profiles)')
|
|
31
32
|
.option('--thread <id>', 'Thread ID (optional)')
|
|
32
33
|
.option('--json [file]', 'Send rich message from JSON file (or stdin if no file specified)')
|
|
@@ -111,7 +112,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
111
112
|
gchat
|
|
112
113
|
.command('list')
|
|
113
114
|
.description('List messages from a Google Chat space (OAuth profiles only)')
|
|
114
|
-
.
|
|
115
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
115
116
|
.requiredOption('--space <id>', 'Space ID')
|
|
116
117
|
.option('--limit <n>', 'Number of messages', '10')
|
|
117
118
|
.option('--thread <id>', 'Filter by thread ID')
|
|
@@ -135,7 +136,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
135
136
|
gchat
|
|
136
137
|
.command('get <message-id>')
|
|
137
138
|
.description('Get a message from a Google Chat space (OAuth profiles only)')
|
|
138
|
-
.
|
|
139
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
139
140
|
.requiredOption('--space <id>', 'Space ID')
|
|
140
141
|
.action(async (messageId: string, options) => {
|
|
141
142
|
try {
|
|
@@ -154,7 +155,7 @@ export function registerGChatCommands(program: Command): void {
|
|
|
154
155
|
gchat
|
|
155
156
|
.command('spaces')
|
|
156
157
|
.description('List available Google Chat spaces (OAuth profiles only)')
|
|
157
|
-
.
|
|
158
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
158
159
|
.option('--filter <text>', 'Filter spaces by name (case-insensitive)')
|
|
159
160
|
.action(async (options) => {
|
|
160
161
|
try {
|
|
@@ -187,9 +188,15 @@ export function registerGChatCommands(program: Command): void {
|
|
|
187
188
|
try {
|
|
188
189
|
console.error('\nGoogle Chat Setup\n');
|
|
189
190
|
|
|
190
|
-
const profileType = await
|
|
191
|
+
const profileType = await interactiveSelect({
|
|
192
|
+
message: 'Choose profile type:',
|
|
193
|
+
choices: [
|
|
194
|
+
{ name: 'Webhook', value: 'webhook', description: 'Simple incoming webhook URL' },
|
|
195
|
+
{ name: 'OAuth', value: 'oauth', description: 'Full API access with Google Workspace account' },
|
|
196
|
+
],
|
|
197
|
+
});
|
|
191
198
|
|
|
192
|
-
if (profileType
|
|
199
|
+
if (profileType === 'webhook') {
|
|
193
200
|
if (!options.profile) {
|
|
194
201
|
throw new CliError(
|
|
195
202
|
'INVALID_PARAMS',
|
|
@@ -198,10 +205,8 @@ export function registerGChatCommands(program: Command): void {
|
|
|
198
205
|
);
|
|
199
206
|
}
|
|
200
207
|
await setupWebhookProfile(options.profile);
|
|
201
|
-
} else if (profileType.toLowerCase() === 'oauth') {
|
|
202
|
-
await setupOAuthProfile(options.profile);
|
|
203
208
|
} else {
|
|
204
|
-
|
|
209
|
+
await setupOAuthProfile(options.profile);
|
|
205
210
|
}
|
|
206
211
|
} catch (error) {
|
|
207
212
|
handleError(error);
|
|
@@ -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
|
+
}
|