@plosson/agentio 0.4.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.4.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",
@@ -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 type { Config } from '../types/config';
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: configData,
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
  }
@@ -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
 
@@ -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 prompt('Choose profile type (webhook/oauth): ');
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.toLowerCase() === 'webhook') {
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
- throw new CliError('INVALID_PARAMS', 'Profile type must be "webhook" or "oauth"');
209
+ await setupOAuthProfile(options.profile);
205
210
  }
206
211
  } catch (error) {
207
212
  handleError(error);
@@ -5,7 +5,8 @@ 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, prompt } from '../utils/stdin';
8
+ import { readStdin } from '../utils/stdin';
9
+ import { interactiveSelect } from '../utils/interactive';
9
10
  import {
10
11
  printJiraProjectList,
11
12
  printJiraIssueList,
@@ -220,20 +221,14 @@ export function registerJiraCommands(program: Command): void {
220
221
 
221
222
  // Site selection callback
222
223
  const selectSite = async (sites: AtlassianSite[]): Promise<AtlassianSite> => {
223
- console.error('\nMultiple JIRA sites found:\n');
224
- sites.forEach((site, index) => {
225
- console.error(` [${index + 1}] ${site.name} (${site.url})`);
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
+ })),
226
231
  });
227
- console.error('');
228
-
229
- const choice = await prompt(`? Select a site (1-${sites.length}): `);
230
- const index = parseInt(choice, 10) - 1;
231
-
232
- if (isNaN(index) || index < 0 || index >= sites.length) {
233
- throw new CliError('INVALID_PARAMS', 'Invalid selection');
234
- }
235
-
236
- return sites[index];
237
232
  };
238
233
 
239
234
  const result = await performJiraOAuthFlow(selectSite);
@@ -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>({
@@ -138,31 +139,16 @@ async function promptInteractiveConnection(): Promise<string> {
138
139
  console.error('\nSQL Database Setup (Interactive)\n');
139
140
 
140
141
  // Database type
141
- console.error('Database type:');
142
- console.error(' 1. PostgreSQL');
143
- console.error(' 2. MySQL');
144
- console.error(' 3. SQLite\n');
145
-
146
- const typeChoice = await prompt('? Select type (1-3): ');
147
- let dbType: 'postgres' | 'mysql' | 'sqlite';
148
- let defaultPort: string;
149
-
150
- switch (typeChoice) {
151
- case '1':
152
- dbType = 'postgres';
153
- defaultPort = '5432';
154
- break;
155
- case '2':
156
- dbType = 'mysql';
157
- defaultPort = '3306';
158
- break;
159
- case '3':
160
- dbType = 'sqlite';
161
- defaultPort = '';
162
- break;
163
- default:
164
- throw new CliError('INVALID_PARAMS', 'Invalid database type selection');
165
- }
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' : '';
166
152
 
167
153
  // SQLite only needs a file path
168
154
  if (dbType === 'sqlite') {
@@ -0,0 +1,145 @@
1
+ import {
2
+ select,
3
+ checkbox,
4
+ confirm as inquirerConfirm,
5
+ input,
6
+ } from '@inquirer/prompts';
7
+ import { CliError } from './errors';
8
+
9
+ export interface SelectChoice<T> {
10
+ name: string;
11
+ value: T;
12
+ description?: string;
13
+ }
14
+
15
+ export interface CheckboxChoice<T> {
16
+ name: string;
17
+ value: T;
18
+ checked?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Check if running in an interactive terminal.
23
+ */
24
+ export function isInteractive(): boolean {
25
+ return process.stdin.isTTY === true;
26
+ }
27
+
28
+ /**
29
+ * Interactive select prompt. Falls back to default or throws if not in TTY.
30
+ */
31
+ export async function interactiveSelect<T>(options: {
32
+ message: string;
33
+ choices: SelectChoice<T>[];
34
+ default?: T;
35
+ }): Promise<T> {
36
+ if (!isInteractive()) {
37
+ if (options.default !== undefined) {
38
+ return options.default;
39
+ }
40
+ throw new CliError(
41
+ 'INVALID_PARAMS',
42
+ 'Interactive input required but not running in terminal',
43
+ 'Run this command in an interactive terminal'
44
+ );
45
+ }
46
+
47
+ return select({
48
+ message: options.message,
49
+ choices: options.choices,
50
+ default: options.default,
51
+ loop: false,
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Interactive checkbox (multi-select) prompt. Falls back to default or throws if not in TTY.
57
+ */
58
+ export async function interactiveCheckbox<T>(options: {
59
+ message: string;
60
+ choices: CheckboxChoice<T>[];
61
+ required?: boolean;
62
+ }): Promise<T[]> {
63
+ if (!isInteractive()) {
64
+ // Return all checked items as default
65
+ const defaults = options.choices.filter((c) => c.checked).map((c) => c.value);
66
+ if (defaults.length > 0 || !options.required) {
67
+ return defaults;
68
+ }
69
+ throw new CliError(
70
+ 'INVALID_PARAMS',
71
+ 'Interactive input required but not running in terminal',
72
+ 'Run this command in an interactive terminal'
73
+ );
74
+ }
75
+
76
+ const result = await checkbox({
77
+ message: options.message,
78
+ choices: options.choices,
79
+ loop: false,
80
+ });
81
+
82
+ if (options.required && result.length === 0) {
83
+ throw new CliError('INVALID_PARAMS', 'At least one option must be selected');
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Interactive confirm prompt. Falls back to default or throws if not in TTY.
91
+ */
92
+ export async function interactiveConfirm(options: {
93
+ message: string;
94
+ default?: boolean;
95
+ }): Promise<boolean> {
96
+ if (!isInteractive()) {
97
+ if (options.default !== undefined) {
98
+ return options.default;
99
+ }
100
+ throw new CliError(
101
+ 'INVALID_PARAMS',
102
+ 'Interactive input required but not running in terminal',
103
+ 'Run this command in an interactive terminal'
104
+ );
105
+ }
106
+
107
+ return inquirerConfirm({
108
+ message: options.message,
109
+ default: options.default,
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Interactive text input prompt. Falls back to default or throws if not in TTY.
115
+ */
116
+ export async function interactiveInput(options: {
117
+ message: string;
118
+ default?: string;
119
+ required?: boolean;
120
+ }): Promise<string> {
121
+ if (!isInteractive()) {
122
+ if (options.default !== undefined) {
123
+ return options.default;
124
+ }
125
+ if (!options.required) {
126
+ return '';
127
+ }
128
+ throw new CliError(
129
+ 'INVALID_PARAMS',
130
+ 'Interactive input required but not running in terminal',
131
+ 'Run this command in an interactive terminal'
132
+ );
133
+ }
134
+
135
+ const result = await input({
136
+ message: options.message,
137
+ default: options.default,
138
+ });
139
+
140
+ if (options.required && !result.trim()) {
141
+ throw new CliError('INVALID_PARAMS', 'Input is required');
142
+ }
143
+
144
+ return result;
145
+ }