@gxp-dev/tools 2.0.16 → 2.0.17
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/lib/tui/App.tsx +68 -48
- package/bin/lib/tui/components/AIPanel.tsx +180 -0
- package/bin/lib/tui/components/CommandInput.tsx +76 -33
- package/bin/lib/tui/services/AIService.ts +509 -0
- package/bin/lib/tui/services/index.ts +11 -0
- package/browser-extensions/chrome/background.js +16 -0
- package/browser-extensions/chrome/popup.html +63 -19
- package/browser-extensions/chrome/popup.js +47 -9
- package/browser-extensions/firefox/popup.html +67 -14
- package/browser-extensions/firefox/popup.js +45 -9
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +65 -45
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/AIPanel.d.ts +7 -0
- package/dist/tui/components/AIPanel.d.ts.map +1 -0
- package/dist/tui/components/AIPanel.js +116 -0
- package/dist/tui/components/AIPanel.js.map +1 -0
- package/dist/tui/components/CommandInput.d.ts.map +1 -1
- package/dist/tui/components/CommandInput.js +40 -13
- package/dist/tui/components/CommandInput.js.map +1 -1
- package/dist/tui/services/AIService.d.ts +48 -0
- package/dist/tui/services/AIService.d.ts.map +1 -0
- package/dist/tui/services/AIService.js +429 -0
- package/dist/tui/services/AIService.js.map +1 -0
- package/dist/tui/services/index.d.ts +1 -0
- package/dist/tui/services/index.d.ts.map +1 -1
- package/dist/tui/services/index.js +1 -0
- package/dist/tui/services/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
// AI Provider types
|
|
7
|
+
export type AIProvider = 'claude' | 'codex' | 'gemini';
|
|
8
|
+
|
|
9
|
+
export interface AIProviderInfo {
|
|
10
|
+
id: AIProvider;
|
|
11
|
+
name: string;
|
|
12
|
+
available: boolean;
|
|
13
|
+
method?: string; // For gemini: 'cli', 'api_key', 'gcloud'
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// AI Configuration
|
|
18
|
+
export interface AIConfig {
|
|
19
|
+
provider: AIProvider;
|
|
20
|
+
systemPrompt: string;
|
|
21
|
+
projectContext: boolean;
|
|
22
|
+
maxContextTokens: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get the gxdev config directory
|
|
26
|
+
function getConfigDir(): string {
|
|
27
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
28
|
+
return path.join(home, '.gxdev');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Ensure config directory exists
|
|
32
|
+
function ensureConfigDir(): void {
|
|
33
|
+
const configDir = getConfigDir();
|
|
34
|
+
if (!fs.existsSync(configDir)) {
|
|
35
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get AI config file path
|
|
40
|
+
function getAIConfigPath(): string {
|
|
41
|
+
return path.join(getConfigDir(), 'ai-config.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Load AI config
|
|
45
|
+
export function loadAIConfig(): AIConfig {
|
|
46
|
+
try {
|
|
47
|
+
const configPath = getAIConfigPath();
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
50
|
+
return JSON.parse(content);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Invalid or missing config file
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
provider: 'claude', // Default to Claude
|
|
57
|
+
systemPrompt: 'You are a helpful assistant for GxP plugin development. Help the user build Vue.js components for the GxP kiosk platform.',
|
|
58
|
+
projectContext: true,
|
|
59
|
+
maxContextTokens: 4000,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Save AI config
|
|
64
|
+
export function saveAIConfig(config: AIConfig): void {
|
|
65
|
+
ensureConfigDir();
|
|
66
|
+
const configPath = getAIConfigPath();
|
|
67
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if a command exists
|
|
71
|
+
function commandExists(cmd: string): boolean {
|
|
72
|
+
try {
|
|
73
|
+
execSync(`which ${cmd}`, { stdio: 'pipe' });
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check available AI providers
|
|
81
|
+
export function getAvailableProviders(): AIProviderInfo[] {
|
|
82
|
+
const providers: AIProviderInfo[] = [];
|
|
83
|
+
|
|
84
|
+
// Check Claude CLI
|
|
85
|
+
if (commandExists('claude')) {
|
|
86
|
+
providers.push({
|
|
87
|
+
id: 'claude',
|
|
88
|
+
name: 'Claude',
|
|
89
|
+
available: true,
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
providers.push({
|
|
93
|
+
id: 'claude',
|
|
94
|
+
name: 'Claude',
|
|
95
|
+
available: false,
|
|
96
|
+
reason: 'Install: npm i -g @anthropic-ai/claude-code && claude login',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check Codex CLI
|
|
101
|
+
if (commandExists('codex')) {
|
|
102
|
+
providers.push({
|
|
103
|
+
id: 'codex',
|
|
104
|
+
name: 'Codex',
|
|
105
|
+
available: true,
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
providers.push({
|
|
109
|
+
id: 'codex',
|
|
110
|
+
name: 'Codex',
|
|
111
|
+
available: false,
|
|
112
|
+
reason: 'Install: npm i -g @openai/codex && codex auth',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check Gemini (CLI, API key, or gcloud)
|
|
117
|
+
if (commandExists('gemini')) {
|
|
118
|
+
providers.push({
|
|
119
|
+
id: 'gemini',
|
|
120
|
+
name: 'Gemini',
|
|
121
|
+
available: true,
|
|
122
|
+
method: 'cli',
|
|
123
|
+
});
|
|
124
|
+
} else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
125
|
+
providers.push({
|
|
126
|
+
id: 'gemini',
|
|
127
|
+
name: 'Gemini',
|
|
128
|
+
available: true,
|
|
129
|
+
method: 'api_key',
|
|
130
|
+
});
|
|
131
|
+
} else if (commandExists('gcloud')) {
|
|
132
|
+
try {
|
|
133
|
+
const authList = execSync("gcloud auth list --format='value(account)'", { stdio: 'pipe' }).toString();
|
|
134
|
+
if (authList.trim()) {
|
|
135
|
+
providers.push({
|
|
136
|
+
id: 'gemini',
|
|
137
|
+
name: 'Gemini',
|
|
138
|
+
available: true,
|
|
139
|
+
method: 'gcloud',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
providers.push({
|
|
144
|
+
id: 'gemini',
|
|
145
|
+
name: 'Gemini',
|
|
146
|
+
available: false,
|
|
147
|
+
reason: 'Install: npm i -g @google/gemini-cli && gemini',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
providers.push({
|
|
152
|
+
id: 'gemini',
|
|
153
|
+
name: 'Gemini',
|
|
154
|
+
available: false,
|
|
155
|
+
reason: 'Install: npm i -g @google/gemini-cli && gemini',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return providers;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Get provider display name with status
|
|
163
|
+
export function getProviderStatus(provider: AIProviderInfo): string {
|
|
164
|
+
if (!provider.available) {
|
|
165
|
+
return `${provider.name} (not available)`;
|
|
166
|
+
}
|
|
167
|
+
if (provider.method) {
|
|
168
|
+
switch (provider.method) {
|
|
169
|
+
case 'cli':
|
|
170
|
+
return `${provider.name} (CLI)`;
|
|
171
|
+
case 'api_key':
|
|
172
|
+
return `${provider.name} (API key)`;
|
|
173
|
+
case 'gcloud':
|
|
174
|
+
return `${provider.name} (gcloud)`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return `${provider.name} (logged in)`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// AI Service class
|
|
181
|
+
export class AIService extends EventEmitter {
|
|
182
|
+
private conversationHistory: Array<{ role: string; content: string }> = [];
|
|
183
|
+
private projectContext: string = '';
|
|
184
|
+
private currentProvider: AIProvider;
|
|
185
|
+
private geminiMethod?: string;
|
|
186
|
+
|
|
187
|
+
constructor() {
|
|
188
|
+
super();
|
|
189
|
+
const config = loadAIConfig();
|
|
190
|
+
this.currentProvider = config.provider;
|
|
191
|
+
|
|
192
|
+
// Determine gemini method if that's the current provider
|
|
193
|
+
const providers = getAvailableProviders();
|
|
194
|
+
const geminiProvider = providers.find(p => p.id === 'gemini');
|
|
195
|
+
if (geminiProvider?.available) {
|
|
196
|
+
this.geminiMethod = geminiProvider.method;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get current provider
|
|
201
|
+
getProvider(): AIProvider {
|
|
202
|
+
return this.currentProvider;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Set current provider
|
|
206
|
+
setProvider(provider: AIProvider): { success: boolean; message: string } {
|
|
207
|
+
const providers = getAvailableProviders();
|
|
208
|
+
const providerInfo = providers.find(p => p.id === provider);
|
|
209
|
+
|
|
210
|
+
if (!providerInfo) {
|
|
211
|
+
return { success: false, message: `Unknown provider: ${provider}` };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!providerInfo.available) {
|
|
215
|
+
return { success: false, message: `${providerInfo.name} is not available. ${providerInfo.reason || ''}` };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.currentProvider = provider;
|
|
219
|
+
if (provider === 'gemini') {
|
|
220
|
+
this.geminiMethod = providerInfo.method;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Save to config
|
|
224
|
+
const config = loadAIConfig();
|
|
225
|
+
config.provider = provider;
|
|
226
|
+
saveAIConfig(config);
|
|
227
|
+
|
|
228
|
+
this.clearConversation();
|
|
229
|
+
return { success: true, message: `Switched to ${providerInfo.name}` };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if current provider is available
|
|
233
|
+
isAvailable(): boolean {
|
|
234
|
+
const providers = getAvailableProviders();
|
|
235
|
+
const current = providers.find(p => p.id === this.currentProvider);
|
|
236
|
+
return current?.available || false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Get provider info
|
|
240
|
+
getProviderInfo(): AIProviderInfo | undefined {
|
|
241
|
+
const providers = getAvailableProviders();
|
|
242
|
+
return providers.find(p => p.id === this.currentProvider);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Load project context
|
|
246
|
+
loadProjectContext(cwd: string): void {
|
|
247
|
+
const files = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', 'README.md', 'package.json', 'app-manifest.json'];
|
|
248
|
+
const contextParts: string[] = [];
|
|
249
|
+
|
|
250
|
+
for (const file of files) {
|
|
251
|
+
const filePath = path.join(cwd, file);
|
|
252
|
+
if (fs.existsSync(filePath)) {
|
|
253
|
+
try {
|
|
254
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
255
|
+
// Limit each file to 2000 chars
|
|
256
|
+
contextParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`);
|
|
257
|
+
} catch {
|
|
258
|
+
// Skip unreadable files
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.projectContext = contextParts.join('\n\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Send message using current provider
|
|
267
|
+
async sendMessage(message: string): Promise<string> {
|
|
268
|
+
const config = loadAIConfig();
|
|
269
|
+
|
|
270
|
+
// Build context
|
|
271
|
+
let systemContext = config.systemPrompt || '';
|
|
272
|
+
if (config.projectContext && this.projectContext) {
|
|
273
|
+
systemContext += '\n\nProject Context:\n' + this.projectContext;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
switch (this.currentProvider) {
|
|
277
|
+
case 'claude':
|
|
278
|
+
return this.sendWithClaude(message, systemContext);
|
|
279
|
+
case 'codex':
|
|
280
|
+
return this.sendWithCodex(message, systemContext);
|
|
281
|
+
case 'gemini':
|
|
282
|
+
return this.sendWithGemini(message, systemContext);
|
|
283
|
+
default:
|
|
284
|
+
throw new Error(`Unknown provider: ${this.currentProvider}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Send message with Claude CLI
|
|
289
|
+
private async sendWithClaude(message: string, systemContext: string): Promise<string> {
|
|
290
|
+
return new Promise((resolve, reject) => {
|
|
291
|
+
let output = '';
|
|
292
|
+
let errorOutput = '';
|
|
293
|
+
|
|
294
|
+
const fullPrompt = systemContext ? `${systemContext}\n\nUser: ${message}` : message;
|
|
295
|
+
|
|
296
|
+
const claude = spawn('claude', ['--print', '-p', fullPrompt], {
|
|
297
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
298
|
+
shell: true,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
claude.stdout.on('data', (data) => {
|
|
302
|
+
output += data.toString();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
claude.stderr.on('data', (data) => {
|
|
306
|
+
errorOutput += data.toString();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
claude.on('close', (code) => {
|
|
310
|
+
if (code !== 0) {
|
|
311
|
+
reject(new Error(`Claude error: ${errorOutput || 'Unknown error'}`));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
this.conversationHistory.push({ role: 'user', content: message });
|
|
315
|
+
this.conversationHistory.push({ role: 'assistant', content: output });
|
|
316
|
+
resolve(output.trim());
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
claude.on('error', (err) => {
|
|
320
|
+
reject(new Error(`Failed to run Claude: ${err.message}`));
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Send message with Codex CLI
|
|
326
|
+
private async sendWithCodex(message: string, systemContext: string): Promise<string> {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
let output = '';
|
|
329
|
+
let errorOutput = '';
|
|
330
|
+
|
|
331
|
+
const fullPrompt = systemContext ? `${systemContext}\n\nUser: ${message}` : message;
|
|
332
|
+
|
|
333
|
+
const codex = spawn('codex', ['--quiet', '-p', fullPrompt], {
|
|
334
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
335
|
+
shell: true,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
codex.stdout.on('data', (data) => {
|
|
339
|
+
output += data.toString();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
codex.stderr.on('data', (data) => {
|
|
343
|
+
errorOutput += data.toString();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
codex.on('close', (code) => {
|
|
347
|
+
if (code !== 0) {
|
|
348
|
+
reject(new Error(`Codex error: ${errorOutput || 'Unknown error'}`));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.conversationHistory.push({ role: 'user', content: message });
|
|
352
|
+
this.conversationHistory.push({ role: 'assistant', content: output });
|
|
353
|
+
resolve(output.trim());
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
codex.on('error', (err) => {
|
|
357
|
+
reject(new Error(`Failed to run Codex: ${err.message}`));
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Send message with Gemini
|
|
363
|
+
private async sendWithGemini(message: string, systemContext: string): Promise<string> {
|
|
364
|
+
const fullPrompt = systemContext ? `${systemContext}\n\nUser: ${message}` : message;
|
|
365
|
+
|
|
366
|
+
if (this.geminiMethod === 'cli') {
|
|
367
|
+
return this.sendWithGeminiCli(fullPrompt);
|
|
368
|
+
} else if (this.geminiMethod === 'api_key') {
|
|
369
|
+
return this.sendWithGeminiApi(fullPrompt);
|
|
370
|
+
} else if (this.geminiMethod === 'gcloud') {
|
|
371
|
+
return this.sendWithGeminiGcloud(fullPrompt);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
throw new Error('Gemini is not properly configured');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Send with Gemini CLI
|
|
378
|
+
private async sendWithGeminiCli(prompt: string): Promise<string> {
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
let output = '';
|
|
381
|
+
let errorOutput = '';
|
|
382
|
+
|
|
383
|
+
const gemini = spawn('gemini', ['-p', prompt], {
|
|
384
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
385
|
+
shell: true,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
gemini.stdout.on('data', (data) => {
|
|
389
|
+
output += data.toString();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
gemini.stderr.on('data', (data) => {
|
|
393
|
+
errorOutput += data.toString();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
gemini.on('close', (code) => {
|
|
397
|
+
if (code !== 0) {
|
|
398
|
+
reject(new Error(`Gemini error: ${errorOutput || 'Unknown error'}`));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.conversationHistory.push({ role: 'user', content: prompt });
|
|
402
|
+
this.conversationHistory.push({ role: 'assistant', content: output });
|
|
403
|
+
resolve(output.trim());
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
gemini.on('error', (err) => {
|
|
407
|
+
reject(new Error(`Failed to run Gemini: ${err.message}`));
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Send with Gemini API
|
|
413
|
+
private async sendWithGeminiApi(prompt: string): Promise<string> {
|
|
414
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
415
|
+
if (!apiKey) {
|
|
416
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const response = await fetch(
|
|
420
|
+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
|
|
421
|
+
{
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: { 'Content-Type': 'application/json' },
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
426
|
+
generationConfig: { maxOutputTokens: 2048, temperature: 0.7 },
|
|
427
|
+
}),
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
const errorText = await response.text();
|
|
433
|
+
throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const data = await response.json() as any;
|
|
437
|
+
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
|
|
438
|
+
|
|
439
|
+
this.conversationHistory.push({ role: 'user', content: prompt });
|
|
440
|
+
this.conversationHistory.push({ role: 'assistant', content: responseText });
|
|
441
|
+
|
|
442
|
+
return responseText;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Send with Gemini via gcloud
|
|
446
|
+
private async sendWithGeminiGcloud(prompt: string): Promise<string> {
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
let accessToken: string;
|
|
449
|
+
let projectId: string;
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
accessToken = execSync('gcloud auth print-access-token', { stdio: 'pipe' }).toString().trim();
|
|
453
|
+
projectId = execSync('gcloud config get-value project', { stdio: 'pipe' }).toString().trim();
|
|
454
|
+
} catch (error) {
|
|
455
|
+
reject(new Error('Failed to get gcloud credentials'));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const requestBody = JSON.stringify({
|
|
460
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
461
|
+
generationConfig: { maxOutputTokens: 2048, temperature: 0.7 },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const curl = spawn('curl', [
|
|
465
|
+
'-s', '-X', 'POST',
|
|
466
|
+
`https://us-central1-aiplatform.googleapis.com/v1/projects/${projectId}/locations/us-central1/publishers/google/models/gemini-1.5-flash:generateContent`,
|
|
467
|
+
'-H', `Authorization: Bearer ${accessToken}`,
|
|
468
|
+
'-H', 'Content-Type: application/json',
|
|
469
|
+
'-d', requestBody,
|
|
470
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
471
|
+
|
|
472
|
+
let output = '';
|
|
473
|
+
let errorOutput = '';
|
|
474
|
+
|
|
475
|
+
curl.stdout.on('data', (data) => { output += data.toString(); });
|
|
476
|
+
curl.stderr.on('data', (data) => { errorOutput += data.toString(); });
|
|
477
|
+
|
|
478
|
+
curl.on('close', (code) => {
|
|
479
|
+
if (code !== 0) {
|
|
480
|
+
reject(new Error(`Gemini gcloud error: ${errorOutput}`));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const data = JSON.parse(output);
|
|
486
|
+
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
|
|
487
|
+
this.conversationHistory.push({ role: 'user', content: prompt });
|
|
488
|
+
this.conversationHistory.push({ role: 'assistant', content: responseText });
|
|
489
|
+
resolve(responseText);
|
|
490
|
+
} catch (parseError) {
|
|
491
|
+
reject(new Error(`Failed to parse Gemini response`));
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Clear conversation history
|
|
498
|
+
clearConversation(): void {
|
|
499
|
+
this.conversationHistory = [];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Get conversation history
|
|
503
|
+
getConversationHistory(): Array<{ role: string; content: string }> {
|
|
504
|
+
return [...this.conversationHistory];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Singleton instance
|
|
509
|
+
export const aiService = new AIService();
|
|
@@ -11,3 +11,14 @@ export {
|
|
|
11
11
|
saveGeminiConfig,
|
|
12
12
|
clearAuthTokens
|
|
13
13
|
} from './GeminiService.js';
|
|
14
|
+
export {
|
|
15
|
+
aiService,
|
|
16
|
+
AIService,
|
|
17
|
+
AIProvider,
|
|
18
|
+
AIProviderInfo,
|
|
19
|
+
AIConfig,
|
|
20
|
+
loadAIConfig,
|
|
21
|
+
saveAIConfig,
|
|
22
|
+
getAvailableProviders,
|
|
23
|
+
getProviderStatus
|
|
24
|
+
} from './AIService.js';
|
|
@@ -836,6 +836,22 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
836
836
|
});
|
|
837
837
|
return true; // Keep message channel open for async response
|
|
838
838
|
|
|
839
|
+
case "openDevTools":
|
|
840
|
+
// Open DevTools for the specified tab
|
|
841
|
+
// Note: Chrome doesn't have a direct API to open DevTools to a specific panel
|
|
842
|
+
// We inject a script that triggers the inspector and prompt user to open DevTools
|
|
843
|
+
if (request.tabId) {
|
|
844
|
+
chrome.scripting.executeScript({
|
|
845
|
+
target: { tabId: request.tabId },
|
|
846
|
+
func: () => {
|
|
847
|
+
// Log a message suggesting to open DevTools
|
|
848
|
+
console.log('%c[GxP Inspector] Inspector enabled! Press F12 or Ctrl+Shift+J to open DevTools and see the GxP Inspector panel.', 'color: #667eea; font-weight: bold; font-size: 14px;');
|
|
849
|
+
}
|
|
850
|
+
}).catch(err => console.log('[GxP DevTools] Could not inject script:', err));
|
|
851
|
+
}
|
|
852
|
+
sendResponse({ success: true });
|
|
853
|
+
return false;
|
|
854
|
+
|
|
839
855
|
default:
|
|
840
856
|
console.warn("[JavaScript Proxy] Unknown action:", request.action);
|
|
841
857
|
sendResponse({ success: false, error: "Unknown action" });
|
|
@@ -255,18 +255,64 @@
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/* Inspector section */
|
|
258
|
-
.inspector-
|
|
258
|
+
.inspector-card {
|
|
259
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
260
|
+
border-radius: 8px;
|
|
261
|
+
padding: 14px;
|
|
262
|
+
margin-top: 16px;
|
|
263
|
+
cursor: pointer;
|
|
264
|
+
transition: all 0.2s;
|
|
265
|
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.inspector-card:hover {
|
|
269
|
+
transform: translateY(-1px);
|
|
270
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.inspector-card-header {
|
|
259
274
|
display: flex;
|
|
260
275
|
align-items: center;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
276
|
+
justify-content: space-between;
|
|
277
|
+
margin-bottom: 8px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.inspector-card-title {
|
|
281
|
+
font-size: 13px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
color: white;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.inspector-badge {
|
|
287
|
+
background: rgba(255, 255, 255, 0.2);
|
|
288
|
+
padding: 3px 8px;
|
|
289
|
+
border-radius: 10px;
|
|
290
|
+
font-size: 10px;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
color: white;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.inspector-badge.enabled {
|
|
296
|
+
background: rgba(40, 167, 69, 0.8);
|
|
265
297
|
}
|
|
266
298
|
|
|
267
|
-
.inspector-
|
|
268
|
-
padding: 4px 10px;
|
|
299
|
+
.inspector-card-desc {
|
|
269
300
|
font-size: 11px;
|
|
301
|
+
color: rgba(255, 255, 255, 0.85);
|
|
302
|
+
margin-bottom: 8px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.inspector-card-hint {
|
|
306
|
+
font-size: 10px;
|
|
307
|
+
color: rgba(255, 255, 255, 0.6);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.inspector-card-hint kbd {
|
|
311
|
+
background: rgba(255, 255, 255, 0.2);
|
|
312
|
+
color: white;
|
|
313
|
+
padding: 2px 5px;
|
|
314
|
+
border-radius: 3px;
|
|
315
|
+
font-size: 9px;
|
|
270
316
|
}
|
|
271
317
|
|
|
272
318
|
kbd {
|
|
@@ -407,6 +453,16 @@
|
|
|
407
453
|
<div class="help-text">RegEx pattern to match JavaScript file URLs</div>
|
|
408
454
|
</div>
|
|
409
455
|
</div>
|
|
456
|
+
|
|
457
|
+
<!-- Component Inspector Card -->
|
|
458
|
+
<div id="inspectorCard" class="inspector-card">
|
|
459
|
+
<div class="inspector-card-header">
|
|
460
|
+
<span class="inspector-card-title">Component Inspector</span>
|
|
461
|
+
<span id="inspectorBadge" class="inspector-badge">OFF</span>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="inspector-card-desc">Click to open DevTools and inspect Vue components</div>
|
|
464
|
+
<div class="inspector-card-hint">Or press <kbd>Ctrl+Shift+I</kbd> on the page</div>
|
|
465
|
+
</div>
|
|
410
466
|
</div>
|
|
411
467
|
|
|
412
468
|
<!-- CSS Tab -->
|
|
@@ -472,18 +528,6 @@
|
|
|
472
528
|
<label for="maskingMode">URL Masking Mode</label>
|
|
473
529
|
</div>
|
|
474
530
|
</div>
|
|
475
|
-
|
|
476
|
-
<div class="settings-section">
|
|
477
|
-
<div class="settings-title">Component Inspector</div>
|
|
478
|
-
<div style="display: flex; align-items: center; gap: 10px;">
|
|
479
|
-
<button id="inspectorToggle" class="toggle-button inspector-toggle">
|
|
480
|
-
<div class="status-dot"></div>
|
|
481
|
-
<span id="inspectorText">OFF</span>
|
|
482
|
-
</button>
|
|
483
|
-
<span style="font-size: 11px; color: #6c757d;">or press <kbd>Ctrl+Shift+I</kbd></span>
|
|
484
|
-
</div>
|
|
485
|
-
<div class="help-text" style="margin-top: 8px;">Inspect Vue components and extract hardcoded strings</div>
|
|
486
|
-
</div>
|
|
487
531
|
</div>
|
|
488
532
|
</div>
|
|
489
533
|
|