@plosson/agentio 0.1.11 → 0.1.13

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.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -112,3 +112,11 @@ export async function hasCredentials(
112
112
  const credentials = await loadCredentials();
113
113
  return !!credentials[service]?.[profile];
114
114
  }
115
+
116
+ export async function getAllCredentials(): Promise<StoredCredentials> {
117
+ return loadCredentials();
118
+ }
119
+
120
+ export async function setAllCredentials(credentials: StoredCredentials): Promise<void> {
121
+ return saveCredentials(credentials);
122
+ }
@@ -0,0 +1,238 @@
1
+ import { Command } from 'commander';
2
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
3
+ import { readFile, writeFile } from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { loadConfig, saveConfig } from '../config/config-manager';
7
+ import { getAllCredentials, setAllCredentials } from '../auth/token-store';
8
+ import { CliError, handleError } from '../utils/errors';
9
+ import type { Config } from '../types/config';
10
+ import type { StoredCredentials } from '../types/tokens';
11
+
12
+ const ALGORITHM = 'aes-256-gcm';
13
+ const DEFAULT_EXPORT_FILE = 'agentio.config';
14
+
15
+ interface ExportedData {
16
+ version: number;
17
+ config: Config;
18
+ credentials: StoredCredentials;
19
+ }
20
+
21
+ function deriveKeyFromPassword(password: string): Buffer {
22
+ return scryptSync(password, 'agentio-export-salt', 32);
23
+ }
24
+
25
+ function generateKey(): string {
26
+ return randomBytes(32).toString('hex');
27
+ }
28
+
29
+ function encrypt(data: string, key: Buffer): { iv: string; tag: string; data: string } {
30
+ const iv = randomBytes(16);
31
+ const cipher = createCipheriv(ALGORITHM, key, iv);
32
+
33
+ const encrypted = Buffer.concat([
34
+ cipher.update(data, 'utf-8'),
35
+ cipher.final(),
36
+ ]);
37
+
38
+ const tag = cipher.getAuthTag();
39
+
40
+ return {
41
+ iv: iv.toString('hex'),
42
+ tag: tag.toString('hex'),
43
+ data: encrypted.toString('hex'),
44
+ };
45
+ }
46
+
47
+ function decrypt(encrypted: { iv: string; tag: string; data: string }, key: Buffer): string {
48
+ const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(encrypted.iv, 'hex'));
49
+ decipher.setAuthTag(Buffer.from(encrypted.tag, 'hex'));
50
+
51
+ const decrypted = Buffer.concat([
52
+ decipher.update(Buffer.from(encrypted.data, 'hex')),
53
+ decipher.final(),
54
+ ]);
55
+
56
+ return decrypted.toString('utf-8');
57
+ }
58
+
59
+ export function registerConfigCommands(program: Command): void {
60
+ const config = program
61
+ .command('config')
62
+ .description('Configuration management');
63
+
64
+ config
65
+ .command('export')
66
+ .description('Export configuration and credentials to an encrypted file')
67
+ .option('--key <key>', 'Encryption key (64 hex characters). If not provided, a random key will be generated')
68
+ .option('--output <file>', 'Output file path', DEFAULT_EXPORT_FILE)
69
+ .action(async (options) => {
70
+ try {
71
+ // Validate key if provided
72
+ let encryptionKey: string;
73
+ if (options.key) {
74
+ if (!/^[0-9a-fA-F]{64}$/.test(options.key)) {
75
+ throw new CliError(
76
+ 'INVALID_PARAMS',
77
+ 'Invalid encryption key format',
78
+ 'Key must be exactly 64 hexadecimal characters'
79
+ );
80
+ }
81
+ encryptionKey = options.key;
82
+ } else {
83
+ encryptionKey = generateKey();
84
+ }
85
+
86
+ // Load config and credentials
87
+ const configData = await loadConfig();
88
+ const credentials = await getAllCredentials();
89
+
90
+ const exportData: ExportedData = {
91
+ version: 1,
92
+ config: configData,
93
+ credentials,
94
+ };
95
+
96
+ // Encrypt the data
97
+ const key = deriveKeyFromPassword(encryptionKey);
98
+ const encrypted = encrypt(JSON.stringify(exportData), key);
99
+
100
+ // Write to file
101
+ const outputPath = join(process.cwd(), options.output);
102
+ await writeFile(outputPath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
103
+
104
+ console.log(`Configuration exported to: ${outputPath}`);
105
+ console.log(`Encryption key: ${encryptionKey}`);
106
+ console.log('');
107
+ console.log('Keep this key safe! You will need it to import the configuration.');
108
+ } catch (error) {
109
+ handleError(error);
110
+ }
111
+ });
112
+
113
+ config
114
+ .command('import')
115
+ .description('Import configuration and credentials from an encrypted file')
116
+ .argument('<file>', 'Path to the encrypted configuration file')
117
+ .option('--key <key>', 'Encryption key (64 hex characters). Falls back to AGENTIO_KEY env var')
118
+ .option('--merge', 'Merge with existing configuration instead of replacing')
119
+ .action(async (file, options) => {
120
+ try {
121
+ // Get key from option or environment variable
122
+ const key = options.key || process.env.AGENTIO_KEY;
123
+ if (!key) {
124
+ throw new CliError(
125
+ 'INVALID_PARAMS',
126
+ 'No encryption key provided',
127
+ 'Provide --key option or set AGENTIO_KEY environment variable'
128
+ );
129
+ }
130
+
131
+ // Validate key
132
+ if (!/^[0-9a-fA-F]{64}$/.test(key)) {
133
+ throw new CliError(
134
+ 'INVALID_PARAMS',
135
+ 'Invalid encryption key format',
136
+ 'Key must be exactly 64 hexadecimal characters'
137
+ );
138
+ }
139
+
140
+ // Check file exists
141
+ const filePath = file.startsWith('/') ? file : join(process.cwd(), file);
142
+ if (!existsSync(filePath)) {
143
+ throw new CliError(
144
+ 'NOT_FOUND',
145
+ `File not found: ${filePath}`,
146
+ 'Provide a valid path to the exported configuration file'
147
+ );
148
+ }
149
+
150
+ // Read and parse the encrypted file
151
+ const encryptedContent = await readFile(filePath, 'utf-8');
152
+ let encrypted: { iv: string; tag: string; data: string };
153
+ try {
154
+ encrypted = JSON.parse(encryptedContent);
155
+ } catch {
156
+ throw new CliError(
157
+ 'INVALID_PARAMS',
158
+ 'Invalid file format',
159
+ 'The file does not appear to be a valid agentio export file'
160
+ );
161
+ }
162
+
163
+ // Decrypt
164
+ const derivedKey = deriveKeyFromPassword(key);
165
+ let exportData: ExportedData;
166
+ try {
167
+ const decrypted = decrypt(encrypted, derivedKey);
168
+ exportData = JSON.parse(decrypted);
169
+ } catch {
170
+ throw new CliError(
171
+ 'AUTH_FAILED',
172
+ 'Failed to decrypt configuration',
173
+ 'Check that you are using the correct encryption key'
174
+ );
175
+ }
176
+
177
+ // Validate version
178
+ if (exportData.version !== 1) {
179
+ throw new CliError(
180
+ 'INVALID_PARAMS',
181
+ `Unsupported export version: ${exportData.version}`,
182
+ 'This version of agentio may not support this export format'
183
+ );
184
+ }
185
+
186
+ if (options.merge) {
187
+ // Merge with existing config
188
+ const currentConfig = await loadConfig();
189
+ const currentCredentials = await getAllCredentials();
190
+
191
+ // Merge profiles
192
+ for (const [service, profiles] of Object.entries(exportData.config.profiles)) {
193
+ if (profiles) {
194
+ if (!currentConfig.profiles[service as keyof typeof currentConfig.profiles]) {
195
+ (currentConfig.profiles as Record<string, string[]>)[service] = [];
196
+ }
197
+ for (const profile of profiles) {
198
+ if (!(currentConfig.profiles as Record<string, string[]>)[service].includes(profile)) {
199
+ (currentConfig.profiles as Record<string, string[]>)[service].push(profile);
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // Merge defaults (only if not set)
206
+ for (const [service, defaultProfile] of Object.entries(exportData.config.defaults)) {
207
+ if (defaultProfile && !(currentConfig.defaults as Record<string, string | undefined>)[service]) {
208
+ (currentConfig.defaults as Record<string, string>)[service] = defaultProfile;
209
+ }
210
+ }
211
+
212
+ // Merge credentials
213
+ for (const [service, profiles] of Object.entries(exportData.credentials)) {
214
+ if (!currentCredentials[service]) {
215
+ currentCredentials[service] = {};
216
+ }
217
+ for (const [profile, creds] of Object.entries(profiles)) {
218
+ // Only add if not already exists
219
+ if (!currentCredentials[service][profile]) {
220
+ currentCredentials[service][profile] = creds;
221
+ }
222
+ }
223
+ }
224
+
225
+ await saveConfig(currentConfig);
226
+ await setAllCredentials(currentCredentials);
227
+ console.log('Configuration merged successfully');
228
+ } else {
229
+ // Replace existing config
230
+ await saveConfig(exportData.config);
231
+ await setAllCredentials(exportData.credentials);
232
+ console.log('Configuration imported successfully');
233
+ }
234
+ } catch (error) {
235
+ handleError(error);
236
+ }
237
+ });
238
+ }
@@ -1,6 +1,5 @@
1
1
  import { Command } from 'commander';
2
2
  import { google } from 'googleapis';
3
- import { createInterface } from 'readline';
4
3
  import { readFile } from 'fs/promises';
5
4
  import { setCredentials, removeCredentials, getCredentials } from '../auth/token-store';
6
5
  import { setProfile, removeProfile, listProfiles, getProfile } from '../config/config-manager';
@@ -8,24 +7,10 @@ import { performOAuthFlow } from '../auth/oauth';
8
7
  import { createGoogleAuth } from '../auth/token-manager';
9
8
  import { GChatClient } from '../services/gchat/client';
10
9
  import { CliError, handleError } from '../utils/errors';
11
- import { readStdin } from '../utils/stdin';
10
+ import { readStdin, prompt, resolveProfileName } from '../utils/stdin';
12
11
  import { printGChatSendResult, printGChatMessageList, printGChatMessage } from '../utils/output';
13
12
  import type { GChatCredentials, GChatWebhookCredentials, GChatOAuthCredentials } from '../types/gchat';
14
13
 
15
- function prompt(question: string): Promise<string> {
16
- const rl = createInterface({
17
- input: process.stdin,
18
- output: process.stderr,
19
- });
20
-
21
- return new Promise((resolve) => {
22
- rl.question(question, (answer) => {
23
- rl.close();
24
- resolve(answer.trim());
25
- });
26
- });
27
- }
28
-
29
14
  async function getGChatClient(profileName?: string): Promise<{ client: GChatClient; profile: string }> {
30
15
  const profile = await getProfile('gchat', profileName);
31
16
 
@@ -195,7 +180,7 @@ export function registerGChatCommands(program: Command): void {
195
180
  .option('--profile <name>', 'Profile name', 'default')
196
181
  .action(async (options) => {
197
182
  try {
198
- const profileName = options.profile;
183
+ const profileName = await resolveProfileName('gchat', options.profile);
199
184
 
200
185
  console.error('\nGoogle Chat Setup\n');
201
186
 
@@ -8,7 +8,7 @@ import { performOAuthFlow } from '../auth/oauth';
8
8
  import { GmailClient } from '../services/gmail/client';
9
9
  import { printMessageList, printMessage, printSendResult, printArchived, printMarked, raw } from '../utils/output';
10
10
  import { CliError, handleError } from '../utils/errors';
11
- import { readStdin } from '../utils/stdin';
11
+ import { readStdin, resolveProfileName } from '../utils/stdin';
12
12
  import type { GmailAttachment } from '../types/gmail';
13
13
 
14
14
  async function getGmailClient(profileName?: string): Promise<{ client: GmailClient; profile: string }> {
@@ -245,7 +245,7 @@ Query Syntax Examples:
245
245
  .option('--profile <name>', 'Profile name', 'default')
246
246
  .action(async (options) => {
247
247
  try {
248
- const profileName = options.profile;
248
+ const profileName = await resolveProfileName('gmail', options.profile);
249
249
 
250
250
  console.error(`Starting OAuth flow for Gmail profile "${profileName}"...`);
251
251
 
@@ -1,11 +1,10 @@
1
1
  import { Command } from 'commander';
2
- import { createInterface } from 'readline';
3
2
  import { setCredentials, removeCredentials, getCredentials, setCredentials as updateCredentials } from '../auth/token-store';
4
3
  import { setProfile, removeProfile, listProfiles, getProfile } from '../config/config-manager';
5
4
  import { performJiraOAuthFlow, refreshJiraToken, type AtlassianSite } from '../auth/jira-oauth';
6
5
  import { JiraClient } from '../services/jira/client';
7
6
  import { CliError, handleError } from '../utils/errors';
8
- import { readStdin } from '../utils/stdin';
7
+ import { readStdin, prompt, resolveProfileName } from '../utils/stdin';
9
8
  import {
10
9
  printJiraProjectList,
11
10
  printJiraIssueList,
@@ -16,20 +15,6 @@ import {
16
15
  } from '../utils/output';
17
16
  import type { JiraCredentials } from '../types/jira';
18
17
 
19
- function prompt(question: string): Promise<string> {
20
- const rl = createInterface({
21
- input: process.stdin,
22
- output: process.stderr,
23
- });
24
-
25
- return new Promise((resolve) => {
26
- rl.question(question, (answer) => {
27
- rl.close();
28
- resolve(answer.trim());
29
- });
30
- });
31
- }
32
-
33
18
  async function ensureValidToken(credentials: JiraCredentials, profile: string): Promise<JiraCredentials> {
34
19
  // Check if token is expired or about to expire (within 5 minutes)
35
20
  const bufferTime = 5 * 60 * 1000;
@@ -228,7 +213,7 @@ export function registerJiraCommands(program: Command): void {
228
213
  .option('--profile <name>', 'Profile name', 'default')
229
214
  .action(async (options) => {
230
215
  try {
231
- const profileName = options.profile;
216
+ const profileName = await resolveProfileName('jira', options.profile);
232
217
 
233
218
  console.error('\nšŸ”§ JIRA OAuth Setup\n');
234
219
 
@@ -1,28 +1,13 @@
1
1
  import { Command } from 'commander';
2
- import { createInterface } from 'readline';
3
2
  import { readFile } from 'fs/promises';
4
3
  import { setCredentials, removeCredentials, getCredentials } from '../auth/token-store';
5
4
  import { setProfile, removeProfile, listProfiles, getProfile } from '../config/config-manager';
6
5
  import { SlackClient } from '../services/slack/client';
7
6
  import { CliError, handleError } from '../utils/errors';
8
- import { readStdin } from '../utils/stdin';
7
+ import { readStdin, prompt, resolveProfileName } from '../utils/stdin';
9
8
  import { printSlackSendResult } from '../utils/output';
10
9
  import type { SlackCredentials, SlackWebhookCredentials } from '../types/slack';
11
10
 
12
- function prompt(question: string): Promise<string> {
13
- const rl = createInterface({
14
- input: process.stdin,
15
- output: process.stderr,
16
- });
17
-
18
- return new Promise((resolve) => {
19
- rl.question(question, (answer) => {
20
- rl.close();
21
- resolve(answer.trim());
22
- });
23
- });
24
- }
25
-
26
11
  async function getSlackClient(profileName?: string): Promise<{ client: SlackClient; profile: string }> {
27
12
  const profile = await getProfile('slack', profileName);
28
13
 
@@ -149,7 +134,7 @@ export function registerSlackCommands(program: Command): void {
149
134
  .option('--profile <name>', 'Profile name', 'default')
150
135
  .action(async (options) => {
151
136
  try {
152
- const profileName = options.profile;
137
+ const profileName = await resolveProfileName('slack', options.profile);
153
138
  await setupWebhookProfile(profileName);
154
139
  } catch (error) {
155
140
  handleError(error);
@@ -1,26 +1,11 @@
1
1
  import { Command } from 'commander';
2
- import { createInterface } from 'readline';
3
2
  import { setCredentials, removeCredentials, getCredentials } from '../auth/token-store';
4
3
  import { setProfile, removeProfile, listProfiles, getProfile } from '../config/config-manager';
5
4
  import { TelegramClient } from '../services/telegram/client';
6
5
  import { CliError, handleError } from '../utils/errors';
7
- import { readStdin } from '../utils/stdin';
6
+ import { readStdin, prompt, resolveProfileName } from '../utils/stdin';
8
7
  import type { TelegramCredentials, TelegramSendOptions } from '../types/telegram';
9
8
 
10
- function prompt(question: string): Promise<string> {
11
- const rl = createInterface({
12
- input: process.stdin,
13
- output: process.stderr,
14
- });
15
-
16
- return new Promise((resolve) => {
17
- rl.question(question, (answer) => {
18
- rl.close();
19
- resolve(answer.trim());
20
- });
21
- });
22
- }
23
-
24
9
  async function getTelegramClient(profileName?: string): Promise<{ client: TelegramClient; profile: string }> {
25
10
  const profile = await getProfile('telegram', profileName);
26
11
 
@@ -107,7 +92,7 @@ export function registerTelegramCommands(program: Command): void {
107
92
  .option('--profile <name>', 'Profile name', 'default')
108
93
  .action(async (options) => {
109
94
  try {
110
- const profileName = options.profile;
95
+ const profileName = await resolveProfileName('telegram', options.profile);
111
96
 
112
97
  console.error('\nšŸ“± Telegram Bot Setup\n');
113
98
 
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import { registerGChatCommands } from './commands/gchat';
6
6
  import { registerJiraCommands } from './commands/jira';
7
7
  import { registerSlackCommands } from './commands/slack';
8
8
  import { registerUpdateCommand } from './commands/update';
9
+ import { registerConfigCommands } from './commands/config';
9
10
 
10
11
  declare const BUILD_VERSION: string | undefined;
11
12
 
@@ -30,5 +31,6 @@ registerGChatCommands(program);
30
31
  registerJiraCommands(program);
31
32
  registerSlackCommands(program);
32
33
  registerUpdateCommand(program);
34
+ registerConfigCommands(program);
33
35
 
34
36
  program.parse();
@@ -1,3 +1,66 @@
1
+ import { createInterface } from 'readline';
2
+ import { getProfile } from '../config/config-manager';
3
+ import type { ServiceName } from '../types/config';
4
+
5
+ /**
6
+ * Prompt the user for input with a question.
7
+ */
8
+ export function prompt(question: string): Promise<string> {
9
+ const rl = createInterface({
10
+ input: process.stdin,
11
+ output: process.stderr,
12
+ });
13
+
14
+ return new Promise((resolve) => {
15
+ rl.question(question, (answer) => {
16
+ rl.close();
17
+ resolve(answer.trim());
18
+ });
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Prompt for yes/no confirmation.
24
+ */
25
+ export async function confirm(question: string): Promise<boolean> {
26
+ const answer = await prompt(`${question} (y/n): `);
27
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
28
+ }
29
+
30
+ /**
31
+ * Resolve profile name, checking for existing profiles and prompting for override.
32
+ * If the profile already exists, asks the user whether to override or choose a new name.
33
+ */
34
+ export async function resolveProfileName(
35
+ service: ServiceName,
36
+ requestedName: string
37
+ ): Promise<string> {
38
+ const existingProfile = await getProfile(service, requestedName);
39
+
40
+ if (!existingProfile) {
41
+ // Profile doesn't exist, use the requested name
42
+ return requestedName;
43
+ }
44
+
45
+ // Profile exists, ask if user wants to override
46
+ console.error(`\nProfile "${requestedName}" already exists for ${service}.`);
47
+ const shouldOverride = await confirm('Do you want to override it?');
48
+
49
+ if (shouldOverride) {
50
+ return requestedName;
51
+ }
52
+
53
+ // Ask for a new profile name
54
+ const newName = await prompt('Enter a new profile name: ');
55
+
56
+ if (!newName) {
57
+ throw new Error('Profile name is required');
58
+ }
59
+
60
+ // Recursively check if the new name also exists
61
+ return resolveProfileName(service, newName);
62
+ }
63
+
1
64
  export async function readStdin(): Promise<string | null> {
2
65
  // Check if stdin is a TTY (interactive terminal)
3
66
  if (process.stdin.isTTY) {