@nhonh/qabot 0.1.0 → 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.
- package/bin/qabot.js +2 -0
- package/package.json +1 -1
- package/src/ai/ai-engine.js +158 -31
- package/src/cli/commands/auth.js +253 -0
- package/src/cli/commands/generate.js +13 -3
- package/src/core/constants.js +64 -3
package/bin/qabot.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerRunCommand } from "../src/cli/commands/run.js";
|
|
|
7
7
|
import { registerListCommand } from "../src/cli/commands/list.js";
|
|
8
8
|
import { registerGenerateCommand } from "../src/cli/commands/generate.js";
|
|
9
9
|
import { registerReportCommand } from "../src/cli/commands/report.js";
|
|
10
|
+
import { registerAuthCommand } from "../src/cli/commands/auth.js";
|
|
10
11
|
|
|
11
12
|
const program = new Command();
|
|
12
13
|
|
|
@@ -20,5 +21,6 @@ registerRunCommand(program);
|
|
|
20
21
|
registerListCommand(program);
|
|
21
22
|
registerGenerateCommand(program);
|
|
22
23
|
registerReportCommand(program);
|
|
24
|
+
registerAuthCommand(program);
|
|
23
25
|
|
|
24
26
|
program.parse();
|
package/package.json
CHANGED
package/src/ai/ai-engine.js
CHANGED
|
@@ -3,16 +3,42 @@ import {
|
|
|
3
3
|
buildGenerationPrompt,
|
|
4
4
|
buildRecommendationPrompt,
|
|
5
5
|
} from "./prompt-builder.js";
|
|
6
|
+
import { AI_PROVIDER_DEFAULTS } from "../core/constants.js";
|
|
6
7
|
|
|
7
8
|
export class AIEngine {
|
|
8
9
|
constructor(config = {}) {
|
|
9
10
|
this.provider = config.provider || "none";
|
|
10
|
-
this.model =
|
|
11
|
-
|
|
11
|
+
this.model =
|
|
12
|
+
config.model || AI_PROVIDER_DEFAULTS[this.provider]?.model || "gpt-4o";
|
|
13
|
+
this.apiKey = resolveApiKey(config);
|
|
14
|
+
this.baseUrl =
|
|
15
|
+
config.baseUrl || AI_PROVIDER_DEFAULTS[this.provider]?.baseUrl || "";
|
|
16
|
+
this.authHeader =
|
|
17
|
+
config.authHeader ||
|
|
18
|
+
AI_PROVIDER_DEFAULTS[this.provider]?.authHeader ||
|
|
19
|
+
"Authorization";
|
|
20
|
+
this.authPrefix =
|
|
21
|
+
config.authPrefix ??
|
|
22
|
+
AI_PROVIDER_DEFAULTS[this.provider]?.authPrefix ??
|
|
23
|
+
"Bearer ";
|
|
24
|
+
this.extraHeaders = config.extraHeaders || {};
|
|
25
|
+
this.temperature = config.temperature ?? 0.3;
|
|
26
|
+
this.maxTokens = config.maxTokens ?? 4096;
|
|
12
27
|
}
|
|
13
28
|
|
|
14
29
|
isAvailable() {
|
|
15
|
-
|
|
30
|
+
if (this.provider === "none") return false;
|
|
31
|
+
if (this.provider === "ollama") return true;
|
|
32
|
+
return !!this.apiKey;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getProviderInfo() {
|
|
36
|
+
return {
|
|
37
|
+
provider: this.provider,
|
|
38
|
+
model: this.model,
|
|
39
|
+
baseUrl: this.baseUrl,
|
|
40
|
+
hasApiKey: !!this.apiKey,
|
|
41
|
+
};
|
|
16
42
|
}
|
|
17
43
|
|
|
18
44
|
async analyzeCode(code, context) {
|
|
@@ -45,70 +71,171 @@ export class AIEngine {
|
|
|
45
71
|
async complete(prompt) {
|
|
46
72
|
switch (this.provider) {
|
|
47
73
|
case "openai":
|
|
48
|
-
|
|
74
|
+
case "deepseek":
|
|
75
|
+
case "groq":
|
|
76
|
+
return this.callOpenAICompatible(prompt);
|
|
49
77
|
case "anthropic":
|
|
50
78
|
return this.callAnthropic(prompt);
|
|
79
|
+
case "gemini":
|
|
80
|
+
return this.callGemini(prompt);
|
|
51
81
|
case "ollama":
|
|
52
82
|
return this.callOllama(prompt);
|
|
83
|
+
case "proxy":
|
|
84
|
+
return this.callProxy(prompt);
|
|
53
85
|
default:
|
|
54
86
|
throw new Error(
|
|
55
|
-
`AI provider "${this.provider}" not
|
|
87
|
+
`AI provider "${this.provider}" not supported. Available: openai, anthropic, gemini, deepseek, groq, ollama, proxy`,
|
|
56
88
|
);
|
|
57
89
|
}
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
|
|
61
|
-
const
|
|
92
|
+
buildAuthHeaders() {
|
|
93
|
+
const headers = {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
...this.extraHeaders,
|
|
96
|
+
};
|
|
97
|
+
if (this.authHeader && this.apiKey) {
|
|
98
|
+
headers[this.authHeader] = `${this.authPrefix}${this.apiKey}`;
|
|
99
|
+
}
|
|
100
|
+
return headers;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async callOpenAICompatible(prompt) {
|
|
104
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
105
|
+
const headers = this.buildAuthHeaders();
|
|
106
|
+
|
|
107
|
+
const res = await fetch(url, {
|
|
62
108
|
method: "POST",
|
|
63
|
-
headers
|
|
64
|
-
"Content-Type": "application/json",
|
|
65
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
66
|
-
},
|
|
109
|
+
headers,
|
|
67
110
|
body: JSON.stringify({
|
|
68
111
|
model: this.model,
|
|
69
112
|
messages: [{ role: "user", content: prompt }],
|
|
70
|
-
temperature:
|
|
71
|
-
max_tokens:
|
|
113
|
+
temperature: this.temperature,
|
|
114
|
+
max_tokens: this.maxTokens,
|
|
72
115
|
}),
|
|
73
116
|
});
|
|
74
|
-
if (!res.ok)
|
|
75
|
-
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const body = await res.text().catch(() => "");
|
|
119
|
+
throw new Error(`${this.provider} API error (${res.status}): ${body}`);
|
|
120
|
+
}
|
|
76
121
|
const data = await res.json();
|
|
77
122
|
return data.choices?.[0]?.message?.content || "";
|
|
78
123
|
}
|
|
79
124
|
|
|
80
125
|
async callAnthropic(prompt) {
|
|
81
|
-
const
|
|
126
|
+
const url = `${this.baseUrl}/messages`;
|
|
127
|
+
const headers = {
|
|
128
|
+
...this.buildAuthHeaders(),
|
|
129
|
+
"anthropic-version": "2023-06-01",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const res = await fetch(url, {
|
|
82
133
|
method: "POST",
|
|
83
|
-
headers
|
|
84
|
-
"Content-Type": "application/json",
|
|
85
|
-
"x-api-key": this.apiKey,
|
|
86
|
-
"anthropic-version": "2023-06-01",
|
|
87
|
-
},
|
|
134
|
+
headers,
|
|
88
135
|
body: JSON.stringify({
|
|
89
|
-
model: this.model
|
|
90
|
-
max_tokens:
|
|
136
|
+
model: this.model,
|
|
137
|
+
max_tokens: this.maxTokens,
|
|
91
138
|
messages: [{ role: "user", content: prompt }],
|
|
92
139
|
}),
|
|
93
140
|
});
|
|
94
|
-
if (!res.ok)
|
|
95
|
-
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
const body = await res.text().catch(() => "");
|
|
143
|
+
throw new Error(`Anthropic API error (${res.status}): ${body}`);
|
|
144
|
+
}
|
|
96
145
|
const data = await res.json();
|
|
97
146
|
return data.content?.[0]?.text || "";
|
|
98
147
|
}
|
|
99
148
|
|
|
149
|
+
async callGemini(prompt) {
|
|
150
|
+
const url = `${this.baseUrl}/models/${this.model}:generateContent`;
|
|
151
|
+
const headers = this.buildAuthHeaders();
|
|
152
|
+
|
|
153
|
+
const res = await fetch(url, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers,
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
158
|
+
generationConfig: {
|
|
159
|
+
temperature: this.temperature,
|
|
160
|
+
maxOutputTokens: this.maxTokens,
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const body = await res.text().catch(() => "");
|
|
166
|
+
throw new Error(`Gemini API error (${res.status}): ${body}`);
|
|
167
|
+
}
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
170
|
+
}
|
|
171
|
+
|
|
100
172
|
async callOllama(prompt) {
|
|
101
|
-
const
|
|
173
|
+
const url = `${this.baseUrl}/api/generate`;
|
|
174
|
+
const res = await fetch(url, {
|
|
102
175
|
method: "POST",
|
|
103
176
|
headers: { "Content-Type": "application/json" },
|
|
177
|
+
body: JSON.stringify({ model: this.model, prompt, stream: false }),
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok) throw new Error(`Ollama API error (${res.status})`);
|
|
180
|
+
const data = await res.json();
|
|
181
|
+
return data.response || "";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async callProxy(prompt) {
|
|
185
|
+
if (!this.baseUrl) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
"Proxy provider requires baseUrl. Set ai.baseUrl in qabot.config.js",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const headers = this.buildAuthHeaders();
|
|
192
|
+
|
|
193
|
+
const res = await fetch(this.baseUrl, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers,
|
|
104
196
|
body: JSON.stringify({
|
|
105
|
-
model: this.model
|
|
106
|
-
prompt,
|
|
107
|
-
|
|
197
|
+
model: this.model,
|
|
198
|
+
messages: [{ role: "user", content: prompt }],
|
|
199
|
+
temperature: this.temperature,
|
|
200
|
+
max_tokens: this.maxTokens,
|
|
108
201
|
}),
|
|
109
202
|
});
|
|
110
|
-
if (!res.ok)
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
const body = await res.text().catch(() => "");
|
|
205
|
+
throw new Error(`Proxy API error (${res.status}): ${body}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
111
208
|
const data = await res.json();
|
|
112
|
-
|
|
209
|
+
|
|
210
|
+
if (data.choices?.[0]?.message?.content)
|
|
211
|
+
return data.choices[0].message.content;
|
|
212
|
+
if (data.content?.[0]?.text) return data.content[0].text;
|
|
213
|
+
if (data.candidates?.[0]?.content?.parts?.[0]?.text)
|
|
214
|
+
return data.candidates[0].content.parts[0].text;
|
|
215
|
+
if (data.response) return data.response;
|
|
216
|
+
if (typeof data.text === "string") return data.text;
|
|
217
|
+
if (typeof data.output === "string") return data.output;
|
|
218
|
+
if (typeof data.result === "string") return data.result;
|
|
219
|
+
|
|
220
|
+
throw new Error(
|
|
221
|
+
"Proxy returned unknown response format. Expected OpenAI/Anthropic/Gemini compatible response.",
|
|
222
|
+
);
|
|
113
223
|
}
|
|
114
224
|
}
|
|
225
|
+
|
|
226
|
+
function resolveApiKey(config) {
|
|
227
|
+
if (config.apiKey) return config.apiKey;
|
|
228
|
+
if (config.apiKeyEnv) return process.env[config.apiKeyEnv] || "";
|
|
229
|
+
|
|
230
|
+
const envMap = {
|
|
231
|
+
openai: "OPENAI_API_KEY",
|
|
232
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
233
|
+
gemini: "GEMINI_API_KEY",
|
|
234
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
235
|
+
groq: "GROQ_API_KEY",
|
|
236
|
+
proxy: "PROXY_API_KEY",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const envName = envMap[config.provider];
|
|
240
|
+
return envName ? process.env[envName] || "" : "";
|
|
241
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import Enquirer from "enquirer";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { logger } from "../../core/logger.js";
|
|
4
|
+
import { loadConfig, writeConfig } from "../../core/config.js";
|
|
5
|
+
import { AI_PROVIDERS, AI_PROVIDER_DEFAULTS } from "../../core/constants.js";
|
|
6
|
+
import { AIEngine } from "../../ai/ai-engine.js";
|
|
7
|
+
|
|
8
|
+
export function registerAuthCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("auth")
|
|
11
|
+
.description("Configure AI provider for test generation")
|
|
12
|
+
.option("--test", "Test current AI configuration")
|
|
13
|
+
.option("--show", "Show current AI configuration")
|
|
14
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
15
|
+
.action(runAuth);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function runAuth(options) {
|
|
19
|
+
const projectDir = options.dir;
|
|
20
|
+
const { config, isEmpty } = await loadConfig(projectDir);
|
|
21
|
+
|
|
22
|
+
if (options.show) {
|
|
23
|
+
showCurrentConfig(config);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options.test) {
|
|
28
|
+
await testConnection(config);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await interactiveSetup(projectDir, config, isEmpty);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function showCurrentConfig(config) {
|
|
36
|
+
const ai = config.ai || {};
|
|
37
|
+
const engine = new AIEngine(ai);
|
|
38
|
+
const info = engine.getProviderInfo();
|
|
39
|
+
|
|
40
|
+
logger.header("QABot \u2014 AI Configuration");
|
|
41
|
+
logger.blank();
|
|
42
|
+
logger.table(
|
|
43
|
+
["Setting", "Value"],
|
|
44
|
+
[
|
|
45
|
+
["Provider", info.provider || "none"],
|
|
46
|
+
["Model", info.model || "-"],
|
|
47
|
+
["Base URL", info.baseUrl || "(default)"],
|
|
48
|
+
["API Key", info.hasApiKey ? "\u2713 configured" : "\u2717 missing"],
|
|
49
|
+
],
|
|
50
|
+
);
|
|
51
|
+
logger.blank();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function testConnection(config) {
|
|
55
|
+
const ai = config.ai || {};
|
|
56
|
+
const engine = new AIEngine(ai);
|
|
57
|
+
|
|
58
|
+
if (!engine.isAvailable()) {
|
|
59
|
+
logger.error("AI is not configured. Run `qabot auth` to set up.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.info(`Testing connection to ${chalk.bold(engine.provider)}...`);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await engine.complete("Reply with exactly: QABOT_OK");
|
|
67
|
+
if (response.includes("QABOT_OK")) {
|
|
68
|
+
logger.success(
|
|
69
|
+
`Connection successful! Provider: ${engine.provider}, Model: ${engine.model}`,
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
logger.success(
|
|
73
|
+
`Got response from ${engine.provider} (model may not follow simple instructions exactly)`,
|
|
74
|
+
);
|
|
75
|
+
logger.dim(`Response: ${response.slice(0, 100)}`);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.error(`Connection failed: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function interactiveSetup(projectDir, config, isEmpty) {
|
|
83
|
+
const enquirer = new Enquirer();
|
|
84
|
+
|
|
85
|
+
logger.header("QABot \u2014 AI Provider Setup");
|
|
86
|
+
logger.blank();
|
|
87
|
+
|
|
88
|
+
const providerChoices = [
|
|
89
|
+
{ name: "openai", message: "OpenAI (GPT-4o, GPT-4-turbo)" },
|
|
90
|
+
{ name: "anthropic", message: "Anthropic (Claude 4, Claude Sonnet)" },
|
|
91
|
+
{ name: "gemini", message: "Google Gemini (Gemini 2.5 Flash/Pro)" },
|
|
92
|
+
{ name: "deepseek", message: "DeepSeek (DeepSeek-V3, DeepSeek-Chat)" },
|
|
93
|
+
{ name: "groq", message: "Groq (LLaMA 3.3, Mixtral)" },
|
|
94
|
+
{ name: "ollama", message: "Ollama (Local models, no API key)" },
|
|
95
|
+
{
|
|
96
|
+
name: "proxy",
|
|
97
|
+
message: "Custom Proxy (Any OpenAI-compatible endpoint)",
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const { provider } = await enquirer.prompt({
|
|
102
|
+
type: "select",
|
|
103
|
+
name: "provider",
|
|
104
|
+
message: "Select AI provider",
|
|
105
|
+
choices: providerChoices,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const defaults = AI_PROVIDER_DEFAULTS[provider] || {};
|
|
109
|
+
const aiConfig = { provider };
|
|
110
|
+
|
|
111
|
+
if (provider === "proxy") {
|
|
112
|
+
const proxyAnswers = await enquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: "input",
|
|
115
|
+
name: "baseUrl",
|
|
116
|
+
message:
|
|
117
|
+
"Proxy URL (full endpoint, e.g. https://my-proxy.com/v1/chat/completions)",
|
|
118
|
+
validate: (v) =>
|
|
119
|
+
v.startsWith("http")
|
|
120
|
+
? true
|
|
121
|
+
: "Must be a valid URL starting with http",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: "input",
|
|
125
|
+
name: "apiKey",
|
|
126
|
+
message: "API Key (leave empty if not required)",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: "input",
|
|
130
|
+
name: "model",
|
|
131
|
+
message: "Model name (leave empty for proxy default)",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: "select",
|
|
135
|
+
name: "authMethod",
|
|
136
|
+
message: "Authentication method",
|
|
137
|
+
choices: [
|
|
138
|
+
{
|
|
139
|
+
name: "bearer",
|
|
140
|
+
message: "Bearer Token (Authorization: Bearer <key>)",
|
|
141
|
+
},
|
|
142
|
+
{ name: "x-api-key", message: "X-API-Key (x-api-key: <key>)" },
|
|
143
|
+
{ name: "custom", message: "Custom Header" },
|
|
144
|
+
{ name: "none", message: "No Auth" },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
aiConfig.baseUrl = proxyAnswers.baseUrl;
|
|
150
|
+
if (proxyAnswers.model) aiConfig.model = proxyAnswers.model;
|
|
151
|
+
|
|
152
|
+
if (proxyAnswers.authMethod === "bearer") {
|
|
153
|
+
aiConfig.authHeader = "Authorization";
|
|
154
|
+
aiConfig.authPrefix = "Bearer ";
|
|
155
|
+
} else if (proxyAnswers.authMethod === "x-api-key") {
|
|
156
|
+
aiConfig.authHeader = "x-api-key";
|
|
157
|
+
aiConfig.authPrefix = "";
|
|
158
|
+
} else if (proxyAnswers.authMethod === "custom") {
|
|
159
|
+
const { customHeader } = await enquirer.prompt({
|
|
160
|
+
type: "input",
|
|
161
|
+
name: "customHeader",
|
|
162
|
+
message: "Custom header name (e.g. X-Custom-Auth)",
|
|
163
|
+
});
|
|
164
|
+
aiConfig.authHeader = customHeader;
|
|
165
|
+
aiConfig.authPrefix = "";
|
|
166
|
+
} else {
|
|
167
|
+
aiConfig.authHeader = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (proxyAnswers.apiKey) {
|
|
171
|
+
aiConfig.apiKeyEnv = "PROXY_API_KEY";
|
|
172
|
+
logger.blank();
|
|
173
|
+
logger.warn("Store your API key as an environment variable:");
|
|
174
|
+
logger.dim(` export PROXY_API_KEY="${proxyAnswers.apiKey}"`);
|
|
175
|
+
logger.dim(" Or add to .env file: PROXY_API_KEY=your-key");
|
|
176
|
+
}
|
|
177
|
+
} else if (provider === "ollama") {
|
|
178
|
+
const { baseUrl, model } = await enquirer.prompt([
|
|
179
|
+
{
|
|
180
|
+
type: "input",
|
|
181
|
+
name: "baseUrl",
|
|
182
|
+
message: "Ollama URL",
|
|
183
|
+
initial: defaults.baseUrl,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: "input",
|
|
187
|
+
name: "model",
|
|
188
|
+
message: "Model name",
|
|
189
|
+
initial: defaults.model,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
aiConfig.baseUrl = baseUrl;
|
|
193
|
+
aiConfig.model = model;
|
|
194
|
+
} else {
|
|
195
|
+
const envMap = {
|
|
196
|
+
openai: "OPENAI_API_KEY",
|
|
197
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
198
|
+
gemini: "GEMINI_API_KEY",
|
|
199
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
200
|
+
groq: "GROQ_API_KEY",
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const envName = envMap[provider];
|
|
204
|
+
|
|
205
|
+
const answers = await enquirer.prompt([
|
|
206
|
+
{
|
|
207
|
+
type: "input",
|
|
208
|
+
name: "model",
|
|
209
|
+
message: "Model name",
|
|
210
|
+
initial: defaults.model,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: "password",
|
|
214
|
+
name: "apiKey",
|
|
215
|
+
message: `API Key (${envName})`,
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
aiConfig.model = answers.model;
|
|
220
|
+
aiConfig.apiKeyEnv = envName;
|
|
221
|
+
|
|
222
|
+
if (answers.apiKey) {
|
|
223
|
+
logger.blank();
|
|
224
|
+
logger.warn("Store your API key as an environment variable:");
|
|
225
|
+
logger.dim(` export ${envName}="${answers.apiKey}"`);
|
|
226
|
+
logger.dim(` Or add to .env file: ${envName}=your-key`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
config.ai = aiConfig;
|
|
231
|
+
|
|
232
|
+
if (isEmpty) {
|
|
233
|
+
logger.blank();
|
|
234
|
+
logger.warn(
|
|
235
|
+
"No qabot.config.js found. Run `qabot init` first, then `qabot auth`.",
|
|
236
|
+
);
|
|
237
|
+
logger.blank();
|
|
238
|
+
logger.info("Or manually add to qabot.config.js:");
|
|
239
|
+
logger.dim(` ai: ${JSON.stringify(aiConfig, null, 4)}`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await writeConfig(projectDir, config);
|
|
244
|
+
logger.blank();
|
|
245
|
+
logger.success("AI configuration saved to qabot.config.js");
|
|
246
|
+
logger.blank();
|
|
247
|
+
logger.info("Test your setup:");
|
|
248
|
+
logger.dim(" qabot auth --test");
|
|
249
|
+
logger.blank();
|
|
250
|
+
logger.info("Generate tests:");
|
|
251
|
+
logger.dim(" qabot generate <feature>");
|
|
252
|
+
logger.blank();
|
|
253
|
+
}
|
|
@@ -33,9 +33,19 @@ async function runGenerate(feature, options) {
|
|
|
33
33
|
|
|
34
34
|
const ai = new AIEngine(aiConfig);
|
|
35
35
|
if (!ai.isAvailable()) {
|
|
36
|
-
logger.error("AI is not configured.
|
|
37
|
-
logger.
|
|
38
|
-
logger.
|
|
36
|
+
logger.error("AI is not configured.");
|
|
37
|
+
logger.blank();
|
|
38
|
+
logger.info("Quick setup:");
|
|
39
|
+
logger.dim(" qabot auth # Interactive provider setup");
|
|
40
|
+
logger.blank();
|
|
41
|
+
logger.info("Or set manually:");
|
|
42
|
+
logger.dim(
|
|
43
|
+
" export OPENAI_API_KEY=sk-... # Then set ai.provider in qabot.config.js",
|
|
44
|
+
);
|
|
45
|
+
logger.blank();
|
|
46
|
+
logger.info(
|
|
47
|
+
"Supported: openai, anthropic, gemini, deepseek, groq, ollama, proxy",
|
|
48
|
+
);
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
41
51
|
|
package/src/core/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.2.0";
|
|
2
2
|
export const TOOL_NAME = "qabot";
|
|
3
3
|
|
|
4
4
|
export const PROJECT_TYPES = [
|
|
@@ -20,7 +20,61 @@ export const RUNNERS = [
|
|
|
20
20
|
"xunit",
|
|
21
21
|
"dotnet-test",
|
|
22
22
|
];
|
|
23
|
-
export const AI_PROVIDERS = [
|
|
23
|
+
export const AI_PROVIDERS = [
|
|
24
|
+
"openai",
|
|
25
|
+
"anthropic",
|
|
26
|
+
"gemini",
|
|
27
|
+
"deepseek",
|
|
28
|
+
"groq",
|
|
29
|
+
"ollama",
|
|
30
|
+
"proxy",
|
|
31
|
+
"none",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const AI_PROVIDER_DEFAULTS = {
|
|
35
|
+
openai: {
|
|
36
|
+
baseUrl: "https://api.openai.com/v1",
|
|
37
|
+
model: "gpt-4o",
|
|
38
|
+
authHeader: "Authorization",
|
|
39
|
+
authPrefix: "Bearer ",
|
|
40
|
+
},
|
|
41
|
+
anthropic: {
|
|
42
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
43
|
+
model: "claude-sonnet-4-20250514",
|
|
44
|
+
authHeader: "x-api-key",
|
|
45
|
+
authPrefix: "",
|
|
46
|
+
},
|
|
47
|
+
gemini: {
|
|
48
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
49
|
+
model: "gemini-2.5-flash",
|
|
50
|
+
authHeader: "x-goog-api-key",
|
|
51
|
+
authPrefix: "",
|
|
52
|
+
},
|
|
53
|
+
deepseek: {
|
|
54
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
55
|
+
model: "deepseek-chat",
|
|
56
|
+
authHeader: "Authorization",
|
|
57
|
+
authPrefix: "Bearer ",
|
|
58
|
+
},
|
|
59
|
+
groq: {
|
|
60
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
61
|
+
model: "llama-3.3-70b-versatile",
|
|
62
|
+
authHeader: "Authorization",
|
|
63
|
+
authPrefix: "Bearer ",
|
|
64
|
+
},
|
|
65
|
+
ollama: {
|
|
66
|
+
baseUrl: "http://localhost:11434",
|
|
67
|
+
model: "llama3",
|
|
68
|
+
authHeader: null,
|
|
69
|
+
authPrefix: "",
|
|
70
|
+
},
|
|
71
|
+
proxy: {
|
|
72
|
+
baseUrl: "",
|
|
73
|
+
model: "",
|
|
74
|
+
authHeader: "Authorization",
|
|
75
|
+
authPrefix: "Bearer ",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
24
78
|
export const LAYERS = ["unit", "integration", "e2e"];
|
|
25
79
|
export const PRIORITIES = ["P0", "P1", "P2", "P3"];
|
|
26
80
|
|
|
@@ -39,7 +93,14 @@ export const DEFAULT_CONFIG = {
|
|
|
39
93
|
history: true,
|
|
40
94
|
formats: ["html", "json"],
|
|
41
95
|
},
|
|
42
|
-
ai: {
|
|
96
|
+
ai: {
|
|
97
|
+
provider: "none",
|
|
98
|
+
model: "gpt-4o",
|
|
99
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
100
|
+
baseUrl: null,
|
|
101
|
+
authHeader: null,
|
|
102
|
+
authPrefix: null,
|
|
103
|
+
},
|
|
43
104
|
useCases: { dir: "./docs/use-cases", formats: ["md", "feature", "txt"] },
|
|
44
105
|
};
|
|
45
106
|
|