gemini-cli-devtools 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -4,21 +4,15 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import http from 'node:http';
7
- import { EventEmitter } from 'node:events';
8
- import { execFile } from 'node:child_process';
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import os from 'node:os';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { readFileSync } from 'node:fs';
12
9
  import { fileURLToPath } from 'node:url';
10
+ import { dirname, join } from 'node:path';
11
+ import { EventEmitter } from 'node:events';
13
12
  import { WebSocketServer } from 'ws';
14
- const VITE_DEV_PORT = 5174;
15
- // Check if running in dev mode by looking for src directory (not in npm package)
16
- const isDev = (() => {
17
- const srcDir = path.dirname(fileURLToPath(import.meta.url));
18
- // In dev: src/index.ts -> srcDir is "src"
19
- // In prod: dist/src/index.js -> srcDir is "dist/src"
20
- return srcDir.endsWith('/src') && !srcDir.includes('/dist/');
21
- })();
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const indexHtml = readFileSync(join(__dirname, '../../client/index.html'), 'utf-8');
15
+ const mainJs = readFileSync(join(__dirname, '../client/main.js'), 'utf-8');
22
16
  /**
23
17
  * DevTools Viewer
24
18
  *
@@ -31,6 +25,7 @@ export class DevTools extends EventEmitter {
31
25
  server = null;
32
26
  wss = null;
33
27
  sessions = new Map();
28
+ heartbeatTimer = null;
34
29
  port = 25417;
35
30
  constructor() {
36
31
  super();
@@ -41,22 +36,17 @@ export class DevTools extends EventEmitter {
41
36
  }
42
37
  return DevTools.instance;
43
38
  }
44
- getNetworkLogs() {
45
- return this.logs;
46
- }
47
- getConsoleLogs() {
48
- return this.consoleLogs;
49
- }
50
39
  addInternalConsoleLog(payload, sessionId, timestamp) {
51
- this.consoleLogs.push({
40
+ const entry = {
52
41
  ...payload,
53
- id: Math.random().toString(36).substring(7),
42
+ id: randomUUID(),
54
43
  sessionId,
55
44
  timestamp: timestamp || Date.now(),
56
- });
45
+ };
46
+ this.consoleLogs.push(entry);
57
47
  if (this.consoleLogs.length > 5000)
58
48
  this.consoleLogs.shift();
59
- this.emit('console-update');
49
+ this.emit('console-update', entry);
60
50
  }
61
51
  addInternalNetworkLog(payload, sessionId, timestamp) {
62
52
  if (!payload.id)
@@ -68,7 +58,6 @@ export class DevTools extends EventEmitter {
68
58
  if (payload.chunk) {
69
59
  const chunks = existing.chunks || [];
70
60
  chunks.push(payload.chunk);
71
- console.log(`[DevTools] Received chunk ${payload.chunk.index} for request ${payload.id}, total chunks: ${chunks.length}`);
72
61
  this.logs[existingIndex] = {
73
62
  ...existing,
74
63
  chunks,
@@ -86,31 +75,47 @@ export class DevTools extends EventEmitter {
86
75
  : existing.response,
87
76
  };
88
77
  }
78
+ this.emit('update', this.logs[existingIndex]);
89
79
  }
90
80
  else if (payload.url) {
91
- this.logs.push({
81
+ const entry = {
92
82
  ...payload,
93
83
  sessionId,
94
84
  timestamp: timestamp || Date.now(),
95
85
  chunks: payload.chunk ? [payload.chunk] : undefined,
96
- });
86
+ };
87
+ this.logs.push(entry);
97
88
  if (this.logs.length > 2000)
98
89
  this.logs.shift();
90
+ this.emit('update', entry);
99
91
  }
100
- this.emit('update');
101
92
  }
102
93
  getUrl() {
103
94
  return `http://127.0.0.1:${this.port}`;
104
95
  }
105
- getClientPath() {
106
- const srcDir = path.dirname(fileURLToPath(import.meta.url));
107
- // When running with tsx from src/, look for dist/client
108
- // When running compiled from dist/src/, look for ../client
109
- const distClient = path.join(srcDir, '../dist/client');
110
- if (fs.existsSync(distClient)) {
111
- return distClient;
112
- }
113
- return path.join(srcDir, '../client');
96
+ getPort() {
97
+ return this.port;
98
+ }
99
+ stop() {
100
+ return new Promise((resolve) => {
101
+ if (this.heartbeatTimer) {
102
+ clearInterval(this.heartbeatTimer);
103
+ this.heartbeatTimer = null;
104
+ }
105
+ if (this.wss) {
106
+ this.wss.close();
107
+ this.wss = null;
108
+ }
109
+ if (this.server) {
110
+ this.server.close(() => resolve());
111
+ this.server = null;
112
+ }
113
+ else {
114
+ resolve();
115
+ }
116
+ // Reset singleton so a fresh start() is possible
117
+ DevTools.instance = undefined;
118
+ });
114
119
  }
115
120
  start() {
116
121
  return new Promise((resolve) => {
@@ -118,100 +123,53 @@ export class DevTools extends EventEmitter {
118
123
  resolve(this.getUrl());
119
124
  return;
120
125
  }
121
- const clientPath = this.getClientPath();
122
126
  this.server = http.createServer((req, res) => {
123
127
  res.setHeader('Access-Control-Allow-Origin', '*');
124
128
  // API routes
125
- if (req.url === '/logs') {
126
- res.writeHead(200, { 'Content-Type': 'application/json' });
127
- res.end(JSON.stringify(this.logs));
128
- }
129
- else if (req.url === '/console-logs') {
130
- res.writeHead(200, { 'Content-Type': 'application/json' });
131
- res.end(JSON.stringify(this.consoleLogs));
132
- }
133
- else if (req.url === '/sessions') {
134
- res.writeHead(200, { 'Content-Type': 'application/json' });
135
- const sessionIds = Array.from(this.sessions.keys());
136
- res.end(JSON.stringify(sessionIds));
137
- }
138
- else if (req.url === '/events') {
129
+ if (req.url === '/events') {
139
130
  res.writeHead(200, {
140
131
  'Content-Type': 'text/event-stream',
141
132
  'Cache-Control': 'no-cache',
142
133
  Connection: 'keep-alive',
143
134
  });
144
- const l1 = () => res.write(`event: update\ndata: \n\n`);
145
- const l2 = () => res.write(`event: console-update\ndata: \n\n`);
146
- const l3 = () => res.write(`event: session-update\ndata: \n\n`);
147
- this.on('update', l1);
148
- this.on('console-update', l2);
149
- this.on('session-update', l3);
135
+ // Send full snapshot on connect
136
+ const snapshot = JSON.stringify({
137
+ networkLogs: this.logs,
138
+ consoleLogs: this.consoleLogs,
139
+ sessions: Array.from(this.sessions.keys()),
140
+ });
141
+ res.write(`event: snapshot\ndata: ${snapshot}\n\n`);
142
+ // Incremental updates
143
+ const onNetwork = (log) => {
144
+ res.write(`event: network\ndata: ${JSON.stringify(log)}\n\n`);
145
+ };
146
+ const onConsole = (log) => {
147
+ res.write(`event: console\ndata: ${JSON.stringify(log)}\n\n`);
148
+ };
149
+ const onSession = () => {
150
+ const sessions = Array.from(this.sessions.keys());
151
+ res.write(`event: session\ndata: ${JSON.stringify(sessions)}\n\n`);
152
+ };
153
+ this.on('update', onNetwork);
154
+ this.on('console-update', onConsole);
155
+ this.on('session-update', onSession);
150
156
  req.on('close', () => {
151
- this.off('update', l1);
152
- this.off('console-update', l2);
153
- this.off('session-update', l3);
157
+ this.off('update', onNetwork);
158
+ this.off('console-update', onConsole);
159
+ this.off('session-update', onSession);
154
160
  });
155
161
  }
156
- else if (req.method === 'POST' && req.url === '/ingest') {
157
- let body = '';
158
- req.on('data', (chunk) => (body += chunk));
159
- req.on('end', () => {
160
- try {
161
- const entry = JSON.parse(body);
162
- if (entry.type === 'console') {
163
- this.addInternalConsoleLog(entry.payload, entry.sessionId, entry.timestamp);
164
- }
165
- else if (entry.type === 'network') {
166
- this.addInternalNetworkLog(entry.payload, entry.sessionId, entry.timestamp);
167
- }
168
- res.writeHead(200);
169
- res.end('OK');
170
- }
171
- catch {
172
- res.writeHead(400);
173
- res.end('Invalid JSON');
174
- }
175
- });
162
+ else if (req.url === '/' || req.url === '/index.html') {
163
+ res.writeHead(200, { 'Content-Type': 'text/html' });
164
+ res.end(indexHtml);
176
165
  }
177
- else if (req.method === 'POST' && req.url === '/api/to-textproto') {
178
- let body = '';
179
- req.on('data', (chunk) => (body += chunk));
180
- req.on('end', () => {
181
- const tmpIn = path.join(os.tmpdir(), `req-${Date.now()}.json`);
182
- fs.writeFileSync(tmpIn, body);
183
- const script = `
184
- import sys
185
- sys.path.insert(0, '/tmp/proto_gen')
186
- from google.cloud.aiplatform.v1 import prediction_service_pb2
187
- from google.protobuf.json_format import Parse
188
- from google.protobuf import text_format
189
- import json
190
-
191
- with open(sys.argv[1]) as f:
192
- msg = Parse(f.read(), prediction_service_pb2.GenerateContentRequest(), ignore_unknown_fields=True)
193
- print(text_format.MessageToString(msg, as_utf8=True), end='')
194
- `;
195
- execFile('/tmp/proto_venv/bin/python3', ['-c', script, tmpIn], { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
196
- fs.unlinkSync(tmpIn);
197
- if (err) {
198
- res.writeHead(500, { 'Content-Type': 'text/plain' });
199
- res.end(stderr || err.message);
200
- }
201
- else {
202
- res.writeHead(200, { 'Content-Type': 'text/plain' });
203
- res.end(stdout);
204
- }
205
- });
206
- });
207
- }
208
- else if (isDev) {
209
- // Dev mode: proxy to Vite
210
- this.proxyToVite(req, res);
166
+ else if (req.url === '/assets/main.js') {
167
+ res.writeHead(200, { 'Content-Type': 'application/javascript' });
168
+ res.end(mainJs);
211
169
  }
212
170
  else {
213
- // Production: serve static files
214
- this.serveStatic(req, res, clientPath);
171
+ res.writeHead(404);
172
+ res.end('Not Found');
215
173
  }
216
174
  });
217
175
  this.server.on('error', (e) => {
@@ -246,7 +204,6 @@ print(text_format.MessageToString(msg, as_utf8=True), end='')
246
204
  ws,
247
205
  lastPing: Date.now(),
248
206
  });
249
- console.log(`📡 WebSocket registered: ${sessionId}`);
250
207
  // Notify session update
251
208
  this.emit('session-update');
252
209
  // Send registration acknowledgement
@@ -260,28 +217,25 @@ print(text_format.MessageToString(msg, as_utf8=True), end='')
260
217
  this.handleWebSocketMessage(sessionId, message);
261
218
  }
262
219
  }
263
- catch (err) {
264
- console.error('Invalid WebSocket message:', err);
220
+ catch {
221
+ // Invalid WebSocket message
265
222
  }
266
223
  });
267
224
  ws.on('close', () => {
268
225
  if (sessionId) {
269
- console.log(`📡 WebSocket disconnected: ${sessionId}`);
270
226
  this.sessions.delete(sessionId);
271
- this.emit('session-disconnected', sessionId);
272
227
  this.emit('session-update');
273
228
  }
274
229
  });
275
- ws.on('error', (err) => {
276
- console.error(`WebSocket error:`, err);
230
+ ws.on('error', () => {
231
+ // WebSocket error — no action needed
277
232
  });
278
233
  });
279
234
  // Heartbeat mechanism
280
- setInterval(() => {
235
+ this.heartbeatTimer = setInterval(() => {
281
236
  const now = Date.now();
282
237
  this.sessions.forEach((session, sessionId) => {
283
238
  if (now - session.lastPing > 30000) {
284
- console.log(`⚠️ Session ${sessionId} timeout, closing...`);
285
239
  session.ws.close();
286
240
  this.sessions.delete(sessionId);
287
241
  }
@@ -307,47 +261,7 @@ print(text_format.MessageToString(msg, as_utf8=True), end='')
307
261
  this.addInternalNetworkLog(message.payload, sessionId, message.timestamp);
308
262
  break;
309
263
  default:
310
- console.warn(`Unknown message type: ${message.type}`);
264
+ break;
311
265
  }
312
266
  }
313
- proxyToVite(req, res) {
314
- const proxyReq = http.request({
315
- hostname: '127.0.0.1',
316
- port: VITE_DEV_PORT,
317
- path: req.url,
318
- method: req.method,
319
- headers: req.headers,
320
- }, (proxyRes) => {
321
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
322
- proxyRes.pipe(res);
323
- });
324
- proxyReq.on('error', () => {
325
- res.writeHead(502);
326
- res.end('Vite dev server not ready');
327
- });
328
- req.pipe(proxyReq);
329
- }
330
- serveStatic(req, res, clientPath) {
331
- const url = req.url === '/' ? '/index.html' : req.url || '/index.html';
332
- const filePath = path.join(clientPath, url);
333
- fs.readFile(filePath, (err, data) => {
334
- if (err) {
335
- res.writeHead(404);
336
- res.end('Not Found');
337
- return;
338
- }
339
- const ext = path.extname(filePath);
340
- const contentTypes = {
341
- '.html': 'text/html',
342
- '.js': 'application/javascript',
343
- '.css': 'text/css',
344
- '.svg': 'image/svg+xml',
345
- '.json': 'application/json',
346
- };
347
- res.writeHead(200, {
348
- 'Content-Type': contentTypes[ext] || 'text/plain',
349
- });
350
- res.end(data);
351
- });
352
- }
353
267
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export interface NetworkLog {
7
+ id: string;
8
+ sessionId?: string;
9
+ timestamp: number;
10
+ method: string;
11
+ url: string;
12
+ headers: Record<string, string | string[] | undefined>;
13
+ body?: string;
14
+ pending?: boolean;
15
+ chunks?: Array<{
16
+ index: number;
17
+ data: string;
18
+ timestamp: number;
19
+ }>;
20
+ response?: {
21
+ status: number;
22
+ headers: Record<string, string | string[] | undefined>;
23
+ body?: string;
24
+ durationMs: number;
25
+ };
26
+ error?: string;
27
+ }
28
+ export interface ConsoleLogPayload {
29
+ type: 'log' | 'warn' | 'error' | 'debug' | 'info';
30
+ content: string;
31
+ }
32
+ export interface InspectorConsoleLog extends ConsoleLogPayload {
33
+ id: string;
34
+ sessionId?: string;
35
+ timestamp: number;
36
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "gemini-cli-devtools",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
7
- "bin": {
8
- "gemini-cli-devtools": "dist/src/bin.js"
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/src/index.d.ts",
10
+ "default": "./dist/src/index.js"
11
+ }
9
12
  },
10
13
  "scripts": {
11
- "dev": "concurrently -k \"vite --port 5174\" \"tsx watch src/bin.ts\"",
12
- "build": "npm run build:client && tsc --outDir dist/src --module ESNext --target ESNext --moduleResolution bundler src/index.ts src/bin.ts",
13
- "build:client": "vite build"
14
+ "build": "npm run build:client && tsc --outDir dist/src --declaration --module ESNext --target ESNext --moduleResolution bundler src/index.ts",
15
+ "build:client": "node esbuild.client.js"
14
16
  },
15
17
  "files": [
16
18
  "dist"
@@ -19,19 +21,16 @@
19
21
  "node": ">=20"
20
22
  },
21
23
  "dependencies": {
22
- "react": "^18.2.0",
23
- "react-dom": "^18.2.0",
24
24
  "ws": "^8.16.0"
25
25
  },
26
26
  "devDependencies": {
27
+ "@types/node": "^20.11.24",
27
28
  "@types/react": "^18.2.0",
28
29
  "@types/react-dom": "^18.2.0",
29
30
  "@types/ws": "^8.5.10",
30
- "@vitejs/plugin-react": "^4.2.0",
31
- "typescript": "^5.3.3",
32
- "vite": "^5.0.0",
33
- "@types/node": "^20.11.24",
34
- "concurrently": "^8.2.2",
35
- "tsx": "^4.7.0"
31
+ "esbuild": "^0.24.0",
32
+ "react": "^18.2.0",
33
+ "react-dom": "^18.2.0",
34
+ "typescript": "^5.3.3"
36
35
  }
37
36
  }