@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
|
@@ -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
|
package/src/commands/gmail.ts
CHANGED
|
@@ -68,7 +68,7 @@ function findChromePath(): string | null {
|
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
async function getGmailClient(profileName
|
|
71
|
+
async function getGmailClient(profileName?: string): Promise<{ client: GmailClient; profile: string }> {
|
|
72
72
|
const { tokens, profile } = await getValidTokens('gmail', profileName);
|
|
73
73
|
const auth = createGoogleAuth(tokens);
|
|
74
74
|
return { client: new GmailClient(auth), profile };
|
|
@@ -82,7 +82,7 @@ export function registerGmailCommands(program: Command): void {
|
|
|
82
82
|
gmail
|
|
83
83
|
.command('list')
|
|
84
84
|
.description('List messages')
|
|
85
|
-
.
|
|
85
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
86
86
|
.option('--limit <n>', 'Number of messages', '10')
|
|
87
87
|
.option('--query <query>', 'Gmail search query (see "gmail search --help" for syntax)')
|
|
88
88
|
.option('--label <label>', 'Filter by label (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
@@ -103,7 +103,7 @@ export function registerGmailCommands(program: Command): void {
|
|
|
103
103
|
gmail
|
|
104
104
|
.command('get <message-id>')
|
|
105
105
|
.description('Get a message')
|
|
106
|
-
.
|
|
106
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
107
107
|
.option('--format <format>', 'Body format: text, html, or raw', 'text')
|
|
108
108
|
.option('--body-only', 'Output only the message body')
|
|
109
109
|
.action(async (messageId: string, options) => {
|
|
@@ -124,7 +124,7 @@ export function registerGmailCommands(program: Command): void {
|
|
|
124
124
|
.command('search')
|
|
125
125
|
.description('Search messages using Gmail query syntax')
|
|
126
126
|
.requiredOption('--query <query>', 'Search query')
|
|
127
|
-
.
|
|
127
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
128
128
|
.option('--limit <n>', 'Max results', '10')
|
|
129
129
|
.addHelpText('after', `
|
|
130
130
|
Query Syntax Examples:
|
|
@@ -178,7 +178,7 @@ Query Syntax Examples:
|
|
|
178
178
|
gmail
|
|
179
179
|
.command('send')
|
|
180
180
|
.description('Send an email')
|
|
181
|
-
.
|
|
181
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
182
182
|
.requiredOption('--to <email>', 'Recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
183
183
|
.option('--cc <email>', 'CC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
184
184
|
.option('--bcc <email>', 'BCC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
@@ -246,7 +246,7 @@ Query Syntax Examples:
|
|
|
246
246
|
gmail
|
|
247
247
|
.command('reply')
|
|
248
248
|
.description('Reply to a thread')
|
|
249
|
-
.
|
|
249
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
250
250
|
.requiredOption('--thread-id <id>', 'Thread ID')
|
|
251
251
|
.option('--body <body>', 'Reply body (or pipe via stdin)')
|
|
252
252
|
.option('--html', 'Treat body as HTML')
|
|
@@ -277,7 +277,7 @@ Query Syntax Examples:
|
|
|
277
277
|
gmail
|
|
278
278
|
.command('archive <message-id...>')
|
|
279
279
|
.description('Archive one or more messages')
|
|
280
|
-
.
|
|
280
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
281
281
|
.action(async (messageIds: string[], options) => {
|
|
282
282
|
try {
|
|
283
283
|
const { client } = await getGmailClient(options.profile);
|
|
@@ -293,7 +293,7 @@ Query Syntax Examples:
|
|
|
293
293
|
gmail
|
|
294
294
|
.command('mark <message-id...>')
|
|
295
295
|
.description('Mark one or more messages as read or unread')
|
|
296
|
-
.
|
|
296
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
297
297
|
.option('--read', 'Mark as read')
|
|
298
298
|
.option('--unread', 'Mark as unread')
|
|
299
299
|
.action(async (messageIds: string[], options) => {
|
|
@@ -318,7 +318,7 @@ Query Syntax Examples:
|
|
|
318
318
|
gmail
|
|
319
319
|
.command('attachment <message-id>')
|
|
320
320
|
.description('Download attachments from a message')
|
|
321
|
-
.
|
|
321
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
322
322
|
.option('--name <filename>', 'Download specific attachment by filename (downloads all if not specified)')
|
|
323
323
|
.option('--output <dir>', 'Output directory', '.')
|
|
324
324
|
.action(async (messageId: string, options) => {
|
|
@@ -361,7 +361,7 @@ Query Syntax Examples:
|
|
|
361
361
|
gmail
|
|
362
362
|
.command('export <message-id>')
|
|
363
363
|
.description('Export a message as PDF')
|
|
364
|
-
.
|
|
364
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
365
365
|
.option('--output <path>', 'Output file path', 'message.pdf')
|
|
366
366
|
.action(async (messageId: string, options) => {
|
|
367
367
|
try {
|
package/src/commands/jira.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { setCredentials, getCredentials } from '../auth/token-store';
|
|
3
|
-
import { setProfile,
|
|
3
|
+
import { setProfile, resolveProfile } from '../config/config-manager';
|
|
4
4
|
import { createProfileCommands } from '../utils/profile-commands';
|
|
5
5
|
import { performJiraOAuthFlow, refreshJiraToken, type AtlassianSite } from '../auth/jira-oauth';
|
|
6
6
|
import { JiraClient } from '../services/jira/client';
|
|
7
7
|
import { CliError, handleError } from '../utils/errors';
|
|
8
|
-
import { readStdin
|
|
8
|
+
import { readStdin } from '../utils/stdin';
|
|
9
|
+
import { interactiveSelect } from '../utils/interactive';
|
|
9
10
|
import {
|
|
10
11
|
printJiraProjectList,
|
|
11
12
|
printJiraIssueList,
|
|
@@ -46,15 +47,17 @@ async function ensureValidToken(credentials: JiraCredentials, profile: string):
|
|
|
46
47
|
return credentials;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
async function getJiraClient(profileName
|
|
50
|
-
const profile = await
|
|
50
|
+
async function getJiraClient(profileName?: string): Promise<{ client: JiraClient; profile: string }> {
|
|
51
|
+
const { profile, error } = await resolveProfile('jira', profileName);
|
|
51
52
|
|
|
52
53
|
if (!profile) {
|
|
53
|
-
|
|
54
|
-
'PROFILE_NOT_FOUND',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
if (error === 'none') {
|
|
55
|
+
throw new CliError('PROFILE_NOT_FOUND', 'No jira profile configured', 'Run: agentio jira profile add');
|
|
56
|
+
}
|
|
57
|
+
if (error === 'multiple') {
|
|
58
|
+
throw new CliError('PROFILE_NOT_FOUND', 'Multiple jira profiles exist', 'Specify --profile <name>');
|
|
59
|
+
}
|
|
60
|
+
throw new CliError('PROFILE_NOT_FOUND', `Profile "${profileName}" not found for jira`, 'Run: agentio jira profile add');
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
let credentials = await getCredentials<JiraCredentials>('jira', profile);
|
|
@@ -85,7 +88,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
85
88
|
jira
|
|
86
89
|
.command('projects')
|
|
87
90
|
.description('List JIRA projects')
|
|
88
|
-
.
|
|
91
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
89
92
|
.option('--limit <number>', 'Maximum number of projects', '50')
|
|
90
93
|
.action(async (options) => {
|
|
91
94
|
try {
|
|
@@ -103,7 +106,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
103
106
|
jira
|
|
104
107
|
.command('search')
|
|
105
108
|
.description('Search JIRA issues')
|
|
106
|
-
.
|
|
109
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
107
110
|
.option('--jql <query>', 'JQL query')
|
|
108
111
|
.option('--project <key>', 'Project key')
|
|
109
112
|
.option('--status <status>', 'Issue status')
|
|
@@ -130,7 +133,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
130
133
|
.command('get')
|
|
131
134
|
.description('Get JIRA issue details')
|
|
132
135
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
133
|
-
.
|
|
136
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
134
137
|
.action(async (issueKey: string, options) => {
|
|
135
138
|
try {
|
|
136
139
|
const { client } = await getJiraClient(options.profile);
|
|
@@ -147,7 +150,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
147
150
|
.description('Add a comment to an issue')
|
|
148
151
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
149
152
|
.argument('[body]', 'Comment body (or pipe via stdin)')
|
|
150
|
-
.
|
|
153
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
151
154
|
.action(async (issueKey: string, body: string | undefined, options) => {
|
|
152
155
|
try {
|
|
153
156
|
let text = body;
|
|
@@ -173,7 +176,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
173
176
|
.command('transitions')
|
|
174
177
|
.description('List available transitions for an issue')
|
|
175
178
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
176
|
-
.
|
|
179
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
177
180
|
.action(async (issueKey: string, options) => {
|
|
178
181
|
try {
|
|
179
182
|
const { client } = await getJiraClient(options.profile);
|
|
@@ -190,7 +193,7 @@ export function registerJiraCommands(program: Command): void {
|
|
|
190
193
|
.description('Transition an issue to a new status')
|
|
191
194
|
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
192
195
|
.argument('<transition-id>', 'Transition ID (use "transitions" command to see available)')
|
|
193
|
-
.
|
|
196
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
194
197
|
.action(async (issueKey: string, transitionId: string, options) => {
|
|
195
198
|
try {
|
|
196
199
|
const { client } = await getJiraClient(options.profile);
|
|
@@ -218,20 +221,14 @@ export function registerJiraCommands(program: Command): void {
|
|
|
218
221
|
|
|
219
222
|
// Site selection callback
|
|
220
223
|
const selectSite = async (sites: AtlassianSite[]): Promise<AtlassianSite> => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
return interactiveSelect({
|
|
225
|
+
message: 'Select a JIRA site:',
|
|
226
|
+
choices: sites.map((site) => ({
|
|
227
|
+
name: site.name,
|
|
228
|
+
value: site,
|
|
229
|
+
description: site.url,
|
|
230
|
+
})),
|
|
224
231
|
});
|
|
225
|
-
console.error('');
|
|
226
|
-
|
|
227
|
-
const choice = await prompt(`? Select a site (1-${sites.length}): `);
|
|
228
|
-
const index = parseInt(choice, 10) - 1;
|
|
229
|
-
|
|
230
|
-
if (isNaN(index) || index < 0 || index >= sites.length) {
|
|
231
|
-
throw new CliError('INVALID_PARAMS', 'Invalid selection');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return sites[index];
|
|
235
232
|
};
|
|
236
233
|
|
|
237
234
|
const result = await performJiraOAuthFlow(selectSite);
|
package/src/commands/slack.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function registerSlackCommands(program: Command): void {
|
|
|
23
23
|
slack
|
|
24
24
|
.command('send')
|
|
25
25
|
.description('Send a message to Slack')
|
|
26
|
-
.
|
|
26
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
27
27
|
.option('--json [file]', 'Send Block Kit message from JSON file (or stdin if no file specified)')
|
|
28
28
|
.argument('[message]', 'Message text (or pipe via stdin)')
|
|
29
29
|
.action(async (message: string | undefined, options) => {
|
package/src/commands/sql.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createClientGetter } from '../utils/client-factory';
|
|
|
6
6
|
import { SqlClient } from '../services/sql/client';
|
|
7
7
|
import { CliError, handleError } from '../utils/errors';
|
|
8
8
|
import { readStdin, prompt } from '../utils/stdin';
|
|
9
|
+
import { interactiveSelect } from '../utils/interactive';
|
|
9
10
|
import type { SqlCredentials } from '../types/sql';
|
|
10
11
|
|
|
11
12
|
const getSqlClient = createClientGetter<SqlCredentials, SqlClient>({
|
|
@@ -16,9 +17,10 @@ const getSqlClient = createClientGetter<SqlCredentials, SqlClient>({
|
|
|
16
17
|
function extractDisplayName(url: string): string {
|
|
17
18
|
try {
|
|
18
19
|
const parsed = new URL(url);
|
|
20
|
+
const username = parsed.username ? decodeURIComponent(parsed.username) : '';
|
|
19
21
|
const host = parsed.hostname || 'localhost';
|
|
20
22
|
const db = parsed.pathname.replace(/^\//, '') || 'database';
|
|
21
|
-
return `${host}/${db}`;
|
|
23
|
+
return username ? `${username}@${host}/${db}` : `${host}/${db}`;
|
|
22
24
|
} catch {
|
|
23
25
|
return url.substring(0, 30);
|
|
24
26
|
}
|
|
@@ -32,7 +34,7 @@ export function registerSqlCommands(program: Command): void {
|
|
|
32
34
|
sql
|
|
33
35
|
.command('query')
|
|
34
36
|
.description('Execute a SQL query')
|
|
35
|
-
.
|
|
37
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
36
38
|
.option('--limit <n>', 'Maximum rows to return', '100')
|
|
37
39
|
.argument('[query]', 'SQL query (or pipe via stdin)')
|
|
38
40
|
.action(async (query: string | undefined, options) => {
|
|
@@ -137,31 +139,16 @@ async function promptInteractiveConnection(): Promise<string> {
|
|
|
137
139
|
console.error('\nSQL Database Setup (Interactive)\n');
|
|
138
140
|
|
|
139
141
|
// Database type
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
case '1':
|
|
151
|
-
dbType = 'postgres';
|
|
152
|
-
defaultPort = '5432';
|
|
153
|
-
break;
|
|
154
|
-
case '2':
|
|
155
|
-
dbType = 'mysql';
|
|
156
|
-
defaultPort = '3306';
|
|
157
|
-
break;
|
|
158
|
-
case '3':
|
|
159
|
-
dbType = 'sqlite';
|
|
160
|
-
defaultPort = '';
|
|
161
|
-
break;
|
|
162
|
-
default:
|
|
163
|
-
throw new CliError('INVALID_PARAMS', 'Invalid database type selection');
|
|
164
|
-
}
|
|
142
|
+
const dbType = await interactiveSelect({
|
|
143
|
+
message: 'Select database type:',
|
|
144
|
+
choices: [
|
|
145
|
+
{ name: 'PostgreSQL', value: 'postgres' as const, description: 'Default port 5432' },
|
|
146
|
+
{ name: 'MySQL', value: 'mysql' as const, description: 'Default port 3306' },
|
|
147
|
+
{ name: 'SQLite', value: 'sqlite' as const, description: 'Local file database' },
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const defaultPort = dbType === 'postgres' ? '5432' : dbType === 'mysql' ? '3306' : '';
|
|
165
152
|
|
|
166
153
|
// SQLite only needs a file path
|
|
167
154
|
if (dbType === 'sqlite') {
|
package/src/commands/status.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { createGoogleAuth } from '../auth/token-manager';
|
|
|
5
5
|
import { refreshJiraToken } from '../auth/jira-oauth';
|
|
6
6
|
import { TelegramClient } from '../services/telegram/client';
|
|
7
7
|
import { GmailClient } from '../services/gmail/client';
|
|
8
|
+
import { GDocsClient } from '../services/gdocs/client';
|
|
9
|
+
import { GDriveClient } from '../services/gdrive/client';
|
|
8
10
|
import { GitHubClient } from '../services/github/client';
|
|
9
11
|
import { JiraClient } from '../services/jira/client';
|
|
10
12
|
import { GChatClient } from '../services/gchat/client';
|
|
@@ -17,6 +19,8 @@ import type { OAuthTokens } from '../types/tokens';
|
|
|
17
19
|
import type { TelegramCredentials } from '../types/telegram';
|
|
18
20
|
import type { GitHubCredentials } from '../types/github';
|
|
19
21
|
import type { JiraCredentials } from '../types/jira';
|
|
22
|
+
import type { GDocsCredentials } from '../types/gdocs';
|
|
23
|
+
import type { GDriveCredentials } from '../types/gdrive';
|
|
20
24
|
import type { GChatCredentials } from '../types/gchat';
|
|
21
25
|
import type { SlackCredentials } from '../types/slack';
|
|
22
26
|
import type { DiscourseCredentials } from '../types/discourse';
|
|
@@ -46,6 +50,16 @@ async function createServiceClient(
|
|
|
46
50
|
return new GmailClient(auth);
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
case 'gdocs': {
|
|
54
|
+
const creds = credentials as GDocsCredentials;
|
|
55
|
+
return new GDocsClient(creds);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'gdrive': {
|
|
59
|
+
const creds = credentials as GDriveCredentials;
|
|
60
|
+
return new GDriveClient(creds);
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
case 'telegram': {
|
|
50
64
|
const creds = credentials as TelegramCredentials;
|
|
51
65
|
return new TelegramClient(creds.botToken, creds.channelId);
|
package/src/commands/telegram.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function registerTelegramCommands(program: Command): void {
|
|
|
21
21
|
telegram
|
|
22
22
|
.command('send')
|
|
23
23
|
.description('Send a message to the channel')
|
|
24
|
-
.
|
|
24
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
25
25
|
.option('--parse-mode <mode>', 'Message format: html or markdown')
|
|
26
26
|
.option('--silent', 'Send without notification')
|
|
27
27
|
.argument('[message]', 'Message text (or pipe via stdin)')
|