@gxp-dev/tools 2.0.62 → 2.0.64

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 +207 -132
  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,395 +1,425 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import http from 'http';
4
- import { fileURLToPath } from 'url';
5
- import open from 'open';
6
- import { EventEmitter } from 'events';
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import http from "http"
4
+ import { fileURLToPath } from "url"
5
+ import open from "open"
6
+ import { EventEmitter } from "events"
7
7
 
8
8
  // Gemini configuration interface
9
9
  export interface GeminiConfig {
10
- systemPrompt?: string;
11
- includeDocs?: string[];
12
- projectContext?: boolean;
13
- maxContextTokens?: number;
10
+ systemPrompt?: string
11
+ includeDocs?: string[]
12
+ projectContext?: boolean
13
+ maxContextTokens?: number
14
14
  }
15
15
 
16
16
  // Auth tokens interface
17
17
  interface AuthTokens {
18
- accessToken: string;
19
- refreshToken: string;
20
- expiresAt: number;
18
+ accessToken: string
19
+ refreshToken: string
20
+ expiresAt: number
21
21
  }
22
22
 
23
23
  // Get the gxdev config directory
24
24
  function getConfigDir(): string {
25
- const home = process.env.HOME || process.env.USERPROFILE || '';
26
- return path.join(home, '.gxdev');
25
+ const home = process.env.HOME || process.env.USERPROFILE || ""
26
+ return path.join(home, ".gxdev")
27
27
  }
28
28
 
29
29
  // Ensure config directory exists
30
30
  function ensureConfigDir(): void {
31
- const configDir = getConfigDir();
32
- if (!fs.existsSync(configDir)) {
33
- fs.mkdirSync(configDir, { recursive: true });
34
- }
31
+ const configDir = getConfigDir()
32
+ if (!fs.existsSync(configDir)) {
33
+ fs.mkdirSync(configDir, { recursive: true })
34
+ }
35
35
  }
36
36
 
37
37
  // Get auth file path
38
38
  function getAuthFilePath(): string {
39
- return path.join(getConfigDir(), 'gemini-auth.json');
39
+ return path.join(getConfigDir(), "gemini-auth.json")
40
40
  }
41
41
 
42
42
  // Get config file path
43
43
  function getConfigFilePath(): string {
44
- return path.join(getConfigDir(), 'gemini-config.json');
44
+ return path.join(getConfigDir(), "gemini-config.json")
45
45
  }
46
46
 
47
47
  // Get docs directory path
48
48
  function getDocsDir(): string {
49
- return path.join(getConfigDir(), 'gemini-docs');
49
+ return path.join(getConfigDir(), "gemini-docs")
50
50
  }
51
51
 
52
52
  // Load saved auth tokens
53
53
  export function loadAuthTokens(): AuthTokens | null {
54
- try {
55
- const authPath = getAuthFilePath();
56
- if (fs.existsSync(authPath)) {
57
- const content = fs.readFileSync(authPath, 'utf-8');
58
- return JSON.parse(content);
59
- }
60
- } catch {
61
- // Invalid or missing auth file
62
- }
63
- return null;
54
+ try {
55
+ const authPath = getAuthFilePath()
56
+ if (fs.existsSync(authPath)) {
57
+ const content = fs.readFileSync(authPath, "utf-8")
58
+ return JSON.parse(content)
59
+ }
60
+ } catch {
61
+ // Invalid or missing auth file
62
+ }
63
+ return null
64
64
  }
65
65
 
66
66
  // Save auth tokens
67
67
  function saveAuthTokens(tokens: AuthTokens): void {
68
- ensureConfigDir();
69
- const authPath = getAuthFilePath();
70
- fs.writeFileSync(authPath, JSON.stringify(tokens, null, 2));
68
+ ensureConfigDir()
69
+ const authPath = getAuthFilePath()
70
+ fs.writeFileSync(authPath, JSON.stringify(tokens, null, 2))
71
71
  }
72
72
 
73
73
  // Clear auth tokens
74
74
  export function clearAuthTokens(): void {
75
- const authPath = getAuthFilePath();
76
- if (fs.existsSync(authPath)) {
77
- fs.unlinkSync(authPath);
78
- }
75
+ const authPath = getAuthFilePath()
76
+ if (fs.existsSync(authPath)) {
77
+ fs.unlinkSync(authPath)
78
+ }
79
79
  }
80
80
 
81
81
  // Load Gemini config
82
82
  export function loadGeminiConfig(): GeminiConfig {
83
- try {
84
- const configPath = getConfigFilePath();
85
- if (fs.existsSync(configPath)) {
86
- const content = fs.readFileSync(configPath, 'utf-8');
87
- return JSON.parse(content);
88
- }
89
- } catch {
90
- // Invalid or missing config file
91
- }
92
- return {
93
- systemPrompt: 'You are a helpful assistant for GxP plugin development.',
94
- includeDocs: [],
95
- projectContext: true,
96
- maxContextTokens: 4000,
97
- };
83
+ try {
84
+ const configPath = getConfigFilePath()
85
+ if (fs.existsSync(configPath)) {
86
+ const content = fs.readFileSync(configPath, "utf-8")
87
+ return JSON.parse(content)
88
+ }
89
+ } catch {
90
+ // Invalid or missing config file
91
+ }
92
+ return {
93
+ systemPrompt: "You are a helpful assistant for GxP plugin development.",
94
+ includeDocs: [],
95
+ projectContext: true,
96
+ maxContextTokens: 4000,
97
+ }
98
98
  }
99
99
 
100
100
  // Save Gemini config
101
101
  export function saveGeminiConfig(config: GeminiConfig): void {
102
- ensureConfigDir();
103
- const configPath = getConfigFilePath();
104
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
102
+ ensureConfigDir()
103
+ const configPath = getConfigFilePath()
104
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
105
105
  }
106
106
 
107
107
  // Check if authenticated
108
108
  export function isAuthenticated(): boolean {
109
- const tokens = loadAuthTokens();
110
- if (!tokens) return false;
111
- // Check if token is expired (with 5 min buffer)
112
- return tokens.expiresAt > Date.now() + 5 * 60 * 1000;
109
+ const tokens = loadAuthTokens()
110
+ if (!tokens) return false
111
+ // Check if token is expired (with 5 min buffer)
112
+ return tokens.expiresAt > Date.now() + 5 * 60 * 1000
113
113
  }
114
114
 
115
115
  // Gemini Service class
116
116
  export class GeminiService extends EventEmitter {
117
- private conversationHistory: Array<{ role: string; content: string }> = [];
118
- private projectContext: string = '';
119
-
120
- constructor() {
121
- super();
122
- }
123
-
124
- // Start OAuth flow
125
- async startOAuthFlow(): Promise<{ success: boolean; message: string }> {
126
- return new Promise((resolve) => {
127
- // Create local server for OAuth callback
128
- const PORT = 8234;
129
- let server: http.Server;
130
-
131
- const handleCallback = (req: http.IncomingMessage, res: http.ServerResponse) => {
132
- const url = new URL(req.url || '', `http://localhost:${PORT}`);
133
-
134
- if (url.pathname === '/callback') {
135
- const code = url.searchParams.get('code');
136
- const error = url.searchParams.get('error');
137
-
138
- if (error) {
139
- res.writeHead(200, { 'Content-Type': 'text/html' });
140
- res.end('<html><body><h1>Authentication Failed</h1><p>You can close this window.</p></body></html>');
141
- server.close();
142
- resolve({ success: false, message: `OAuth error: ${error}` });
143
- return;
144
- }
145
-
146
- if (code) {
147
- // Exchange code for tokens
148
- this.exchangeCodeForTokens(code)
149
- .then((tokens) => {
150
- saveAuthTokens(tokens);
151
- res.writeHead(200, { 'Content-Type': 'text/html' });
152
- res.end('<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to gxdev.</p></body></html>');
153
- server.close();
154
- resolve({ success: true, message: 'Successfully authenticated with Google.' });
155
- })
156
- .catch((err) => {
157
- res.writeHead(200, { 'Content-Type': 'text/html' });
158
- res.end('<html><body><h1>Authentication Failed</h1><p>Error exchanging code for tokens.</p></body></html>');
159
- server.close();
160
- resolve({ success: false, message: `Token exchange error: ${err.message}` });
161
- });
162
- return;
163
- }
164
- }
165
-
166
- res.writeHead(404);
167
- res.end('Not found');
168
- };
169
-
170
- server = http.createServer(handleCallback);
171
-
172
- server.listen(PORT, () => {
173
- // Construct OAuth URL
174
- // Note: These are placeholder values - you'll need to set up actual Google Cloud credentials
175
- const clientId = process.env.GOOGLE_CLIENT_ID || 'YOUR_CLIENT_ID';
176
- const redirectUri = `http://localhost:${PORT}/callback`;
177
- const scope = 'https://www.googleapis.com/auth/generative-language';
178
-
179
- const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
180
- `client_id=${encodeURIComponent(clientId)}` +
181
- `&redirect_uri=${encodeURIComponent(redirectUri)}` +
182
- `&response_type=code` +
183
- `&scope=${encodeURIComponent(scope)}` +
184
- `&access_type=offline` +
185
- `&prompt=consent`;
186
-
187
- this.emit('log', `Opening browser for Google authentication...`);
188
- this.emit('log', `If browser doesn't open, visit: ${authUrl}`);
189
-
190
- open(authUrl).catch(() => {
191
- this.emit('log', `Could not open browser automatically. Please visit the URL above.`);
192
- });
193
- });
194
-
195
- // Timeout after 5 minutes
196
- setTimeout(() => {
197
- server.close();
198
- resolve({ success: false, message: 'Authentication timed out. Please try again.' });
199
- }, 5 * 60 * 1000);
200
- });
201
- }
202
-
203
- // Exchange authorization code for tokens
204
- private async exchangeCodeForTokens(code: string): Promise<AuthTokens> {
205
- const clientId = process.env.GOOGLE_CLIENT_ID || 'YOUR_CLIENT_ID';
206
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
207
- const redirectUri = 'http://localhost:8234/callback';
208
-
209
- const response = await fetch('https://oauth2.googleapis.com/token', {
210
- method: 'POST',
211
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
212
- body: new URLSearchParams({
213
- code,
214
- client_id: clientId,
215
- client_secret: clientSecret,
216
- redirect_uri: redirectUri,
217
- grant_type: 'authorization_code',
218
- }),
219
- });
220
-
221
- if (!response.ok) {
222
- throw new Error(`Token exchange failed: ${response.statusText}`);
223
- }
224
-
225
- const data = await response.json() as any;
226
- return {
227
- accessToken: data.access_token,
228
- refreshToken: data.refresh_token,
229
- expiresAt: Date.now() + (data.expires_in * 1000),
230
- };
231
- }
232
-
233
- // Refresh access token
234
- private async refreshAccessToken(): Promise<void> {
235
- const tokens = loadAuthTokens();
236
- if (!tokens?.refreshToken) {
237
- throw new Error('No refresh token available');
238
- }
239
-
240
- const clientId = process.env.GOOGLE_CLIENT_ID || 'YOUR_CLIENT_ID';
241
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
242
-
243
- const response = await fetch('https://oauth2.googleapis.com/token', {
244
- method: 'POST',
245
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
246
- body: new URLSearchParams({
247
- refresh_token: tokens.refreshToken,
248
- client_id: clientId,
249
- client_secret: clientSecret,
250
- grant_type: 'refresh_token',
251
- }),
252
- });
253
-
254
- if (!response.ok) {
255
- throw new Error('Failed to refresh token');
256
- }
257
-
258
- const data = await response.json() as any;
259
- saveAuthTokens({
260
- accessToken: data.access_token,
261
- refreshToken: tokens.refreshToken,
262
- expiresAt: Date.now() + (data.expires_in * 1000),
263
- });
264
- }
265
-
266
- // Get valid access token (refresh if needed)
267
- private async getAccessToken(): Promise<string> {
268
- const tokens = loadAuthTokens();
269
- if (!tokens) {
270
- throw new Error('Not authenticated. Run /gemini enable first.');
271
- }
272
-
273
- // Check if token needs refresh (with 5 min buffer)
274
- if (tokens.expiresAt < Date.now() + 5 * 60 * 1000) {
275
- await this.refreshAccessToken();
276
- const newTokens = loadAuthTokens();
277
- return newTokens!.accessToken;
278
- }
279
-
280
- return tokens.accessToken;
281
- }
282
-
283
- // Load project context
284
- loadProjectContext(cwd: string): void {
285
- const files = ['CLAUDE.md', 'README.md', 'package.json'];
286
- const contextParts: string[] = [];
287
-
288
- for (const file of files) {
289
- const filePath = path.join(cwd, file);
290
- if (fs.existsSync(filePath)) {
291
- try {
292
- const content = fs.readFileSync(filePath, 'utf-8');
293
- contextParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`);
294
- } catch {
295
- // Skip unreadable files
296
- }
297
- }
298
- }
299
-
300
- this.projectContext = contextParts.join('\n\n');
301
- }
302
-
303
- // Load custom docs
304
- loadCustomDocs(): string {
305
- const docsDir = getDocsDir();
306
- if (!fs.existsSync(docsDir)) {
307
- return '';
308
- }
309
-
310
- const docParts: string[] = [];
311
- try {
312
- const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
313
- for (const file of files.slice(0, 5)) { // Limit to 5 docs
314
- const content = fs.readFileSync(path.join(docsDir, file), 'utf-8');
315
- docParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`);
316
- }
317
- } catch {
318
- // Skip on error
319
- }
320
-
321
- return docParts.join('\n\n');
322
- }
323
-
324
- // Send message to Gemini
325
- async sendMessage(message: string): Promise<string> {
326
- const config = loadGeminiConfig();
327
- const accessToken = await this.getAccessToken();
328
-
329
- // Build context
330
- let systemContext = config.systemPrompt || '';
331
- if (config.projectContext && this.projectContext) {
332
- systemContext += '\n\nProject Context:\n' + this.projectContext;
333
- }
334
- const customDocs = this.loadCustomDocs();
335
- if (customDocs) {
336
- systemContext += '\n\nDocumentation:\n' + customDocs;
337
- }
338
-
339
- // Add to conversation history
340
- this.conversationHistory.push({ role: 'user', content: message });
341
-
342
- // Prepare request body for Gemini API
343
- const requestBody = {
344
- contents: [
345
- {
346
- role: 'user',
347
- parts: [{ text: systemContext + '\n\nUser: ' + message }]
348
- }
349
- ],
350
- generationConfig: {
351
- maxOutputTokens: 2048,
352
- temperature: 0.7,
353
- }
354
- };
355
-
356
- // Call Gemini API
357
- const response = await fetch(
358
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
359
- {
360
- method: 'POST',
361
- headers: {
362
- 'Authorization': `Bearer ${accessToken}`,
363
- 'Content-Type': 'application/json',
364
- },
365
- body: JSON.stringify(requestBody),
366
- }
367
- );
368
-
369
- if (!response.ok) {
370
- const errorText = await response.text();
371
- throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
372
- }
373
-
374
- const data = await response.json() as any;
375
- const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
376
-
377
- // Add to conversation history
378
- this.conversationHistory.push({ role: 'assistant', content: responseText });
379
-
380
- return responseText;
381
- }
382
-
383
- // Clear conversation history
384
- clearConversation(): void {
385
- this.conversationHistory = [];
386
- }
387
-
388
- // Get conversation history
389
- getConversationHistory(): Array<{ role: string; content: string }> {
390
- return [...this.conversationHistory];
391
- }
117
+ private conversationHistory: Array<{ role: string; content: string }> = []
118
+ private projectContext: string = ""
119
+
120
+ constructor() {
121
+ super()
122
+ }
123
+
124
+ // Start OAuth flow
125
+ async startOAuthFlow(): Promise<{ success: boolean; message: string }> {
126
+ return new Promise((resolve) => {
127
+ // Create local server for OAuth callback
128
+ const PORT = 8234
129
+ let server: http.Server
130
+
131
+ const handleCallback = (
132
+ req: http.IncomingMessage,
133
+ res: http.ServerResponse,
134
+ ) => {
135
+ const url = new URL(req.url || "", `http://localhost:${PORT}`)
136
+
137
+ if (url.pathname === "/callback") {
138
+ const code = url.searchParams.get("code")
139
+ const error = url.searchParams.get("error")
140
+
141
+ if (error) {
142
+ res.writeHead(200, { "Content-Type": "text/html" })
143
+ res.end(
144
+ "<html><body><h1>Authentication Failed</h1><p>You can close this window.</p></body></html>",
145
+ )
146
+ server.close()
147
+ resolve({ success: false, message: `OAuth error: ${error}` })
148
+ return
149
+ }
150
+
151
+ if (code) {
152
+ // Exchange code for tokens
153
+ this.exchangeCodeForTokens(code)
154
+ .then((tokens) => {
155
+ saveAuthTokens(tokens)
156
+ res.writeHead(200, { "Content-Type": "text/html" })
157
+ res.end(
158
+ "<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to gxdev.</p></body></html>",
159
+ )
160
+ server.close()
161
+ resolve({
162
+ success: true,
163
+ message: "Successfully authenticated with Google.",
164
+ })
165
+ })
166
+ .catch((err) => {
167
+ res.writeHead(200, { "Content-Type": "text/html" })
168
+ res.end(
169
+ "<html><body><h1>Authentication Failed</h1><p>Error exchanging code for tokens.</p></body></html>",
170
+ )
171
+ server.close()
172
+ resolve({
173
+ success: false,
174
+ message: `Token exchange error: ${err.message}`,
175
+ })
176
+ })
177
+ return
178
+ }
179
+ }
180
+
181
+ res.writeHead(404)
182
+ res.end("Not found")
183
+ }
184
+
185
+ server = http.createServer(handleCallback)
186
+
187
+ server.listen(PORT, () => {
188
+ // Construct OAuth URL
189
+ // Note: These are placeholder values - you'll need to set up actual Google Cloud credentials
190
+ const clientId = process.env.GOOGLE_CLIENT_ID || "YOUR_CLIENT_ID"
191
+ const redirectUri = `http://localhost:${PORT}/callback`
192
+ const scope = "https://www.googleapis.com/auth/generative-language"
193
+
194
+ const authUrl =
195
+ `https://accounts.google.com/o/oauth2/v2/auth?` +
196
+ `client_id=${encodeURIComponent(clientId)}` +
197
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
198
+ `&response_type=code` +
199
+ `&scope=${encodeURIComponent(scope)}` +
200
+ `&access_type=offline` +
201
+ `&prompt=consent`
202
+
203
+ this.emit("log", `Opening browser for Google authentication...`)
204
+ this.emit("log", `If browser doesn't open, visit: ${authUrl}`)
205
+
206
+ open(authUrl).catch(() => {
207
+ this.emit(
208
+ "log",
209
+ `Could not open browser automatically. Please visit the URL above.`,
210
+ )
211
+ })
212
+ })
213
+
214
+ // Timeout after 5 minutes
215
+ setTimeout(
216
+ () => {
217
+ server.close()
218
+ resolve({
219
+ success: false,
220
+ message: "Authentication timed out. Please try again.",
221
+ })
222
+ },
223
+ 5 * 60 * 1000,
224
+ )
225
+ })
226
+ }
227
+
228
+ // Exchange authorization code for tokens
229
+ private async exchangeCodeForTokens(code: string): Promise<AuthTokens> {
230
+ const clientId = process.env.GOOGLE_CLIENT_ID || "YOUR_CLIENT_ID"
231
+ const clientSecret =
232
+ process.env.GOOGLE_CLIENT_SECRET || "YOUR_CLIENT_SECRET"
233
+ const redirectUri = "http://localhost:8234/callback"
234
+
235
+ const response = await fetch("https://oauth2.googleapis.com/token", {
236
+ method: "POST",
237
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
238
+ body: new URLSearchParams({
239
+ code,
240
+ client_id: clientId,
241
+ client_secret: clientSecret,
242
+ redirect_uri: redirectUri,
243
+ grant_type: "authorization_code",
244
+ }),
245
+ })
246
+
247
+ if (!response.ok) {
248
+ throw new Error(`Token exchange failed: ${response.statusText}`)
249
+ }
250
+
251
+ const data = (await response.json()) as any
252
+ return {
253
+ accessToken: data.access_token,
254
+ refreshToken: data.refresh_token,
255
+ expiresAt: Date.now() + data.expires_in * 1000,
256
+ }
257
+ }
258
+
259
+ // Refresh access token
260
+ private async refreshAccessToken(): Promise<void> {
261
+ const tokens = loadAuthTokens()
262
+ if (!tokens?.refreshToken) {
263
+ throw new Error("No refresh token available")
264
+ }
265
+
266
+ const clientId = process.env.GOOGLE_CLIENT_ID || "YOUR_CLIENT_ID"
267
+ const clientSecret =
268
+ process.env.GOOGLE_CLIENT_SECRET || "YOUR_CLIENT_SECRET"
269
+
270
+ const response = await fetch("https://oauth2.googleapis.com/token", {
271
+ method: "POST",
272
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
273
+ body: new URLSearchParams({
274
+ refresh_token: tokens.refreshToken,
275
+ client_id: clientId,
276
+ client_secret: clientSecret,
277
+ grant_type: "refresh_token",
278
+ }),
279
+ })
280
+
281
+ if (!response.ok) {
282
+ throw new Error("Failed to refresh token")
283
+ }
284
+
285
+ const data = (await response.json()) as any
286
+ saveAuthTokens({
287
+ accessToken: data.access_token,
288
+ refreshToken: tokens.refreshToken,
289
+ expiresAt: Date.now() + data.expires_in * 1000,
290
+ })
291
+ }
292
+
293
+ // Get valid access token (refresh if needed)
294
+ private async getAccessToken(): Promise<string> {
295
+ const tokens = loadAuthTokens()
296
+ if (!tokens) {
297
+ throw new Error("Not authenticated. Run /gemini enable first.")
298
+ }
299
+
300
+ // Check if token needs refresh (with 5 min buffer)
301
+ if (tokens.expiresAt < Date.now() + 5 * 60 * 1000) {
302
+ await this.refreshAccessToken()
303
+ const newTokens = loadAuthTokens()
304
+ return newTokens!.accessToken
305
+ }
306
+
307
+ return tokens.accessToken
308
+ }
309
+
310
+ // Load project context
311
+ loadProjectContext(cwd: string): void {
312
+ const files = ["CLAUDE.md", "README.md", "package.json"]
313
+ const contextParts: string[] = []
314
+
315
+ for (const file of files) {
316
+ const filePath = path.join(cwd, file)
317
+ if (fs.existsSync(filePath)) {
318
+ try {
319
+ const content = fs.readFileSync(filePath, "utf-8")
320
+ contextParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`)
321
+ } catch {
322
+ // Skip unreadable files
323
+ }
324
+ }
325
+ }
326
+
327
+ this.projectContext = contextParts.join("\n\n")
328
+ }
329
+
330
+ // Load custom docs
331
+ loadCustomDocs(): string {
332
+ const docsDir = getDocsDir()
333
+ if (!fs.existsSync(docsDir)) {
334
+ return ""
335
+ }
336
+
337
+ const docParts: string[] = []
338
+ try {
339
+ const files = fs.readdirSync(docsDir).filter((f) => f.endsWith(".md"))
340
+ for (const file of files.slice(0, 5)) {
341
+ // Limit to 5 docs
342
+ const content = fs.readFileSync(path.join(docsDir, file), "utf-8")
343
+ docParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`)
344
+ }
345
+ } catch {
346
+ // Skip on error
347
+ }
348
+
349
+ return docParts.join("\n\n")
350
+ }
351
+
352
+ // Send message to Gemini
353
+ async sendMessage(message: string): Promise<string> {
354
+ const config = loadGeminiConfig()
355
+ const accessToken = await this.getAccessToken()
356
+
357
+ // Build context
358
+ let systemContext = config.systemPrompt || ""
359
+ if (config.projectContext && this.projectContext) {
360
+ systemContext += "\n\nProject Context:\n" + this.projectContext
361
+ }
362
+ const customDocs = this.loadCustomDocs()
363
+ if (customDocs) {
364
+ systemContext += "\n\nDocumentation:\n" + customDocs
365
+ }
366
+
367
+ // Add to conversation history
368
+ this.conversationHistory.push({ role: "user", content: message })
369
+
370
+ // Prepare request body for Gemini API
371
+ const requestBody = {
372
+ contents: [
373
+ {
374
+ role: "user",
375
+ parts: [{ text: systemContext + "\n\nUser: " + message }],
376
+ },
377
+ ],
378
+ generationConfig: {
379
+ maxOutputTokens: 2048,
380
+ temperature: 0.7,
381
+ },
382
+ }
383
+
384
+ // Call Gemini API
385
+ const response = await fetch(
386
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent",
387
+ {
388
+ method: "POST",
389
+ headers: {
390
+ Authorization: `Bearer ${accessToken}`,
391
+ "Content-Type": "application/json",
392
+ },
393
+ body: JSON.stringify(requestBody),
394
+ },
395
+ )
396
+
397
+ if (!response.ok) {
398
+ const errorText = await response.text()
399
+ throw new Error(`Gemini API error: ${response.status} - ${errorText}`)
400
+ }
401
+
402
+ const data = (await response.json()) as any
403
+ const responseText =
404
+ data.candidates?.[0]?.content?.parts?.[0]?.text ||
405
+ "No response generated."
406
+
407
+ // Add to conversation history
408
+ this.conversationHistory.push({ role: "assistant", content: responseText })
409
+
410
+ return responseText
411
+ }
412
+
413
+ // Clear conversation history
414
+ clearConversation(): void {
415
+ this.conversationHistory = []
416
+ }
417
+
418
+ // Get conversation history
419
+ getConversationHistory(): Array<{ role: string; content: string }> {
420
+ return [...this.conversationHistory]
421
+ }
392
422
  }
393
423
 
394
424
  // Singleton instance
395
- export const geminiService = new GeminiService();
425
+ export const geminiService = new GeminiService()