@closedloop-ai/cli 0.2.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.
Files changed (70) hide show
  1. package/dist/__tests__/cli.test.js +37 -0
  2. package/dist/__tests__/cli.test.js.map +1 -0
  3. package/dist/__tests__/config.test.js +33 -0
  4. package/dist/__tests__/config.test.js.map +1 -0
  5. package/dist/__tests__/errors.test.js +109 -0
  6. package/dist/__tests__/errors.test.js.map +1 -0
  7. package/dist/__tests__/setup.js +20 -0
  8. package/dist/__tests__/setup.js.map +1 -0
  9. package/dist/__tests__/validation.test.js +124 -0
  10. package/dist/__tests__/validation.test.js.map +1 -0
  11. package/dist/api/auth.js +27 -0
  12. package/dist/api/auth.js.map +1 -0
  13. package/dist/api/feedback.js +70 -0
  14. package/dist/api/feedback.js.map +1 -0
  15. package/dist/api/input.js +60 -0
  16. package/dist/api/input.js.map +1 -0
  17. package/dist/api/usage.js +49 -0
  18. package/dist/api/usage.js.map +1 -0
  19. package/dist/cli.js +156 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/commands/config.js +186 -0
  22. package/dist/commands/config.js.map +1 -0
  23. package/dist/commands/feedback.js +234 -0
  24. package/dist/commands/feedback.js.map +1 -0
  25. package/dist/commands/input.js +370 -0
  26. package/dist/commands/input.js.map +1 -0
  27. package/dist/commands/list.js +81 -0
  28. package/dist/commands/list.js.map +1 -0
  29. package/dist/commands/status.js +93 -0
  30. package/dist/commands/status.js.map +1 -0
  31. package/dist/commands/submit.js +225 -0
  32. package/dist/commands/submit.js.map +1 -0
  33. package/dist/commands/usage.js +168 -0
  34. package/dist/commands/usage.js.map +1 -0
  35. package/dist/config/constants.js +37 -0
  36. package/dist/config/constants.js.map +1 -0
  37. package/dist/config/index.js +84 -0
  38. package/dist/config/index.js.map +1 -0
  39. package/dist/types/index.js +3 -0
  40. package/dist/types/index.js.map +1 -0
  41. package/dist/utils/config.js +55 -0
  42. package/dist/utils/config.js.map +1 -0
  43. package/dist/utils/errors.js +70 -0
  44. package/dist/utils/errors.js.map +1 -0
  45. package/dist/utils/validation.js +133 -0
  46. package/dist/utils/validation.js.map +1 -0
  47. package/jest.config.js +14 -0
  48. package/package.json +42 -0
  49. package/src/__tests__/cli.test.ts +44 -0
  50. package/src/__tests__/config.test.ts +38 -0
  51. package/src/__tests__/errors.test.ts +132 -0
  52. package/src/__tests__/setup.ts +21 -0
  53. package/src/__tests__/validation.test.ts +156 -0
  54. package/src/api/auth.ts +21 -0
  55. package/src/api/feedback.ts +64 -0
  56. package/src/api/input.ts +55 -0
  57. package/src/api/usage.ts +45 -0
  58. package/src/cli.ts +158 -0
  59. package/src/commands/config.ts +192 -0
  60. package/src/commands/feedback.ts +250 -0
  61. package/src/commands/input.ts +389 -0
  62. package/src/commands/list.ts +84 -0
  63. package/src/commands/status.ts +96 -0
  64. package/src/commands/submit.ts +214 -0
  65. package/src/config/constants.ts +36 -0
  66. package/src/config/index.ts +59 -0
  67. package/src/types/index.ts +119 -0
  68. package/src/utils/errors.ts +98 -0
  69. package/src/utils/validation.ts +166 -0
  70. package/tsconfig.json +25 -0
@@ -0,0 +1,21 @@
1
+ import axios from 'axios';
2
+ import { CONFIG } from '../config/constants';
3
+ import { CliError, handleApiError, ERROR_CODES } from '../utils/errors';
4
+
5
+ export async function validateApiKey(apiKey: string): Promise<boolean> {
6
+ try {
7
+ const response = await axios.get(`${CONFIG.API_BASE_URL}/health`, {
8
+ headers: {
9
+ 'Authorization': `Bearer ${apiKey}`
10
+ },
11
+ timeout: CONFIG.TIMEOUT_MS
12
+ });
13
+
14
+ return response.status === 200;
15
+ } catch (error: any) {
16
+ if (error.response?.status === 401) {
17
+ return false;
18
+ }
19
+ throw handleApiError(error);
20
+ }
21
+ }
@@ -0,0 +1,64 @@
1
+ import axios from 'axios';
2
+ import { config } from '../config';
3
+ import { FeedbackData, ListResponse, CommandOptions, InputData } from '../types';
4
+ import { CONFIG, MESSAGES } from '../config/constants';
5
+ import { CliError, handleApiError, ERROR_CODES } from '../utils/errors';
6
+
7
+ const getClient = () => {
8
+ const apiKey = config.get('apiKey');
9
+
10
+ if (!apiKey) {
11
+ throw new CliError(MESSAGES.NO_API_KEY, ERROR_CODES.NO_API_KEY);
12
+ }
13
+
14
+ return axios.create({
15
+ baseURL: CONFIG.API_BASE_URL,
16
+ headers: {
17
+ 'Authorization': `Bearer ${apiKey}`,
18
+ 'Content-Type': 'application/json',
19
+ 'User-Agent': CONFIG.USER_AGENT
20
+ },
21
+ timeout: CONFIG.TIMEOUT_MS
22
+ });
23
+ };
24
+
25
+ export async function submitFeedback(data: Partial<InputData>): Promise<{ id: string }> {
26
+ try {
27
+ const client = getClient();
28
+ const response = await client.post('/feedbacks', data);
29
+ return response.data.data;
30
+ } catch (error) {
31
+ throw handleApiError(error);
32
+ }
33
+ }
34
+
35
+ export async function listFeedbacks(params: CommandOptions, isNew = false): Promise<ListResponse<FeedbackData>> {
36
+ try {
37
+ const client = getClient();
38
+ const endpoint = isNew ? '/feedbacks/new' : '/feedbacks';
39
+ const response = await client.get(endpoint, { params });
40
+ return response.data.data;
41
+ } catch (error) {
42
+ throw handleApiError(error);
43
+ }
44
+ }
45
+
46
+ export async function getFeedbackStatus(id: string): Promise<FeedbackData> {
47
+ try {
48
+ const client = getClient();
49
+ const response = await client.get(`/feedbacks/new/${id}`);
50
+ return response.data.data;
51
+ } catch (error) {
52
+ throw handleApiError(error);
53
+ }
54
+ }
55
+
56
+ export async function getFeedbackDetail(id: string): Promise<FeedbackData> {
57
+ try {
58
+ const client = getClient();
59
+ const response = await client.get(`/feedbacks/${id}`);
60
+ return response.data.data;
61
+ } catch (error) {
62
+ throw handleApiError(error);
63
+ }
64
+ }
@@ -0,0 +1,55 @@
1
+ import axios from 'axios';
2
+ import { config } from '../config';
3
+ import { InputData, ListResponse, CommandOptions } from '../types';
4
+ import { CONFIG, MESSAGES } from '../config/constants';
5
+ import { CliError, handleApiError, ERROR_CODES } from '../utils/errors';
6
+
7
+ const getClient = () => {
8
+ const apiKey = config.get('apiKey');
9
+
10
+ if (!apiKey) {
11
+ throw new CliError(MESSAGES.NO_API_KEY, ERROR_CODES.NO_API_KEY);
12
+ }
13
+
14
+ return axios.create({
15
+ baseURL: CONFIG.API_BASE_URL,
16
+ headers: {
17
+ 'Authorization': `Bearer ${apiKey}`,
18
+ 'Content-Type': 'application/json',
19
+ 'User-Agent': CONFIG.USER_AGENT
20
+ },
21
+ timeout: CONFIG.TIMEOUT_MS
22
+ });
23
+ };
24
+
25
+ export async function listInputs(page: number = 1, limit: number = 20): Promise<ListResponse<InputData>> {
26
+ try {
27
+ const client = getClient();
28
+ const response = await client.get('/inputs', {
29
+ params: { page, limit }
30
+ });
31
+ return response.data.data;
32
+ } catch (error) {
33
+ throw handleApiError(error);
34
+ }
35
+ }
36
+
37
+ export async function getInputDetail(id: string): Promise<InputData> {
38
+ try {
39
+ const client = getClient();
40
+ const response = await client.get(`/inputs/${id}`);
41
+ return response.data.data;
42
+ } catch (error) {
43
+ throw handleApiError(error);
44
+ }
45
+ }
46
+
47
+ export async function getInputStatus(id: string): Promise<InputData> {
48
+ try {
49
+ const client = getClient();
50
+ const response = await client.get(`/inputs/${id}`);
51
+ return response.data.data;
52
+ } catch (error) {
53
+ throw handleApiError(error);
54
+ }
55
+ }
@@ -0,0 +1,45 @@
1
+ import axios from 'axios';
2
+ import { config } from '../config';
3
+ import { UsageData, ListResponse } from '../types';
4
+ import { CONFIG, MESSAGES } from '../config/constants';
5
+ import { CliError, handleApiError, ERROR_CODES } from '../utils/errors';
6
+
7
+ const getClient = () => {
8
+ const apiKey = config.get('apiKey');
9
+
10
+ if (!apiKey) {
11
+ throw new CliError(MESSAGES.NO_API_KEY, ERROR_CODES.NO_API_KEY);
12
+ }
13
+
14
+ return axios.create({
15
+ baseURL: CONFIG.API_BASE_URL,
16
+ headers: {
17
+ 'Authorization': `Bearer ${apiKey}`,
18
+ 'Content-Type': 'application/json',
19
+ 'User-Agent': CONFIG.USER_AGENT
20
+ },
21
+ timeout: CONFIG.TIMEOUT_MS
22
+ });
23
+ };
24
+
25
+ export async function listUsage(page: number = 1, limit: number = 20): Promise<ListResponse<UsageData>> {
26
+ try {
27
+ const client = getClient();
28
+ const response = await client.get('/usage', {
29
+ params: { page, limit }
30
+ });
31
+ return response.data.data;
32
+ } catch (error) {
33
+ throw handleApiError(error);
34
+ }
35
+ }
36
+
37
+ export async function getUsageDetail(id: string): Promise<UsageData> {
38
+ try {
39
+ const client = getClient();
40
+ const response = await client.get(`/usage/${id}`);
41
+ return response.data.data;
42
+ } catch (error) {
43
+ throw handleApiError(error);
44
+ }
45
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { handleInputCommand } from './commands/input';
6
+ import { handleFeedbackCommand } from './commands/feedback';
7
+ import { configCommand } from './commands/config';
8
+ import { CliError, handleApiError } from './utils/errors';
9
+ import { CommandOptions } from './types';
10
+
11
+ // Create the main program
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('cl')
16
+ .description('ClosedLoop AI CLI - AI-powered customer feedback analysis for product management')
17
+ .version('0.2.0')
18
+ .addHelpText('before', chalk.blue(`
19
+ ╔══════════════════════════════════════════════════════════════════════════════╗
20
+ ║ ClosedLoop AI CLI v0.2.0 ║
21
+ ║ AI-Powered Customer Feedback Analysis ║
22
+ ╚══════════════════════════════════════════════════════════════════════════════╝
23
+ `))
24
+ .addHelpText('after', chalk.black(`
25
+ Commands:
26
+ input [content] Submit customer input for AI analysis
27
+ input List all customer inputs
28
+ input <id> Show specific input details
29
+
30
+ feedback List AI-generated feedback
31
+ feedback <id> Show specific feedback details
32
+
33
+ config Show current configuration
34
+ config set --api-key <key> Set API key
35
+
36
+ version Show version information
37
+ help [command] Show help for a command
38
+
39
+ Examples:
40
+ $ cl input "Dashboard is confusing"
41
+ $ cl input
42
+ $ cl input 74e3dd87-878f-41cf-8e5a-87527bbf7770
43
+ $ cl feedback
44
+ $ cl feedback 2ea8f556-052b-4f5c-bf86-833780b3d00d
45
+ $ cl config set --api-key your-api-key-here
46
+
47
+ Pagination:
48
+ $ cl input --page 2 --limit 10
49
+ $ cl feedback --page 2
50
+
51
+ Get API keys for free at https://closedloop.sh
52
+ `));
53
+
54
+ // Add input command
55
+ program
56
+ .command('input [content]')
57
+ .description('Submit customer input for AI analysis or list inputs')
58
+ .option('-t, --title <title>', 'Title for this input')
59
+ .option('-u, --url <url>', 'Source URL (support ticket, survey, etc.)')
60
+ .option('-c, --customer <id>', 'Customer identifier')
61
+ .option('-n, --name <name>', 'Name of person who provided the input')
62
+ .option('-e, --email <email>', 'Email of person who provided the input')
63
+ .option('--interactive', 'Fill in details step by step')
64
+ .option('-w, --wait', 'Wait until processing is completed')
65
+ .option('--json', 'Output in JSON format')
66
+ .option('--page <number>', 'Page number for list', '1')
67
+ .option('--limit <number>', 'Items per page for list', '20')
68
+ .option('--status <status>', 'Filter by status (processing, completed, failed)')
69
+ .option('--search <query>', 'Search in input content')
70
+ .action(async (content, options: CommandOptions) => {
71
+ try {
72
+ await handleInputCommand(content, options);
73
+ } catch (error: any) {
74
+ handleError(error, options);
75
+ }
76
+ });
77
+
78
+ // Add feedback command
79
+ program
80
+ .command('feedback [id]')
81
+ .description('List AI-generated feedback or show specific feedback details')
82
+ .option('--json', 'Output in JSON format')
83
+ .option('--page <number>', 'Page number for list', '1')
84
+ .option('--limit <number>', 'Items per page for list', '20')
85
+ .option('--search <query>', 'Search feedback by content')
86
+ .action(async (id, options: CommandOptions) => {
87
+ try {
88
+ await handleFeedbackCommand(id, options);
89
+ } catch (error: any) {
90
+ handleError(error, options);
91
+ }
92
+ });
93
+
94
+ // Add config command
95
+ program.addCommand(configCommand);
96
+
97
+ // Add version command
98
+ program
99
+ .command('version')
100
+ .description('Show version information')
101
+ .action(() => {
102
+ console.log(chalk.blue('ClosedLoop AI CLI v0.2.0'));
103
+ console.log(chalk.gray('AI-Powered Customer Feedback Analysis'));
104
+ console.log(chalk.gray('Get started: https://closedloop.sh'));
105
+ });
106
+
107
+ // Add help command
108
+ program
109
+ .command('help [command]')
110
+ .description('Show help for a command')
111
+ .action((command) => {
112
+ if (command) {
113
+ program.outputHelp();
114
+ } else {
115
+ program.outputHelp();
116
+ }
117
+ });
118
+
119
+ // Error handling function
120
+ function handleError(error: any, options: CommandOptions) {
121
+ if (options.json) {
122
+ console.log(JSON.stringify({
123
+ success: false,
124
+ error: error.message,
125
+ code: error.code || 'UNKNOWN_ERROR',
126
+ details: error.response?.data || 'Unknown error occurred'
127
+ }, null, 2));
128
+ } else {
129
+ if (error instanceof CliError) {
130
+ console.error(chalk.red('❌ Error:'), error.message);
131
+ if (error.code === 'NO_API_KEY') {
132
+ console.log(chalk.black('\n💡 To fix:'));
133
+ console.log(chalk.black(' 1. Get your free API key at: https://closedloop.sh'));
134
+ console.log(chalk.black(' 2. Run: cl config set --api-key <your-key>'));
135
+ }
136
+ } else {
137
+ const cliError = handleApiError(error);
138
+ console.error(chalk.red('❌ Error:'), cliError.message);
139
+ console.log(chalk.black('\n💡 Get help: cl --help'));
140
+ }
141
+ }
142
+ process.exit(error.exitCode || 1);
143
+ }
144
+
145
+ // Handle unknown commands
146
+ program.on('command:*', (operands) => {
147
+ console.error(chalk.red(`Unknown command: ${operands[0]}`));
148
+ console.log(chalk.gray('Run "cl --help" to see available commands'));
149
+ process.exit(1);
150
+ });
151
+
152
+ // Parse command line arguments
153
+ program.parse();
154
+
155
+ // If no command provided, show help
156
+ if (!process.argv.slice(2).length) {
157
+ program.outputHelp();
158
+ }
@@ -0,0 +1,192 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { config } from '../config';
5
+ import { validateApiKey } from '../api/auth';
6
+ import { CommandOptions } from '../types';
7
+ import { CONFIG, MESSAGES, URLS } from '../config/constants';
8
+ import { validateApiKey as validateApiKeyFormat } from '../utils/validation';
9
+
10
+ // Create config command
11
+ export const configCommand = new Command('config')
12
+ .description('Configure your API key and settings')
13
+ .option('--set-api-key <key>', 'Set your API key')
14
+ .option('--set-team <team>', 'Set your team (future feature)')
15
+ .option('--json', 'Output in JSON format')
16
+ .action(async (options: CommandOptions) => {
17
+ try {
18
+ await handleConfig(options);
19
+ } catch (error: any) {
20
+ handleError(error, options);
21
+ }
22
+ });
23
+
24
+ // Create config subcommands
25
+ const configSetCommand = new Command('set')
26
+ .description('Set configuration values')
27
+ .option('--api-key <key>', 'Set API key')
28
+ .option('--team <team>', 'Set team (future feature)')
29
+ .action(async (options: CommandOptions) => {
30
+ try {
31
+ await handleConfigSet(options);
32
+ } catch (error: any) {
33
+ handleError(error, options);
34
+ }
35
+ });
36
+
37
+ const configShowCommand = new Command('show')
38
+ .description('Show current configuration')
39
+ .option('--json', 'Output in JSON format')
40
+ .action(async (options: CommandOptions) => {
41
+ try {
42
+ await handleConfigShow(options);
43
+ } catch (error: any) {
44
+ handleError(error, options);
45
+ }
46
+ });
47
+
48
+ // Add subcommands to main config command
49
+ configCommand.addCommand(configSetCommand);
50
+ configCommand.addCommand(configShowCommand);
51
+
52
+ // Helper functions
53
+ async function handleConfig(options: CommandOptions) {
54
+ const currentConfig = config.get();
55
+
56
+ if (options.setApiKey) {
57
+ await setApiKey(options.setApiKey, options);
58
+ return;
59
+ }
60
+
61
+ if (options.setTeam) {
62
+ console.log(chalk.yellow('⚠️ Team configuration is not yet implemented'));
63
+ console.log(chalk.black('This feature will be available in a future update.'));
64
+ return;
65
+ }
66
+
67
+ // Show current config
68
+ await showConfig(currentConfig, options);
69
+ }
70
+
71
+ async function handleConfigSet(options: CommandOptions) {
72
+ if (options.apiKey) {
73
+ await setApiKey(options.apiKey, options);
74
+ } else if (options.team) {
75
+ console.log(chalk.yellow('⚠️ Team configuration is not yet implemented'));
76
+ console.log(chalk.black('This feature will be available in a future update.'));
77
+ } else {
78
+ console.log(chalk.red('❌ No configuration option specified'));
79
+ console.log(chalk.black('Use --api-key or --team to set values'));
80
+ process.exit(1);
81
+ }
82
+ }
83
+
84
+ async function handleConfigShow(options: CommandOptions) {
85
+ const currentConfig = config.get();
86
+ await showConfig(currentConfig, options);
87
+ }
88
+
89
+ async function setApiKey(apiKey: string, options: CommandOptions) {
90
+ try {
91
+ // Validate API key format
92
+ const validatedApiKey = validateApiKeyFormat(apiKey);
93
+
94
+ // Validate API key by making a test request
95
+ const spinner = ora('Validating API key...').start();
96
+
97
+ try {
98
+ await validateApiKey(validatedApiKey);
99
+ spinner.succeed('API key validated successfully!');
100
+
101
+ // Save the API key
102
+ config.set('apiKey', validatedApiKey);
103
+
104
+ if (options.json) {
105
+ console.log(JSON.stringify({
106
+ success: true,
107
+ message: 'API key set successfully',
108
+ data: {
109
+ apiKey: validatedApiKey.substring(0, 8) + '...',
110
+ configFile: config.getConfigPath(),
111
+ configured: true
112
+ }
113
+ }, null, 2));
114
+ } else {
115
+ console.log(chalk.green('✅ API key set successfully!'));
116
+ console.log(chalk.black(`Config file: ${config.getConfigPath()}`));
117
+ }
118
+ } catch (error: any) {
119
+ spinner.fail('API key validation failed');
120
+
121
+ if (error.response?.status === 401) {
122
+ console.error(chalk.red('❌ Invalid API key'));
123
+ console.log(chalk.black('\n💡 To get a valid API key:'));
124
+ console.log(chalk.black(' 1. Visit: https://closedloop.sh'));
125
+ console.log(chalk.black(' 2. Sign up or log in'));
126
+ console.log(chalk.black(' 3. Generate an API key'));
127
+ console.log(chalk.black(' 4. Run: cl config set-api-key <your-key>'));
128
+ } else {
129
+ console.error(chalk.red('❌ Validation Error:'), error.message);
130
+ console.log(chalk.black('\n💡 Check your internet connection and try again'));
131
+ }
132
+ process.exit(1);
133
+ }
134
+ } catch (error: any) {
135
+ console.error(chalk.red('❌ Invalid API key. Get your free API key at https://closedloop.sh'));
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ async function showConfig(currentConfig: any, options: CommandOptions) {
141
+ if (options.json) {
142
+ console.log(JSON.stringify({
143
+ success: true,
144
+ data: {
145
+ apiKey: currentConfig.apiKey ? currentConfig.apiKey.substring(0, 8) + '...' : null,
146
+ configFile: config.getConfigPath(),
147
+ configured: !!currentConfig.apiKey
148
+ }
149
+ }, null, 2));
150
+ return;
151
+ }
152
+
153
+ console.log(chalk.blue('\n⚙️ Configuration:'));
154
+ console.log(chalk.black('─'.repeat(40)));
155
+
156
+ if (currentConfig.apiKey) {
157
+ console.log(chalk.blue('API Key:'), chalk.green('✓ Configured'));
158
+ } else {
159
+ console.log(chalk.blue('API Key:'), chalk.red('✗ Not configured'));
160
+ console.log(chalk.black('Run: cl config set-api-key <your-key>'));
161
+ }
162
+
163
+ console.log(chalk.blue('Config File:'), config.getConfigPath());
164
+
165
+ if (!currentConfig.apiKey) {
166
+ console.log(chalk.yellow('\n💡 Get started:'));
167
+ console.log(chalk.black(' 1. Get your free API key at: https://closedloop.sh'));
168
+ console.log(chalk.black(' 2. Run: cl config set-api-key <your-key>'));
169
+ console.log(chalk.black(' 3. Submit your first input: cl input "Your feedback here"'));
170
+ }
171
+ }
172
+
173
+ function handleError(error: any, options: CommandOptions) {
174
+ if (options.json) {
175
+ console.log(JSON.stringify({
176
+ success: false,
177
+ error: error.message,
178
+ details: error.response?.data || 'Unknown error occurred'
179
+ }, null, 2));
180
+ } else {
181
+ if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
182
+ console.error(chalk.red('❌ Connection Error:'), 'Cannot connect to ClosedLoop AI servers');
183
+ console.log(chalk.black('\n💡 Check your internet connection and try again'));
184
+ } else {
185
+ console.error(chalk.red('❌ Error:'), error.message);
186
+ console.log(chalk.black('\n💡 Get help: cl --help'));
187
+ }
188
+ }
189
+ process.exit(1);
190
+ }
191
+
192
+ export { configCommand as default };