@closedloop-ai/cli 0.2.0 → 0.2.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/CONTRIBUTING.md +437 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/docs/CLI_COMMANDS.md +505 -0
- package/package.json +49 -26
- package/packages/cli/dist/api/team.js +45 -0
- package/packages/cli/dist/api/team.js.map +1 -0
- package/{dist → packages/cli/dist}/cli.js +7 -0
- package/packages/cli/dist/cli.js.map +1 -0
- package/packages/cli/dist/commands/team.js +44 -0
- package/packages/cli/dist/commands/team.js.map +1 -0
- package/dist/cli.js.map +0 -1
- package/jest.config.js +0 -14
- package/src/__tests__/cli.test.ts +0 -44
- package/src/__tests__/config.test.ts +0 -38
- package/src/__tests__/errors.test.ts +0 -132
- package/src/__tests__/setup.ts +0 -21
- package/src/__tests__/validation.test.ts +0 -156
- package/src/api/auth.ts +0 -21
- package/src/api/feedback.ts +0 -64
- package/src/api/input.ts +0 -55
- package/src/api/usage.ts +0 -45
- package/src/cli.ts +0 -158
- package/src/commands/config.ts +0 -192
- package/src/commands/feedback.ts +0 -250
- package/src/commands/input.ts +0 -389
- package/src/commands/list.ts +0 -84
- package/src/commands/status.ts +0 -96
- package/src/commands/submit.ts +0 -214
- package/src/config/constants.ts +0 -36
- package/src/config/index.ts +0 -59
- package/src/types/index.ts +0 -119
- package/src/utils/errors.ts +0 -98
- package/src/utils/validation.ts +0 -166
- package/tsconfig.json +0 -25
- /package/{dist → packages/cli/dist}/__tests__/cli.test.js +0 -0
- /package/{dist → packages/cli/dist}/__tests__/cli.test.js.map +0 -0
- /package/{dist → packages/cli/dist}/__tests__/config.test.js +0 -0
- /package/{dist → packages/cli/dist}/__tests__/config.test.js.map +0 -0
- /package/{dist → packages/cli/dist}/__tests__/errors.test.js +0 -0
- /package/{dist → packages/cli/dist}/__tests__/errors.test.js.map +0 -0
- /package/{dist → packages/cli/dist}/__tests__/setup.js +0 -0
- /package/{dist → packages/cli/dist}/__tests__/setup.js.map +0 -0
- /package/{dist → packages/cli/dist}/__tests__/validation.test.js +0 -0
- /package/{dist → packages/cli/dist}/__tests__/validation.test.js.map +0 -0
- /package/{dist → packages/cli/dist}/api/auth.js +0 -0
- /package/{dist → packages/cli/dist}/api/auth.js.map +0 -0
- /package/{dist → packages/cli/dist}/api/feedback.js +0 -0
- /package/{dist → packages/cli/dist}/api/feedback.js.map +0 -0
- /package/{dist → packages/cli/dist}/api/input.js +0 -0
- /package/{dist → packages/cli/dist}/api/input.js.map +0 -0
- /package/{dist → packages/cli/dist}/api/usage.js +0 -0
- /package/{dist → packages/cli/dist}/api/usage.js.map +0 -0
- /package/{dist → packages/cli/dist}/commands/config.js +0 -0
- /package/{dist → packages/cli/dist}/commands/config.js.map +0 -0
- /package/{dist → packages/cli/dist}/commands/feedback.js +0 -0
- /package/{dist → packages/cli/dist}/commands/feedback.js.map +0 -0
- /package/{dist → packages/cli/dist}/commands/input.js +0 -0
- /package/{dist → packages/cli/dist}/commands/input.js.map +0 -0
- /package/{dist → packages/cli/dist}/commands/list.js +0 -0
- /package/{dist → packages/cli/dist}/commands/list.js.map +0 -0
- /package/{dist → packages/cli/dist}/commands/status.js +0 -0
- /package/{dist → packages/cli/dist}/commands/status.js.map +0 -0
- /package/{dist → packages/cli/dist}/commands/submit.js +0 -0
- /package/{dist → packages/cli/dist}/commands/submit.js.map +0 -0
- /package/{dist → packages/cli/dist}/commands/usage.js +0 -0
- /package/{dist → packages/cli/dist}/commands/usage.js.map +0 -0
- /package/{dist → packages/cli/dist}/config/constants.js +0 -0
- /package/{dist → packages/cli/dist}/config/constants.js.map +0 -0
- /package/{dist → packages/cli/dist}/config/index.js +0 -0
- /package/{dist → packages/cli/dist}/config/index.js.map +0 -0
- /package/{dist → packages/cli/dist}/types/index.js +0 -0
- /package/{dist → packages/cli/dist}/types/index.js.map +0 -0
- /package/{dist → packages/cli/dist}/utils/config.js +0 -0
- /package/{dist → packages/cli/dist}/utils/config.js.map +0 -0
- /package/{dist → packages/cli/dist}/utils/errors.js +0 -0
- /package/{dist → packages/cli/dist}/utils/errors.js.map +0 -0
- /package/{dist → packages/cli/dist}/utils/validation.js +0 -0
- /package/{dist → packages/cli/dist}/utils/validation.js.map +0 -0
package/src/commands/submit.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import { submitFeedback, getFeedbackStatus } from '../api/feedback';
|
|
6
|
-
import { CommandOptions, InputData } from '../types';
|
|
7
|
-
import { CONFIG, MESSAGES } from '../config/constants';
|
|
8
|
-
|
|
9
|
-
export const submitCommand = new Command('submit')
|
|
10
|
-
.description('Submit customer input for AI analysis and insight generation')
|
|
11
|
-
.argument('[content]', 'Customer input content to analyze')
|
|
12
|
-
.option('-t, --title <title>', 'Title for this customer input')
|
|
13
|
-
.option('-u, --url <url>', 'Where this input came from (support ticket, survey, etc.)')
|
|
14
|
-
.option('-c, --customer <id>', 'Customer identifier')
|
|
15
|
-
.option('-n, --name <name>', 'Name of person who provided the input')
|
|
16
|
-
.option('-e, --email <email>', 'Email of person who provided the input')
|
|
17
|
-
.option('--interactive', 'Fill in details step by step')
|
|
18
|
-
.option('-w, --wait', 'Wait until processing is completed')
|
|
19
|
-
.option('--json', 'Output in JSON format')
|
|
20
|
-
.action(async (content, options: CommandOptions) => {
|
|
21
|
-
try {
|
|
22
|
-
let feedbackData: Partial<InputData> = {};
|
|
23
|
-
|
|
24
|
-
if (options.interactive || !content) {
|
|
25
|
-
// Interactive mode
|
|
26
|
-
const answers = await inquirer.prompt([
|
|
27
|
-
{
|
|
28
|
-
type: 'input',
|
|
29
|
-
name: 'content',
|
|
30
|
-
message: 'What customer input would you like to analyze?',
|
|
31
|
-
validate: (input) => input.length >= 10 || 'Input must be at least 10 characters',
|
|
32
|
-
default: content
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
type: 'input',
|
|
36
|
-
name: 'title',
|
|
37
|
-
message: 'What should we call this input? (optional):',
|
|
38
|
-
default: options.title
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
type: 'input',
|
|
42
|
-
name: 'sourceUrl',
|
|
43
|
-
message: 'Where did this come from? (support ticket, survey, etc.) (optional):',
|
|
44
|
-
default: options.url
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
type: 'input',
|
|
48
|
-
name: 'customerId',
|
|
49
|
-
message: 'Customer identifier (optional):',
|
|
50
|
-
default: options.customer
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
type: 'input',
|
|
54
|
-
name: 'reporterName',
|
|
55
|
-
message: 'Who provided this input? (optional):',
|
|
56
|
-
default: options.name
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
type: 'input',
|
|
60
|
-
name: 'reporterEmail',
|
|
61
|
-
message: 'Their email address (optional):',
|
|
62
|
-
default: options.email
|
|
63
|
-
}
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
feedbackData = {
|
|
67
|
-
content: answers.content,
|
|
68
|
-
title: answers.title,
|
|
69
|
-
source_url: answers.sourceUrl,
|
|
70
|
-
customer_id: answers.customerId,
|
|
71
|
-
reporter_name: answers.reporterName,
|
|
72
|
-
reporter_email: answers.reporterEmail
|
|
73
|
-
};
|
|
74
|
-
} else {
|
|
75
|
-
// Command line mode
|
|
76
|
-
feedbackData = {
|
|
77
|
-
content,
|
|
78
|
-
title: options.title,
|
|
79
|
-
source_url: options.url,
|
|
80
|
-
customer_id: options.customer,
|
|
81
|
-
reporter_name: options.name,
|
|
82
|
-
reporter_email: options.email
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const spinner = ora('Submitting customer input for AI analysis...').start();
|
|
87
|
-
|
|
88
|
-
const result = await submitFeedback(feedbackData);
|
|
89
|
-
|
|
90
|
-
if (options.wait) {
|
|
91
|
-
// Wait for processing to complete
|
|
92
|
-
if (options.json) {
|
|
93
|
-
// For JSON, we'll output the final result after waiting
|
|
94
|
-
} else {
|
|
95
|
-
console.log(chalk.green('✅ Customer input submitted successfully!'));
|
|
96
|
-
console.log(chalk.blue(`Submission ID: ${result.id}`));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const waitSpinner = ora('Processing...').start();
|
|
100
|
-
let attempts = 0;
|
|
101
|
-
const maxAttempts = CONFIG.MAX_RETRY_ATTEMPTS;
|
|
102
|
-
|
|
103
|
-
while (attempts < maxAttempts) {
|
|
104
|
-
await new Promise(resolve => setTimeout(resolve, CONFIG.POLL_INTERVAL_MS));
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const statusResult = await getFeedbackStatus(result.id);
|
|
108
|
-
|
|
109
|
-
if (statusResult.status === 'completed') {
|
|
110
|
-
waitSpinner.succeed('Processing completed!');
|
|
111
|
-
|
|
112
|
-
if (options.json) {
|
|
113
|
-
console.log(JSON.stringify({ success: true, data: statusResult }, null, 2));
|
|
114
|
-
} else {
|
|
115
|
-
console.log(chalk.blue(`Feedback generated: ${statusResult.feedbackCount || 0}`));
|
|
116
|
-
console.log(chalk.blue(`Credits used: ${statusResult.creditsConsumed || 0}`));
|
|
117
|
-
if ((statusResult.feedbackCount || 0) > 0) {
|
|
118
|
-
console.log(chalk.gray('Run "cl list" to see all feedback'));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
} else if (statusResult.status === 'failed') {
|
|
123
|
-
waitSpinner.fail('Processing failed');
|
|
124
|
-
if (options.json) {
|
|
125
|
-
console.log(JSON.stringify({ success: false, error: 'Processing failed', data: statusResult }, null, 2));
|
|
126
|
-
} else {
|
|
127
|
-
console.log(chalk.red('\n❌ Processing failed. Check the status for more details.'));
|
|
128
|
-
}
|
|
129
|
-
break;
|
|
130
|
-
} else {
|
|
131
|
-
// Still processing
|
|
132
|
-
waitSpinner.text = `Processing... (${statusResult.status})`;
|
|
133
|
-
}
|
|
134
|
-
} catch (error) {
|
|
135
|
-
// Continue waiting on API errors
|
|
136
|
-
waitSpinner.text = 'Processing... (checking status)';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
attempts++;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (attempts >= maxAttempts) {
|
|
143
|
-
waitSpinner.warn('Processing is taking longer than expected');
|
|
144
|
-
if (options.json) {
|
|
145
|
-
console.log(JSON.stringify({ success: false, error: 'Processing timeout', data: { id: result.id, message: 'Processing is taking longer than expected' } }, null, 2));
|
|
146
|
-
} else {
|
|
147
|
-
console.log(chalk.yellow('\n⏳ Processing is still in progress. You can check status manually:'));
|
|
148
|
-
console.log(chalk.gray(` cl status ${result.id}`));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
if (options.json) {
|
|
153
|
-
console.log(JSON.stringify({ success: true, data: result }, null, 2));
|
|
154
|
-
} else {
|
|
155
|
-
console.log(chalk.green('✅ Customer input submitted successfully!'));
|
|
156
|
-
console.log(chalk.blue(`Submission ID: ${result.id}`));
|
|
157
|
-
console.log(chalk.gray('Run "cl status ' + result.id + '" to check progress'));
|
|
158
|
-
console.log(chalk.gray('Or use "cl submit --wait" to wait for completion'));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
} catch (error: any) {
|
|
163
|
-
if (options.json) {
|
|
164
|
-
console.log(JSON.stringify({
|
|
165
|
-
success: false,
|
|
166
|
-
error: error.message,
|
|
167
|
-
details: error.response?.data || 'Unknown error occurred'
|
|
168
|
-
}, null, 2));
|
|
169
|
-
} else {
|
|
170
|
-
// User-friendly error messages
|
|
171
|
-
if (error.response?.status === 400) {
|
|
172
|
-
const errorData = error.response.data;
|
|
173
|
-
if (errorData?.code === 'VALIDATION_ERROR') {
|
|
174
|
-
console.error(chalk.red('❌ Validation Error:'), errorData.error);
|
|
175
|
-
console.log(chalk.gray('\n💡 Common issues:'));
|
|
176
|
-
console.log(chalk.gray(' • Content must be at least 10 characters'));
|
|
177
|
-
console.log(chalk.gray(' • Check that all required fields are provided'));
|
|
178
|
-
console.log(chalk.gray(' • Ensure API key is valid: cl config'));
|
|
179
|
-
} else if (errorData?.code === 'PROCESSING_ERROR') {
|
|
180
|
-
console.error(chalk.red('❌ Processing Error:'), errorData.error);
|
|
181
|
-
console.log(chalk.gray('\n💡 This usually means:'));
|
|
182
|
-
console.log(chalk.gray(' • The content could not be processed by our AI'));
|
|
183
|
-
console.log(chalk.gray(' • Try rephrasing your input'));
|
|
184
|
-
console.log(chalk.gray(' • Make sure the content is clear and meaningful'));
|
|
185
|
-
console.log(chalk.gray(' • Check API key: cl config'));
|
|
186
|
-
} else {
|
|
187
|
-
console.error(chalk.red('❌ Bad Request:'), errorData?.error || 'Invalid input provided');
|
|
188
|
-
console.log(chalk.gray('\n💡 Try:'));
|
|
189
|
-
console.log(chalk.gray(' • Check your input content'));
|
|
190
|
-
console.log(chalk.gray(' • Verify API key: cl config'));
|
|
191
|
-
console.log(chalk.gray(' • Get help: cl submit --help'));
|
|
192
|
-
}
|
|
193
|
-
} else if (error.response?.status === 401) {
|
|
194
|
-
console.error(chalk.red('❌ Authentication Error:'), 'Invalid API key');
|
|
195
|
-
console.log(chalk.gray('\n💡 To fix:'));
|
|
196
|
-
console.log(chalk.gray(' 1. Get your free API key at: https://closedloop.sh'));
|
|
197
|
-
console.log(chalk.gray(' 2. Run: cl config --set-api-key <your-key>'));
|
|
198
|
-
} else if (error.response?.status === 429) {
|
|
199
|
-
console.error(chalk.red('❌ Rate Limited:'), 'Too many requests');
|
|
200
|
-
console.log(chalk.gray('\n💡 Wait a moment and try again'));
|
|
201
|
-
} else if (error.response?.status >= 500) {
|
|
202
|
-
console.error(chalk.red('❌ Server Error:'), 'Our servers are having issues');
|
|
203
|
-
console.log(chalk.gray('\n💡 Try again in a few minutes'));
|
|
204
|
-
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
205
|
-
console.error(chalk.red('❌ Connection Error:'), 'Cannot connect to ClosedLoop servers');
|
|
206
|
-
console.log(chalk.gray('\n💡 Check your internet connection and try again'));
|
|
207
|
-
} else {
|
|
208
|
-
console.error(chalk.red('❌ Error:'), error.message);
|
|
209
|
-
console.log(chalk.gray('\n💡 Get help: cl --help'));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
});
|
package/src/config/constants.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export const CONFIG = {
|
|
2
|
-
API_BASE_URL: process.env.CLOSEDLOOP_API_URL || 'https://mcp.closedloop.sh',
|
|
3
|
-
USER_AGENT: 'closedloop-cli/0.2.0',
|
|
4
|
-
MAX_RETRY_ATTEMPTS: 100,
|
|
5
|
-
POLL_INTERVAL_MS: 3000,
|
|
6
|
-
DEFAULT_PAGE_SIZE: 20,
|
|
7
|
-
MAX_PAGE_SIZE: 100,
|
|
8
|
-
MIN_API_KEY_LENGTH: 10,
|
|
9
|
-
MAX_CONTENT_LENGTH: 10000,
|
|
10
|
-
TIMEOUT_MS: 30000
|
|
11
|
-
} as const;
|
|
12
|
-
|
|
13
|
-
export const MESSAGES = {
|
|
14
|
-
NO_API_KEY: `🔑 No API key configured!
|
|
15
|
-
|
|
16
|
-
To get started:
|
|
17
|
-
1. Go to https://closedloop.sh
|
|
18
|
-
2. Sign up for a free account
|
|
19
|
-
3. Generate your API key
|
|
20
|
-
4. Run: cl config --set-api-key <your-key>
|
|
21
|
-
|
|
22
|
-
Get your free API key at: https://closedloop.sh`,
|
|
23
|
-
|
|
24
|
-
INVALID_API_KEY: '❌ Invalid API key. Get your free API key at https://closedloop.sh',
|
|
25
|
-
CONNECTION_ERROR: '❌ Connection Error: Cannot connect to ClosedLoop AI servers',
|
|
26
|
-
CHECK_INTERNET: 'Check your internet connection and try again',
|
|
27
|
-
GET_HELP: 'Get help: cl --help',
|
|
28
|
-
PROCESSING_ERROR: '❌ Processing Error: Unable to process feedback',
|
|
29
|
-
VALIDATION_ERROR: '❌ Validation Error:',
|
|
30
|
-
UNKNOWN_ERROR: 'Unknown error occurred'
|
|
31
|
-
} as const;
|
|
32
|
-
|
|
33
|
-
export const URLS = {
|
|
34
|
-
CLOSEDLOOP_HOME: 'https://closedloop.sh',
|
|
35
|
-
GET_API_KEY: 'https://closedloop.sh'
|
|
36
|
-
} as const;
|
package/src/config/index.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
|
|
5
|
-
interface Config {
|
|
6
|
-
apiKey?: string;
|
|
7
|
-
baseUrl?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
class ConfigManager {
|
|
11
|
-
private configPath: string;
|
|
12
|
-
private config: Config = {};
|
|
13
|
-
|
|
14
|
-
constructor() {
|
|
15
|
-
this.configPath = path.join(os.homedir(), '.closedloop', 'config.json');
|
|
16
|
-
this.load();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private load(): void {
|
|
20
|
-
try {
|
|
21
|
-
if (fs.existsSync(this.configPath)) {
|
|
22
|
-
const data = fs.readFileSync(this.configPath, 'utf8');
|
|
23
|
-
this.config = JSON.parse(data);
|
|
24
|
-
}
|
|
25
|
-
} catch (error) {
|
|
26
|
-
// Ignore config load errors
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private save(): void {
|
|
31
|
-
try {
|
|
32
|
-
const dir = path.dirname(this.configPath);
|
|
33
|
-
if (!fs.existsSync(dir)) {
|
|
34
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
|
|
37
|
-
} catch (error) {
|
|
38
|
-
throw new Error('Failed to save configuration');
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
get(key?: keyof Config): any {
|
|
43
|
-
if (key) {
|
|
44
|
-
return this.config[key];
|
|
45
|
-
}
|
|
46
|
-
return { ...this.config };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
set(key: keyof Config, value: any): void {
|
|
50
|
-
this.config[key] = value;
|
|
51
|
-
this.save();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
getConfigPath(): string {
|
|
55
|
-
return fs.existsSync(this.configPath) ? this.configPath : 'Not created yet';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const config = new ConfigManager();
|
package/src/types/index.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
export interface CommandOptions {
|
|
2
|
-
page?: number;
|
|
3
|
-
limit?: number;
|
|
4
|
-
json?: boolean;
|
|
5
|
-
wait?: boolean;
|
|
6
|
-
interactive?: boolean;
|
|
7
|
-
// Config options
|
|
8
|
-
setApiKey?: string;
|
|
9
|
-
setTeam?: string;
|
|
10
|
-
apiKey?: string;
|
|
11
|
-
team?: string;
|
|
12
|
-
// Input options
|
|
13
|
-
title?: string;
|
|
14
|
-
url?: string;
|
|
15
|
-
customer?: string;
|
|
16
|
-
name?: string;
|
|
17
|
-
email?: string;
|
|
18
|
-
// Feedback options
|
|
19
|
-
status?: string;
|
|
20
|
-
search?: string;
|
|
21
|
-
new?: boolean;
|
|
22
|
-
watch?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ApiResponse<T> {
|
|
26
|
-
success: boolean;
|
|
27
|
-
data: T;
|
|
28
|
-
error?: string;
|
|
29
|
-
pagination?: {
|
|
30
|
-
page: number;
|
|
31
|
-
limit: number;
|
|
32
|
-
total: number;
|
|
33
|
-
pages: number;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface InputData {
|
|
38
|
-
id: string;
|
|
39
|
-
title?: string;
|
|
40
|
-
content: string;
|
|
41
|
-
source_url?: string;
|
|
42
|
-
source_id?: string;
|
|
43
|
-
source_timestamp?: string;
|
|
44
|
-
customer_id?: string;
|
|
45
|
-
reporter_name?: string;
|
|
46
|
-
reporter_email?: string;
|
|
47
|
-
language?: string;
|
|
48
|
-
metadata?: any;
|
|
49
|
-
tags?: string[];
|
|
50
|
-
status: string;
|
|
51
|
-
created_at: string;
|
|
52
|
-
updated_at: string;
|
|
53
|
-
claimed_at?: string;
|
|
54
|
-
retry_count?: number;
|
|
55
|
-
error_message?: string;
|
|
56
|
-
feedback_count: number;
|
|
57
|
-
credits_consumed: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface FeedbackData {
|
|
61
|
-
id: string;
|
|
62
|
-
title?: string;
|
|
63
|
-
clarity?: string;
|
|
64
|
-
deal_blocker: boolean;
|
|
65
|
-
pain_point?: string;
|
|
66
|
-
workaround?: string;
|
|
67
|
-
competitor_gap?: string;
|
|
68
|
-
willingness_to_pay?: string;
|
|
69
|
-
use_case?: string;
|
|
70
|
-
feature_area?: string;
|
|
71
|
-
source_url?: string;
|
|
72
|
-
timestamp: string;
|
|
73
|
-
input_id?: string;
|
|
74
|
-
severity?: string;
|
|
75
|
-
status?: string;
|
|
76
|
-
source?: string;
|
|
77
|
-
// For backward compatibility
|
|
78
|
-
signal_title?: string;
|
|
79
|
-
created_at?: string;
|
|
80
|
-
updated_at?: string;
|
|
81
|
-
feedbackCount?: number;
|
|
82
|
-
creditsConsumed?: number;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface UsageData {
|
|
86
|
-
id: string;
|
|
87
|
-
input_id: string;
|
|
88
|
-
title?: string;
|
|
89
|
-
credits_consumed: number;
|
|
90
|
-
feedback_count: number;
|
|
91
|
-
created_at: string;
|
|
92
|
-
updated_at: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface ConfigData {
|
|
96
|
-
apiKey?: string;
|
|
97
|
-
baseUrl?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface PaginationData {
|
|
101
|
-
page: number;
|
|
102
|
-
limit: number;
|
|
103
|
-
total: number;
|
|
104
|
-
pages: number;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface ListResponse<T> {
|
|
108
|
-
data: T[];
|
|
109
|
-
pagination: PaginationData;
|
|
110
|
-
// For backward compatibility
|
|
111
|
-
inputs?: T[];
|
|
112
|
-
feedbacks?: T[];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface ErrorResponse {
|
|
116
|
-
success: false;
|
|
117
|
-
error: string;
|
|
118
|
-
details?: any;
|
|
119
|
-
}
|
package/src/utils/errors.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
export class CliError extends Error {
|
|
2
|
-
constructor(
|
|
3
|
-
message: string,
|
|
4
|
-
public code: string,
|
|
5
|
-
public exitCode: number = 1
|
|
6
|
-
) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = 'CliError';
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const ERROR_CODES = {
|
|
13
|
-
NO_API_KEY: 'NO_API_KEY',
|
|
14
|
-
INVALID_API_KEY: 'INVALID_API_KEY',
|
|
15
|
-
INVALID_INPUT: 'INVALID_INPUT',
|
|
16
|
-
API_ERROR: 'API_ERROR',
|
|
17
|
-
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
18
|
-
CONNECTION_ERROR: 'CONNECTION_ERROR',
|
|
19
|
-
PROCESSING_ERROR: 'PROCESSING_ERROR',
|
|
20
|
-
NOT_FOUND: 'NOT_FOUND',
|
|
21
|
-
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
22
|
-
RATE_LIMITED: 'RATE_LIMITED',
|
|
23
|
-
SERVER_ERROR: 'SERVER_ERROR',
|
|
24
|
-
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
export function handleApiError(error: any): CliError {
|
|
28
|
-
if (error.response) {
|
|
29
|
-
const status = error.response.status;
|
|
30
|
-
const data = error.response.data;
|
|
31
|
-
|
|
32
|
-
switch (status) {
|
|
33
|
-
case 401:
|
|
34
|
-
return new CliError(
|
|
35
|
-
'Invalid API key. Please check your configuration.',
|
|
36
|
-
ERROR_CODES.UNAUTHORIZED,
|
|
37
|
-
1
|
|
38
|
-
);
|
|
39
|
-
case 404:
|
|
40
|
-
return new CliError(
|
|
41
|
-
'Resource not found. Please check the ID and try again.',
|
|
42
|
-
ERROR_CODES.NOT_FOUND,
|
|
43
|
-
1
|
|
44
|
-
);
|
|
45
|
-
case 429:
|
|
46
|
-
return new CliError(
|
|
47
|
-
'Rate limit exceeded. Please wait a moment and try again.',
|
|
48
|
-
ERROR_CODES.RATE_LIMITED,
|
|
49
|
-
1
|
|
50
|
-
);
|
|
51
|
-
case 500:
|
|
52
|
-
case 502:
|
|
53
|
-
case 503:
|
|
54
|
-
case 504:
|
|
55
|
-
return new CliError(
|
|
56
|
-
'Server error. Please try again later.',
|
|
57
|
-
ERROR_CODES.SERVER_ERROR,
|
|
58
|
-
1
|
|
59
|
-
);
|
|
60
|
-
default:
|
|
61
|
-
return new CliError(
|
|
62
|
-
data?.message || `API error (${status})`,
|
|
63
|
-
ERROR_CODES.API_ERROR,
|
|
64
|
-
1
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
68
|
-
return new CliError(
|
|
69
|
-
'Cannot connect to ClosedLoop AI servers. Please check your internet connection.',
|
|
70
|
-
ERROR_CODES.CONNECTION_ERROR,
|
|
71
|
-
1
|
|
72
|
-
);
|
|
73
|
-
} else if (error.code === 'ETIMEDOUT') {
|
|
74
|
-
return new CliError(
|
|
75
|
-
'Request timed out. Please try again.',
|
|
76
|
-
ERROR_CODES.CONNECTION_ERROR,
|
|
77
|
-
1
|
|
78
|
-
);
|
|
79
|
-
} else {
|
|
80
|
-
return new CliError(
|
|
81
|
-
error.message || 'An unknown error occurred',
|
|
82
|
-
ERROR_CODES.UNKNOWN_ERROR,
|
|
83
|
-
1
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function handleValidationError(message: string): CliError {
|
|
89
|
-
return new CliError(message, ERROR_CODES.VALIDATION_ERROR, 1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function handleInputError(message: string): CliError {
|
|
93
|
-
return new CliError(message, ERROR_CODES.INVALID_INPUT, 1);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function handleProcessingError(message: string): CliError {
|
|
97
|
-
return new CliError(message, ERROR_CODES.PROCESSING_ERROR, 1);
|
|
98
|
-
}
|
package/src/utils/validation.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { CliError, handleValidationError, handleInputError, ERROR_CODES } from './errors';
|
|
2
|
-
import { CONFIG } from '../config/constants';
|
|
3
|
-
|
|
4
|
-
// UUID validation
|
|
5
|
-
export function validateUuid(id: string): boolean {
|
|
6
|
-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
-
return uuidRegex.test(id);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Content validation
|
|
11
|
-
export function validateInputContent(content: string): string {
|
|
12
|
-
if (!content || content.trim().length === 0) {
|
|
13
|
-
throw handleInputError('Content cannot be empty');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (content.length > CONFIG.MAX_CONTENT_LENGTH) {
|
|
17
|
-
throw handleInputError(`Content too long (max ${CONFIG.MAX_CONTENT_LENGTH} characters)`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return content.trim();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// API key validation
|
|
24
|
-
export function validateApiKey(apiKey: string): string {
|
|
25
|
-
if (!apiKey || apiKey.trim().length === 0 || apiKey.length < CONFIG.MIN_API_KEY_LENGTH) {
|
|
26
|
-
throw handleValidationError('Invalid API key. Get your free API key at https://closedloop.sh');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return apiKey.trim();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Email validation
|
|
33
|
-
export function validateEmail(email: string): string {
|
|
34
|
-
if (!email || email.trim().length === 0) {
|
|
35
|
-
throw handleValidationError('Email cannot be empty');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const trimmedEmail = email.trim();
|
|
39
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
40
|
-
if (!emailRegex.test(trimmedEmail)) {
|
|
41
|
-
throw handleValidationError('Invalid email format');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return trimmedEmail.toLowerCase();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// URL validation
|
|
48
|
-
export function validateUrl(url: string): string {
|
|
49
|
-
if (!url || url.trim().length === 0) {
|
|
50
|
-
throw handleValidationError('URL cannot be empty');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const urlObj = new URL(url);
|
|
55
|
-
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
|
56
|
-
throw handleValidationError('URL must use HTTP or HTTPS protocol');
|
|
57
|
-
}
|
|
58
|
-
return urlObj.toString();
|
|
59
|
-
} catch (error) {
|
|
60
|
-
throw handleValidationError('Invalid URL format');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Pagination validation
|
|
65
|
-
export function validatePagination(page?: number, limit?: number): { page: number; limit: number } {
|
|
66
|
-
const validatedPage = Math.max(1, page || 1);
|
|
67
|
-
const validatedLimit = Math.min(
|
|
68
|
-
Math.max(1, limit || CONFIG.DEFAULT_PAGE_SIZE),
|
|
69
|
-
CONFIG.MAX_PAGE_SIZE
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
return { page: validatedPage, limit: validatedLimit };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Title validation
|
|
76
|
-
export function validateTitle(title: string): string {
|
|
77
|
-
if (!title || title.trim().length === 0) {
|
|
78
|
-
throw handleValidationError('Title cannot be empty');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (title.length > 200) {
|
|
82
|
-
throw handleValidationError('Title too long (max 200 characters)');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return title.trim();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Customer ID validation
|
|
89
|
-
export function validateCustomerId(customerId: string): string {
|
|
90
|
-
if (!customerId || customerId.trim().length === 0) {
|
|
91
|
-
throw handleValidationError('Customer ID cannot be empty');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (customerId.length > 100) {
|
|
95
|
-
throw handleValidationError('Customer ID too long (max 100 characters)');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return customerId.trim();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Name validation
|
|
102
|
-
export function validateName(name: string): string {
|
|
103
|
-
if (!name || name.trim().length === 0) {
|
|
104
|
-
throw handleValidationError('Name cannot be empty');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (name.length > 100) {
|
|
108
|
-
throw handleValidationError('Name too long (max 100 characters)');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return name.trim();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Sanitize input to prevent XSS
|
|
115
|
-
export function sanitizeInput(input: string): string {
|
|
116
|
-
return input
|
|
117
|
-
.replace(/[<>]/g, '') // Remove < and >
|
|
118
|
-
.replace(/javascript:/gi, '') // Remove javascript: protocol
|
|
119
|
-
.replace(/on\w+=/gi, '') // Remove event handlers
|
|
120
|
-
.trim();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Validate and sanitize all input data
|
|
124
|
-
export function validateAndSanitizeInput(data: {
|
|
125
|
-
content: string;
|
|
126
|
-
title?: string;
|
|
127
|
-
source_url?: string;
|
|
128
|
-
customer_id?: string;
|
|
129
|
-
reporter_name?: string;
|
|
130
|
-
reporter_email?: string;
|
|
131
|
-
}): {
|
|
132
|
-
content: string;
|
|
133
|
-
title?: string;
|
|
134
|
-
source_url?: string;
|
|
135
|
-
customer_id?: string;
|
|
136
|
-
reporter_name?: string;
|
|
137
|
-
reporter_email?: string;
|
|
138
|
-
} {
|
|
139
|
-
const validated: any = {};
|
|
140
|
-
|
|
141
|
-
// Required fields
|
|
142
|
-
validated.content = sanitizeInput(validateInputContent(data.content));
|
|
143
|
-
|
|
144
|
-
// Optional fields
|
|
145
|
-
if (data.title) {
|
|
146
|
-
validated.title = sanitizeInput(validateTitle(data.title));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (data.source_url) {
|
|
150
|
-
validated.source_url = validateUrl(data.source_url);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (data.customer_id) {
|
|
154
|
-
validated.customer_id = sanitizeInput(validateCustomerId(data.customer_id));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (data.reporter_name) {
|
|
158
|
-
validated.reporter_name = sanitizeInput(validateName(data.reporter_name));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (data.reporter_email) {
|
|
162
|
-
validated.reporter_email = validateEmail(data.reporter_email);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return validated;
|
|
166
|
-
}
|