@gxp-dev/tools 2.0.63 → 2.0.65

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 (182) hide show
  1. package/README.md +32 -31
  2. package/bin/gx-devtools.js +74 -54
  3. package/bin/lib/cli.js +23 -21
  4. package/bin/lib/commands/add-dependency.js +366 -325
  5. package/bin/lib/commands/assets.js +137 -139
  6. package/bin/lib/commands/build.js +169 -174
  7. package/bin/lib/commands/datastore.js +181 -183
  8. package/bin/lib/commands/dev.js +127 -131
  9. package/bin/lib/commands/extensions.js +147 -149
  10. package/bin/lib/commands/extract-config.js +73 -67
  11. package/bin/lib/commands/index.js +12 -12
  12. package/bin/lib/commands/init.js +342 -240
  13. package/bin/lib/commands/publish.js +69 -75
  14. package/bin/lib/commands/socket.js +69 -69
  15. package/bin/lib/commands/ssl.js +14 -14
  16. package/bin/lib/constants.js +10 -24
  17. package/bin/lib/tui/App.tsx +761 -705
  18. package/bin/lib/tui/components/AIPanel.tsx +191 -171
  19. package/bin/lib/tui/components/CommandInput.tsx +394 -343
  20. package/bin/lib/tui/components/GeminiPanel.tsx +175 -151
  21. package/bin/lib/tui/components/Header.tsx +23 -21
  22. package/bin/lib/tui/components/LogPanel.tsx +244 -220
  23. package/bin/lib/tui/components/TabBar.tsx +50 -48
  24. package/bin/lib/tui/components/WelcomeScreen.tsx +126 -71
  25. package/bin/lib/tui/index.tsx +37 -39
  26. package/bin/lib/tui/services/AIService.ts +518 -462
  27. package/bin/lib/tui/services/ExtensionService.ts +140 -129
  28. package/bin/lib/tui/services/GeminiService.ts +367 -337
  29. package/bin/lib/tui/services/ServiceManager.ts +344 -322
  30. package/bin/lib/tui/services/SocketService.ts +168 -168
  31. package/bin/lib/tui/services/ViteService.ts +88 -88
  32. package/bin/lib/tui/services/index.ts +47 -22
  33. package/bin/lib/utils/ai-scaffold.js +291 -280
  34. package/bin/lib/utils/extract-config.js +157 -140
  35. package/bin/lib/utils/files.js +82 -86
  36. package/bin/lib/utils/index.js +7 -7
  37. package/bin/lib/utils/paths.js +34 -34
  38. package/bin/lib/utils/prompts.js +194 -169
  39. package/bin/lib/utils/ssl.js +79 -81
  40. package/browser-extensions/README.md +0 -1
  41. package/browser-extensions/chrome/background.js +244 -237
  42. package/browser-extensions/chrome/content.js +32 -29
  43. package/browser-extensions/chrome/devtools.html +7 -7
  44. package/browser-extensions/chrome/devtools.js +19 -19
  45. package/browser-extensions/chrome/inspector.js +802 -767
  46. package/browser-extensions/chrome/manifest.json +71 -63
  47. package/browser-extensions/chrome/panel.html +674 -636
  48. package/browser-extensions/chrome/panel.js +722 -712
  49. package/browser-extensions/chrome/popup.html +586 -543
  50. package/browser-extensions/chrome/popup.js +282 -244
  51. package/browser-extensions/chrome/rules.json +1 -1
  52. package/browser-extensions/chrome/test-chrome.html +216 -136
  53. package/browser-extensions/chrome/test-mixed-content.html +284 -189
  54. package/browser-extensions/chrome/test-uri-pattern.html +221 -198
  55. package/browser-extensions/firefox/README.md +9 -6
  56. package/browser-extensions/firefox/background.js +221 -218
  57. package/browser-extensions/firefox/content.js +55 -52
  58. package/browser-extensions/firefox/debug-errors.html +386 -228
  59. package/browser-extensions/firefox/debug-https.html +153 -105
  60. package/browser-extensions/firefox/devtools.html +7 -7
  61. package/browser-extensions/firefox/devtools.js +23 -20
  62. package/browser-extensions/firefox/inspector.js +802 -767
  63. package/browser-extensions/firefox/manifest.json +68 -68
  64. package/browser-extensions/firefox/panel.html +674 -636
  65. package/browser-extensions/firefox/panel.js +722 -712
  66. package/browser-extensions/firefox/popup.html +572 -535
  67. package/browser-extensions/firefox/popup.js +281 -236
  68. package/browser-extensions/firefox/test-gramercy.html +170 -125
  69. package/browser-extensions/firefox/test-imports.html +59 -55
  70. package/browser-extensions/firefox/test-masking.html +231 -140
  71. package/browser-extensions/firefox/test-uri-pattern.html +221 -198
  72. package/dist/tui/App.d.ts +1 -1
  73. package/dist/tui/App.d.ts.map +1 -1
  74. package/dist/tui/App.js +154 -150
  75. package/dist/tui/App.js.map +1 -1
  76. package/dist/tui/components/AIPanel.d.ts.map +1 -1
  77. package/dist/tui/components/AIPanel.js +42 -35
  78. package/dist/tui/components/AIPanel.js.map +1 -1
  79. package/dist/tui/components/CommandInput.d.ts +1 -1
  80. package/dist/tui/components/CommandInput.d.ts.map +1 -1
  81. package/dist/tui/components/CommandInput.js +92 -62
  82. package/dist/tui/components/CommandInput.js.map +1 -1
  83. package/dist/tui/components/GeminiPanel.d.ts.map +1 -1
  84. package/dist/tui/components/GeminiPanel.js +37 -30
  85. package/dist/tui/components/GeminiPanel.js.map +1 -1
  86. package/dist/tui/components/Header.d.ts.map +1 -1
  87. package/dist/tui/components/Header.js +1 -1
  88. package/dist/tui/components/Header.js.map +1 -1
  89. package/dist/tui/components/LogPanel.d.ts +1 -1
  90. package/dist/tui/components/LogPanel.d.ts.map +1 -1
  91. package/dist/tui/components/LogPanel.js +26 -24
  92. package/dist/tui/components/LogPanel.js.map +1 -1
  93. package/dist/tui/components/TabBar.d.ts +2 -2
  94. package/dist/tui/components/TabBar.d.ts.map +1 -1
  95. package/dist/tui/components/TabBar.js +11 -11
  96. package/dist/tui/components/TabBar.js.map +1 -1
  97. package/dist/tui/components/WelcomeScreen.d.ts.map +1 -1
  98. package/dist/tui/components/WelcomeScreen.js +6 -6
  99. package/dist/tui/components/WelcomeScreen.js.map +1 -1
  100. package/dist/tui/index.d.ts.map +1 -1
  101. package/dist/tui/index.js +8 -8
  102. package/dist/tui/index.js.map +1 -1
  103. package/dist/tui/services/AIService.d.ts +2 -2
  104. package/dist/tui/services/AIService.d.ts.map +1 -1
  105. package/dist/tui/services/AIService.js +165 -125
  106. package/dist/tui/services/AIService.js.map +1 -1
  107. package/dist/tui/services/ExtensionService.d.ts +1 -1
  108. package/dist/tui/services/ExtensionService.d.ts.map +1 -1
  109. package/dist/tui/services/ExtensionService.js +33 -26
  110. package/dist/tui/services/ExtensionService.js.map +1 -1
  111. package/dist/tui/services/GeminiService.d.ts +1 -1
  112. package/dist/tui/services/GeminiService.d.ts.map +1 -1
  113. package/dist/tui/services/GeminiService.js +87 -76
  114. package/dist/tui/services/GeminiService.js.map +1 -1
  115. package/dist/tui/services/ServiceManager.d.ts +3 -3
  116. package/dist/tui/services/ServiceManager.d.ts.map +1 -1
  117. package/dist/tui/services/ServiceManager.js +72 -58
  118. package/dist/tui/services/ServiceManager.js.map +1 -1
  119. package/dist/tui/services/SocketService.d.ts.map +1 -1
  120. package/dist/tui/services/SocketService.js +32 -32
  121. package/dist/tui/services/SocketService.js.map +1 -1
  122. package/dist/tui/services/ViteService.d.ts.map +1 -1
  123. package/dist/tui/services/ViteService.js +26 -28
  124. package/dist/tui/services/ViteService.js.map +1 -1
  125. package/dist/tui/services/index.d.ts +6 -6
  126. package/dist/tui/services/index.d.ts.map +1 -1
  127. package/dist/tui/services/index.js +6 -6
  128. package/dist/tui/services/index.js.map +1 -1
  129. package/mcp/gxp-api-server.js +83 -81
  130. package/package.json +109 -93
  131. package/runtime/PortalContainer.vue +258 -234
  132. package/runtime/dev-tools/DevToolsModal.vue +153 -155
  133. package/runtime/dev-tools/LayoutSwitcher.vue +144 -140
  134. package/runtime/dev-tools/MockDataEditor.vue +456 -433
  135. package/runtime/dev-tools/SocketSimulator.vue +379 -371
  136. package/runtime/dev-tools/StoreInspector.vue +517 -455
  137. package/runtime/dev-tools/index.js +5 -5
  138. package/runtime/fallback-layouts/PrivateLayout.vue +2 -2
  139. package/runtime/fallback-layouts/PublicLayout.vue +2 -2
  140. package/runtime/fallback-layouts/SystemLayout.vue +2 -2
  141. package/runtime/gxpStringsPlugin.js +159 -134
  142. package/runtime/index.html +17 -19
  143. package/runtime/main.js +24 -22
  144. package/runtime/mock-api/auth-middleware.js +15 -15
  145. package/runtime/mock-api/image-generator.js +46 -46
  146. package/runtime/mock-api/index.js +55 -55
  147. package/runtime/mock-api/response-generator.js +116 -105
  148. package/runtime/mock-api/route-generator.js +107 -84
  149. package/runtime/mock-api/socket-triggers.js +94 -93
  150. package/runtime/mock-api/spec-loader.js +79 -80
  151. package/runtime/package.json +3 -0
  152. package/runtime/server.js +68 -68
  153. package/runtime/stores/gxpPortalConfigStore.js +204 -186
  154. package/runtime/stores/index.js +2 -2
  155. package/runtime/vite-inspector-plugin.js +858 -707
  156. package/runtime/vite-source-tracker-plugin.js +132 -113
  157. package/runtime/vite.config.js +191 -139
  158. package/scripts/launch-chrome.js +41 -41
  159. package/scripts/pack-chrome.js +38 -39
  160. package/socket-events/AiSessionMessageCreated.json +17 -17
  161. package/socket-events/SocialStreamPostCreated.json +23 -23
  162. package/socket-events/SocialStreamPostVariantCompleted.json +22 -22
  163. package/template/.claude/agents/gxp-developer.md +100 -99
  164. package/template/.claude/settings.json +7 -7
  165. package/template/AGENTS.md +30 -23
  166. package/template/GEMINI.md +20 -20
  167. package/template/README.md +70 -53
  168. package/template/app-manifest.json +2 -4
  169. package/template/configuration.json +10 -10
  170. package/template/default-styling.css +1 -1
  171. package/template/index.html +18 -20
  172. package/template/main.js +24 -22
  173. package/template/src/DemoPage.vue +415 -362
  174. package/template/src/Plugin.vue +76 -85
  175. package/template/src/stores/index.js +3 -3
  176. package/template/src/stores/test-data.json +164 -172
  177. package/template/theme-layouts/AdditionalStyling.css +50 -50
  178. package/template/theme-layouts/PrivateLayout.vue +8 -12
  179. package/template/theme-layouts/PublicLayout.vue +8 -12
  180. package/template/theme-layouts/SystemLayout.vue +8 -12
  181. package/template/vite.extend.js +45 -0
  182. package/template/vite.config.js +0 -409
@@ -1,509 +1,565 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { spawn, execSync } from 'child_process';
4
- import { EventEmitter } from 'events';
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import { spawn, execSync } from "child_process"
4
+ import { EventEmitter } from "events"
5
5
 
6
6
  // AI Provider types
7
- export type AIProvider = 'claude' | 'codex' | 'gemini';
7
+ export type AIProvider = "claude" | "codex" | "gemini"
8
8
 
9
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;
10
+ id: AIProvider
11
+ name: string
12
+ available: boolean
13
+ method?: string // For gemini: 'cli', 'api_key', 'gcloud'
14
+ reason?: string
15
15
  }
16
16
 
17
17
  // AI Configuration
18
18
  export interface AIConfig {
19
- provider: AIProvider;
20
- systemPrompt: string;
21
- projectContext: boolean;
22
- maxContextTokens: number;
19
+ provider: AIProvider
20
+ systemPrompt: string
21
+ projectContext: boolean
22
+ maxContextTokens: number
23
23
  }
24
24
 
25
25
  // Get the gxdev config directory
26
26
  function getConfigDir(): string {
27
- const home = process.env.HOME || process.env.USERPROFILE || '';
28
- return path.join(home, '.gxdev');
27
+ const home = process.env.HOME || process.env.USERPROFILE || ""
28
+ return path.join(home, ".gxdev")
29
29
  }
30
30
 
31
31
  // Ensure config directory exists
32
32
  function ensureConfigDir(): void {
33
- const configDir = getConfigDir();
34
- if (!fs.existsSync(configDir)) {
35
- fs.mkdirSync(configDir, { recursive: true });
36
- }
33
+ const configDir = getConfigDir()
34
+ if (!fs.existsSync(configDir)) {
35
+ fs.mkdirSync(configDir, { recursive: true })
36
+ }
37
37
  }
38
38
 
39
39
  // Get AI config file path
40
40
  function getAIConfigPath(): string {
41
- return path.join(getConfigDir(), 'ai-config.json');
41
+ return path.join(getConfigDir(), "ai-config.json")
42
42
  }
43
43
 
44
44
  // Load AI config
45
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
- };
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:
58
+ "You are a helpful assistant for GxP plugin development. Help the user build Vue.js components for the GxP kiosk platform.",
59
+ projectContext: true,
60
+ maxContextTokens: 4000,
61
+ }
61
62
  }
62
63
 
63
64
  // Save AI config
64
65
  export function saveAIConfig(config: AIConfig): void {
65
- ensureConfigDir();
66
- const configPath = getAIConfigPath();
67
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
66
+ ensureConfigDir()
67
+ const configPath = getAIConfigPath()
68
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
68
69
  }
69
70
 
70
71
  // Check if a command exists
71
72
  function commandExists(cmd: string): boolean {
72
- try {
73
- execSync(`which ${cmd}`, { stdio: 'pipe' });
74
- return true;
75
- } catch {
76
- return false;
77
- }
73
+ try {
74
+ execSync(`which ${cmd}`, { stdio: "pipe" })
75
+ return true
76
+ } catch {
77
+ return false
78
+ }
78
79
  }
79
80
 
80
81
  // Check available AI providers
81
82
  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;
83
+ const providers: AIProviderInfo[] = []
84
+
85
+ // Check Claude CLI
86
+ if (commandExists("claude")) {
87
+ providers.push({
88
+ id: "claude",
89
+ name: "Claude",
90
+ available: true,
91
+ })
92
+ } else {
93
+ providers.push({
94
+ id: "claude",
95
+ name: "Claude",
96
+ available: false,
97
+ reason: "Install: npm i -g @anthropic-ai/claude-code && claude login",
98
+ })
99
+ }
100
+
101
+ // Check Codex CLI
102
+ if (commandExists("codex")) {
103
+ providers.push({
104
+ id: "codex",
105
+ name: "Codex",
106
+ available: true,
107
+ })
108
+ } else {
109
+ providers.push({
110
+ id: "codex",
111
+ name: "Codex",
112
+ available: false,
113
+ reason: "Install: npm i -g @openai/codex && codex auth",
114
+ })
115
+ }
116
+
117
+ // Check Gemini (CLI, API key, or gcloud)
118
+ if (commandExists("gemini")) {
119
+ providers.push({
120
+ id: "gemini",
121
+ name: "Gemini",
122
+ available: true,
123
+ method: "cli",
124
+ })
125
+ } else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
126
+ providers.push({
127
+ id: "gemini",
128
+ name: "Gemini",
129
+ available: true,
130
+ method: "api_key",
131
+ })
132
+ } else if (commandExists("gcloud")) {
133
+ try {
134
+ const authList = execSync("gcloud auth list --format='value(account)'", {
135
+ stdio: "pipe",
136
+ }).toString()
137
+ if (authList.trim()) {
138
+ providers.push({
139
+ id: "gemini",
140
+ name: "Gemini",
141
+ available: true,
142
+ method: "gcloud",
143
+ })
144
+ }
145
+ } catch {
146
+ providers.push({
147
+ id: "gemini",
148
+ name: "Gemini",
149
+ available: false,
150
+ reason: "Install: npm i -g @google/gemini-cli && gemini",
151
+ })
152
+ }
153
+ } else {
154
+ providers.push({
155
+ id: "gemini",
156
+ name: "Gemini",
157
+ available: false,
158
+ reason: "Install: npm i -g @google/gemini-cli && gemini",
159
+ })
160
+ }
161
+
162
+ return providers
160
163
  }
161
164
 
162
165
  // Get provider display name with status
163
166
  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)`;
167
+ if (!provider.available) {
168
+ return `${provider.name} (not available)`
169
+ }
170
+ if (provider.method) {
171
+ switch (provider.method) {
172
+ case "cli":
173
+ return `${provider.name} (CLI)`
174
+ case "api_key":
175
+ return `${provider.name} (API key)`
176
+ case "gcloud":
177
+ return `${provider.name} (gcloud)`
178
+ }
179
+ }
180
+ return `${provider.name} (logged in)`
178
181
  }
179
182
 
180
183
  // AI Service class
181
184
  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
- }
185
+ private conversationHistory: Array<{ role: string; content: string }> = []
186
+ private projectContext: string = ""
187
+ private currentProvider: AIProvider
188
+ private geminiMethod?: string
189
+
190
+ constructor() {
191
+ super()
192
+ const config = loadAIConfig()
193
+ this.currentProvider = config.provider
194
+
195
+ // Determine gemini method if that's the current provider
196
+ const providers = getAvailableProviders()
197
+ const geminiProvider = providers.find((p) => p.id === "gemini")
198
+ if (geminiProvider?.available) {
199
+ this.geminiMethod = geminiProvider.method
200
+ }
201
+ }
202
+
203
+ // Get current provider
204
+ getProvider(): AIProvider {
205
+ return this.currentProvider
206
+ }
207
+
208
+ // Set current provider
209
+ setProvider(provider: AIProvider): { success: boolean; message: string } {
210
+ const providers = getAvailableProviders()
211
+ const providerInfo = providers.find((p) => p.id === provider)
212
+
213
+ if (!providerInfo) {
214
+ return { success: false, message: `Unknown provider: ${provider}` }
215
+ }
216
+
217
+ if (!providerInfo.available) {
218
+ return {
219
+ success: false,
220
+ message: `${providerInfo.name} is not available. ${providerInfo.reason || ""}`,
221
+ }
222
+ }
223
+
224
+ this.currentProvider = provider
225
+ if (provider === "gemini") {
226
+ this.geminiMethod = providerInfo.method
227
+ }
228
+
229
+ // Save to config
230
+ const config = loadAIConfig()
231
+ config.provider = provider
232
+ saveAIConfig(config)
233
+
234
+ this.clearConversation()
235
+ return { success: true, message: `Switched to ${providerInfo.name}` }
236
+ }
237
+
238
+ // Check if current provider is available
239
+ isAvailable(): boolean {
240
+ const providers = getAvailableProviders()
241
+ const current = providers.find((p) => p.id === this.currentProvider)
242
+ return current?.available || false
243
+ }
244
+
245
+ // Get provider info
246
+ getProviderInfo(): AIProviderInfo | undefined {
247
+ const providers = getAvailableProviders()
248
+ return providers.find((p) => p.id === this.currentProvider)
249
+ }
250
+
251
+ // Load project context
252
+ loadProjectContext(cwd: string): void {
253
+ const files = [
254
+ "CLAUDE.md",
255
+ "AGENTS.md",
256
+ "GEMINI.md",
257
+ "README.md",
258
+ "package.json",
259
+ "app-manifest.json",
260
+ ]
261
+ const contextParts: string[] = []
262
+
263
+ for (const file of files) {
264
+ const filePath = path.join(cwd, file)
265
+ if (fs.existsSync(filePath)) {
266
+ try {
267
+ const content = fs.readFileSync(filePath, "utf-8")
268
+ // Limit each file to 2000 chars
269
+ contextParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`)
270
+ } catch {
271
+ // Skip unreadable files
272
+ }
273
+ }
274
+ }
275
+
276
+ this.projectContext = contextParts.join("\n\n")
277
+ }
278
+
279
+ // Send message using current provider
280
+ async sendMessage(message: string): Promise<string> {
281
+ const config = loadAIConfig()
282
+
283
+ // Build context
284
+ let systemContext = config.systemPrompt || ""
285
+ if (config.projectContext && this.projectContext) {
286
+ systemContext += "\n\nProject Context:\n" + this.projectContext
287
+ }
288
+
289
+ switch (this.currentProvider) {
290
+ case "claude":
291
+ return this.sendWithClaude(message, systemContext)
292
+ case "codex":
293
+ return this.sendWithCodex(message, systemContext)
294
+ case "gemini":
295
+ return this.sendWithGemini(message, systemContext)
296
+ default:
297
+ throw new Error(`Unknown provider: ${this.currentProvider}`)
298
+ }
299
+ }
300
+
301
+ // Send message with Claude CLI
302
+ private async sendWithClaude(
303
+ message: string,
304
+ systemContext: string,
305
+ ): Promise<string> {
306
+ return new Promise((resolve, reject) => {
307
+ let output = ""
308
+ let errorOutput = ""
309
+
310
+ const fullPrompt = systemContext
311
+ ? `${systemContext}\n\nUser: ${message}`
312
+ : message
313
+
314
+ const claude = spawn("claude", ["--print", "-p", fullPrompt], {
315
+ stdio: ["pipe", "pipe", "pipe"],
316
+ shell: true,
317
+ })
318
+
319
+ claude.stdout.on("data", (data) => {
320
+ output += data.toString()
321
+ })
322
+
323
+ claude.stderr.on("data", (data) => {
324
+ errorOutput += data.toString()
325
+ })
326
+
327
+ claude.on("close", (code) => {
328
+ if (code !== 0) {
329
+ reject(new Error(`Claude error: ${errorOutput || "Unknown error"}`))
330
+ return
331
+ }
332
+ this.conversationHistory.push({ role: "user", content: message })
333
+ this.conversationHistory.push({ role: "assistant", content: output })
334
+ resolve(output.trim())
335
+ })
336
+
337
+ claude.on("error", (err) => {
338
+ reject(new Error(`Failed to run Claude: ${err.message}`))
339
+ })
340
+ })
341
+ }
342
+
343
+ // Send message with Codex CLI
344
+ private async sendWithCodex(
345
+ message: string,
346
+ systemContext: string,
347
+ ): Promise<string> {
348
+ return new Promise((resolve, reject) => {
349
+ let output = ""
350
+ let errorOutput = ""
351
+
352
+ const fullPrompt = systemContext
353
+ ? `${systemContext}\n\nUser: ${message}`
354
+ : message
355
+
356
+ const codex = spawn("codex", ["--quiet", "-p", fullPrompt], {
357
+ stdio: ["pipe", "pipe", "pipe"],
358
+ shell: true,
359
+ })
360
+
361
+ codex.stdout.on("data", (data) => {
362
+ output += data.toString()
363
+ })
364
+
365
+ codex.stderr.on("data", (data) => {
366
+ errorOutput += data.toString()
367
+ })
368
+
369
+ codex.on("close", (code) => {
370
+ if (code !== 0) {
371
+ reject(new Error(`Codex error: ${errorOutput || "Unknown error"}`))
372
+ return
373
+ }
374
+ this.conversationHistory.push({ role: "user", content: message })
375
+ this.conversationHistory.push({ role: "assistant", content: output })
376
+ resolve(output.trim())
377
+ })
378
+
379
+ codex.on("error", (err) => {
380
+ reject(new Error(`Failed to run Codex: ${err.message}`))
381
+ })
382
+ })
383
+ }
384
+
385
+ // Send message with Gemini
386
+ private async sendWithGemini(
387
+ message: string,
388
+ systemContext: string,
389
+ ): Promise<string> {
390
+ const fullPrompt = systemContext
391
+ ? `${systemContext}\n\nUser: ${message}`
392
+ : message
393
+
394
+ if (this.geminiMethod === "cli") {
395
+ return this.sendWithGeminiCli(fullPrompt)
396
+ } else if (this.geminiMethod === "api_key") {
397
+ return this.sendWithGeminiApi(fullPrompt)
398
+ } else if (this.geminiMethod === "gcloud") {
399
+ return this.sendWithGeminiGcloud(fullPrompt)
400
+ }
401
+
402
+ throw new Error("Gemini is not properly configured")
403
+ }
404
+
405
+ // Send with Gemini CLI
406
+ private async sendWithGeminiCli(prompt: string): Promise<string> {
407
+ return new Promise((resolve, reject) => {
408
+ let output = ""
409
+ let errorOutput = ""
410
+
411
+ const gemini = spawn("gemini", ["-p", prompt], {
412
+ stdio: ["pipe", "pipe", "pipe"],
413
+ shell: true,
414
+ })
415
+
416
+ gemini.stdout.on("data", (data) => {
417
+ output += data.toString()
418
+ })
419
+
420
+ gemini.stderr.on("data", (data) => {
421
+ errorOutput += data.toString()
422
+ })
423
+
424
+ gemini.on("close", (code) => {
425
+ if (code !== 0) {
426
+ reject(new Error(`Gemini error: ${errorOutput || "Unknown error"}`))
427
+ return
428
+ }
429
+ this.conversationHistory.push({ role: "user", content: prompt })
430
+ this.conversationHistory.push({ role: "assistant", content: output })
431
+ resolve(output.trim())
432
+ })
433
+
434
+ gemini.on("error", (err) => {
435
+ reject(new Error(`Failed to run Gemini: ${err.message}`))
436
+ })
437
+ })
438
+ }
439
+
440
+ // Send with Gemini API
441
+ private async sendWithGeminiApi(prompt: string): Promise<string> {
442
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
443
+ if (!apiKey) {
444
+ throw new Error("GEMINI_API_KEY not set")
445
+ }
446
+
447
+ const response = await fetch(
448
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
449
+ {
450
+ method: "POST",
451
+ headers: { "Content-Type": "application/json" },
452
+ body: JSON.stringify({
453
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
454
+ generationConfig: { maxOutputTokens: 2048, temperature: 0.7 },
455
+ }),
456
+ },
457
+ )
458
+
459
+ if (!response.ok) {
460
+ const errorText = await response.text()
461
+ throw new Error(`Gemini API error: ${response.status} - ${errorText}`)
462
+ }
463
+
464
+ const data = (await response.json()) as any
465
+ const responseText =
466
+ data.candidates?.[0]?.content?.parts?.[0]?.text ||
467
+ "No response generated."
468
+
469
+ this.conversationHistory.push({ role: "user", content: prompt })
470
+ this.conversationHistory.push({ role: "assistant", content: responseText })
471
+
472
+ return responseText
473
+ }
474
+
475
+ // Send with Gemini via gcloud
476
+ private async sendWithGeminiGcloud(prompt: string): Promise<string> {
477
+ return new Promise((resolve, reject) => {
478
+ let accessToken: string
479
+ let projectId: string
480
+
481
+ try {
482
+ accessToken = execSync("gcloud auth print-access-token", {
483
+ stdio: "pipe",
484
+ })
485
+ .toString()
486
+ .trim()
487
+ projectId = execSync("gcloud config get-value project", {
488
+ stdio: "pipe",
489
+ })
490
+ .toString()
491
+ .trim()
492
+ } catch (error) {
493
+ reject(new Error("Failed to get gcloud credentials"))
494
+ return
495
+ }
496
+
497
+ const requestBody = JSON.stringify({
498
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
499
+ generationConfig: { maxOutputTokens: 2048, temperature: 0.7 },
500
+ })
501
+
502
+ const curl = spawn(
503
+ "curl",
504
+ [
505
+ "-s",
506
+ "-X",
507
+ "POST",
508
+ `https://us-central1-aiplatform.googleapis.com/v1/projects/${projectId}/locations/us-central1/publishers/google/models/gemini-1.5-flash:generateContent`,
509
+ "-H",
510
+ `Authorization: Bearer ${accessToken}`,
511
+ "-H",
512
+ "Content-Type: application/json",
513
+ "-d",
514
+ requestBody,
515
+ ],
516
+ { stdio: ["pipe", "pipe", "pipe"] },
517
+ )
518
+
519
+ let output = ""
520
+ let errorOutput = ""
521
+
522
+ curl.stdout.on("data", (data) => {
523
+ output += data.toString()
524
+ })
525
+ curl.stderr.on("data", (data) => {
526
+ errorOutput += data.toString()
527
+ })
528
+
529
+ curl.on("close", (code) => {
530
+ if (code !== 0) {
531
+ reject(new Error(`Gemini gcloud error: ${errorOutput}`))
532
+ return
533
+ }
534
+
535
+ try {
536
+ const data = JSON.parse(output)
537
+ const responseText =
538
+ data.candidates?.[0]?.content?.parts?.[0]?.text ||
539
+ "No response generated."
540
+ this.conversationHistory.push({ role: "user", content: prompt })
541
+ this.conversationHistory.push({
542
+ role: "assistant",
543
+ content: responseText,
544
+ })
545
+ resolve(responseText)
546
+ } catch (parseError) {
547
+ reject(new Error(`Failed to parse Gemini response`))
548
+ }
549
+ })
550
+ })
551
+ }
552
+
553
+ // Clear conversation history
554
+ clearConversation(): void {
555
+ this.conversationHistory = []
556
+ }
557
+
558
+ // Get conversation history
559
+ getConversationHistory(): Array<{ role: string; content: string }> {
560
+ return [...this.conversationHistory]
561
+ }
506
562
  }
507
563
 
508
564
  // Singleton instance
509
- export const aiService = new AIService();
565
+ export const aiService = new AIService()