@qodercn-ai/qoderclicn 0.1.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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { EventEmitter } from 'node:events';
7
+ import { type WebSocket } from 'ws';
8
+ import type { NetworkLog, ConsoleLogPayload } from './types.js';
9
+ export type { NetworkLog, ConsoleLogPayload, InspectorConsoleLog, } from './types.js';
10
+ interface IncomingNetworkPayload extends Partial<NetworkLog> {
11
+ chunk?: {
12
+ index: number;
13
+ data: string;
14
+ timestamp: number;
15
+ };
16
+ }
17
+ export interface SessionInfo {
18
+ sessionId: string;
19
+ ws: WebSocket;
20
+ lastPing: number;
21
+ }
22
+ /**
23
+ * DevTools Viewer
24
+ *
25
+ * Receives logs via WebSocket from CLI sessions.
26
+ */
27
+ export declare class DevTools extends EventEmitter {
28
+ private static instance;
29
+ private logs;
30
+ private consoleLogs;
31
+ private server;
32
+ private wss;
33
+ private sessions;
34
+ private heartbeatTimer;
35
+ private port;
36
+ private static readonly DEFAULT_PORT;
37
+ private static readonly MAX_PORT_RETRIES;
38
+ private constructor();
39
+ static getInstance(): DevTools;
40
+ addInternalConsoleLog(payload: ConsoleLogPayload, sessionId?: string, timestamp?: number): void;
41
+ addInternalNetworkLog(payload: IncomingNetworkPayload, sessionId?: string, timestamp?: number): void;
42
+ getUrl(): string;
43
+ getPort(): number;
44
+ stop(): Promise<void>;
45
+ start(): Promise<string>;
46
+ private setupWebSocketServer;
47
+ private handleWebSocketMessage;
48
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import http from 'node:http';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { EventEmitter } from 'node:events';
9
+ import { WebSocketServer } from 'ws';
10
+ import { INDEX_HTML, CLIENT_JS } from './_client-assets.js';
11
+ /**
12
+ * DevTools Viewer
13
+ *
14
+ * Receives logs via WebSocket from CLI sessions.
15
+ */
16
+ export class DevTools extends EventEmitter {
17
+ static instance;
18
+ logs = [];
19
+ consoleLogs = [];
20
+ server = null;
21
+ wss = null;
22
+ sessions = new Map();
23
+ heartbeatTimer = null;
24
+ port = 25417;
25
+ static DEFAULT_PORT = 25417;
26
+ static MAX_PORT_RETRIES = 10;
27
+ constructor() {
28
+ super();
29
+ // Each SSE client adds 3 listeners; raise the limit to avoid warnings
30
+ this.setMaxListeners(50);
31
+ }
32
+ static getInstance() {
33
+ if (!DevTools.instance) {
34
+ DevTools.instance = new DevTools();
35
+ }
36
+ return DevTools.instance;
37
+ }
38
+ addInternalConsoleLog(payload, sessionId, timestamp) {
39
+ const entry = {
40
+ ...payload,
41
+ id: randomUUID(),
42
+ sessionId,
43
+ timestamp: timestamp || Date.now(),
44
+ };
45
+ this.consoleLogs.push(entry);
46
+ if (this.consoleLogs.length > 5000)
47
+ this.consoleLogs.shift();
48
+ this.emit('console-update', entry);
49
+ }
50
+ addInternalNetworkLog(payload, sessionId, timestamp) {
51
+ if (!payload.id)
52
+ return;
53
+ const existingIndex = this.logs.findIndex((l) => l.id === payload.id);
54
+ if (existingIndex > -1) {
55
+ const existing = this.logs[existingIndex];
56
+ // Handle chunk accumulation
57
+ if (payload.chunk) {
58
+ const chunks = existing.chunks || [];
59
+ chunks.push(payload.chunk);
60
+ this.logs[existingIndex] = {
61
+ ...existing,
62
+ chunks,
63
+ sessionId: sessionId || existing.sessionId,
64
+ };
65
+ }
66
+ else {
67
+ this.logs[existingIndex] = {
68
+ ...existing,
69
+ ...payload,
70
+ sessionId: sessionId || existing.sessionId,
71
+ // Drop chunks once we have the full response body — the data
72
+ // is redundant and keeping both can blow past V8's string limit
73
+ // when serializing the snapshot.
74
+ chunks: payload.response?.body ? undefined : existing.chunks,
75
+ response: payload.response
76
+ ? { ...existing.response, ...payload.response }
77
+ : existing.response,
78
+ };
79
+ }
80
+ this.emit('update', this.logs[existingIndex]);
81
+ }
82
+ else if (payload.url) {
83
+ const entry = {
84
+ ...payload,
85
+ sessionId,
86
+ timestamp: timestamp || Date.now(),
87
+ chunks: payload.chunk ? [payload.chunk] : undefined,
88
+ };
89
+ this.logs.push(entry);
90
+ if (this.logs.length > 2000)
91
+ this.logs.shift();
92
+ this.emit('update', entry);
93
+ }
94
+ }
95
+ getUrl() {
96
+ return `http://127.0.0.1:${this.port}`;
97
+ }
98
+ getPort() {
99
+ return this.port;
100
+ }
101
+ stop() {
102
+ return new Promise((resolve) => {
103
+ if (this.heartbeatTimer) {
104
+ clearInterval(this.heartbeatTimer);
105
+ this.heartbeatTimer = null;
106
+ }
107
+ if (this.wss) {
108
+ this.wss.close();
109
+ this.wss = null;
110
+ }
111
+ if (this.server) {
112
+ this.server.close(() => resolve());
113
+ this.server = null;
114
+ }
115
+ else {
116
+ resolve();
117
+ }
118
+ // Reset singleton so a fresh start() is possible
119
+ DevTools.instance = undefined;
120
+ });
121
+ }
122
+ start() {
123
+ return new Promise((resolve, reject) => {
124
+ if (this.server) {
125
+ resolve(this.getUrl());
126
+ return;
127
+ }
128
+ this.server = http.createServer((req, res) => {
129
+ // Only allow same-origin requests — the client is served from this
130
+ // server so cross-origin access is unnecessary and would let arbitrary
131
+ // websites exfiltrate logs (which may contain API keys/headers).
132
+ const origin = req.headers.origin;
133
+ if (origin) {
134
+ const allowed = `http://127.0.0.1:${this.port}`;
135
+ if (origin === allowed) {
136
+ res.setHeader('Access-Control-Allow-Origin', allowed);
137
+ }
138
+ }
139
+ // API routes
140
+ if (req.url === '/api/trigger-debugger' && req.method === 'POST') {
141
+ let body = '';
142
+ req.on('data', (chunk) => {
143
+ body += chunk;
144
+ });
145
+ req.on('end', () => {
146
+ try {
147
+ const parsed = JSON.parse(body);
148
+ if (typeof parsed !== 'object' ||
149
+ parsed === null ||
150
+ !('sessionId' in parsed) ||
151
+ typeof parsed.sessionId !== 'string') {
152
+ res.writeHead(400, { 'Content-Type': 'application/json' });
153
+ res.end(JSON.stringify({ error: 'Invalid request' }));
154
+ return;
155
+ }
156
+ const sessionId = parsed.sessionId;
157
+ const session = this.sessions.get(sessionId);
158
+ if (session) {
159
+ session.ws.send(JSON.stringify({ type: 'trigger-debugger' }));
160
+ res.writeHead(200, { 'Content-Type': 'application/json' });
161
+ res.end(JSON.stringify({ success: true }));
162
+ }
163
+ else {
164
+ res.writeHead(404, { 'Content-Type': 'application/json' });
165
+ res.end(JSON.stringify({ error: 'Session not found' }));
166
+ }
167
+ }
168
+ catch {
169
+ res.writeHead(400, { 'Content-Type': 'application/json' });
170
+ res.end(JSON.stringify({ error: 'Invalid request' }));
171
+ }
172
+ });
173
+ }
174
+ else if (req.url === '/events') {
175
+ res.writeHead(200, {
176
+ 'Content-Type': 'text/event-stream',
177
+ 'Cache-Control': 'no-cache',
178
+ Connection: 'keep-alive',
179
+ });
180
+ // Send full snapshot on connect
181
+ const snapshot = JSON.stringify({
182
+ networkLogs: this.logs,
183
+ consoleLogs: this.consoleLogs,
184
+ sessions: Array.from(this.sessions.keys()),
185
+ });
186
+ res.write(`event: snapshot\ndata: ${snapshot}\n\n`);
187
+ // Incremental updates
188
+ const onNetwork = (log) => {
189
+ res.write(`event: network\ndata: ${JSON.stringify(log)}\n\n`);
190
+ };
191
+ const onConsole = (log) => {
192
+ res.write(`event: console\ndata: ${JSON.stringify(log)}\n\n`);
193
+ };
194
+ const onSession = () => {
195
+ const sessions = Array.from(this.sessions.keys());
196
+ res.write(`event: session\ndata: ${JSON.stringify(sessions)}\n\n`);
197
+ };
198
+ this.on('update', onNetwork);
199
+ this.on('console-update', onConsole);
200
+ this.on('session-update', onSession);
201
+ req.on('close', () => {
202
+ this.off('update', onNetwork);
203
+ this.off('console-update', onConsole);
204
+ this.off('session-update', onSession);
205
+ });
206
+ }
207
+ else if (req.url === '/' || req.url === '/index.html') {
208
+ res.writeHead(200, { 'Content-Type': 'text/html' });
209
+ res.end(INDEX_HTML);
210
+ }
211
+ else if (req.url === '/assets/main.js') {
212
+ res.writeHead(200, { 'Content-Type': 'application/javascript' });
213
+ res.end(CLIENT_JS);
214
+ }
215
+ else {
216
+ res.writeHead(404);
217
+ res.end('Not Found');
218
+ }
219
+ });
220
+ this.server.on('error', (e) => {
221
+ if (typeof e === 'object' &&
222
+ e !== null &&
223
+ 'code' in e &&
224
+ e.code === 'EADDRINUSE') {
225
+ if (this.port - DevTools.DEFAULT_PORT >= DevTools.MAX_PORT_RETRIES) {
226
+ reject(new Error(`DevTools: all ports ${DevTools.DEFAULT_PORT}–${this.port} in use`));
227
+ return;
228
+ }
229
+ this.port++;
230
+ this.server?.listen(this.port, '127.0.0.1');
231
+ }
232
+ else {
233
+ reject(e instanceof Error ? e : new Error(String(e)));
234
+ }
235
+ });
236
+ this.server.listen(this.port, '127.0.0.1', () => {
237
+ this.setupWebSocketServer();
238
+ resolve(this.getUrl());
239
+ });
240
+ });
241
+ }
242
+ setupWebSocketServer() {
243
+ if (!this.server)
244
+ return;
245
+ this.wss = new WebSocketServer({ server: this.server, path: '/ws' });
246
+ this.wss.on('connection', (ws) => {
247
+ let sessionId = null;
248
+ ws.on('message', (data) => {
249
+ try {
250
+ const message = JSON.parse(data.toString());
251
+ // Handle registration first
252
+ if (message.type === 'register') {
253
+ sessionId = String(message.sessionId);
254
+ if (!sessionId)
255
+ return;
256
+ this.sessions.set(sessionId, {
257
+ sessionId,
258
+ ws,
259
+ lastPing: Date.now(),
260
+ });
261
+ // Notify session update
262
+ this.emit('session-update');
263
+ // Send registration acknowledgement
264
+ ws.send(JSON.stringify({
265
+ type: 'registered',
266
+ sessionId,
267
+ timestamp: Date.now(),
268
+ }));
269
+ }
270
+ else if (sessionId) {
271
+ this.handleWebSocketMessage(sessionId, message);
272
+ }
273
+ }
274
+ catch {
275
+ // Invalid WebSocket message
276
+ }
277
+ });
278
+ ws.on('close', () => {
279
+ if (sessionId) {
280
+ this.sessions.delete(sessionId);
281
+ this.emit('session-update');
282
+ }
283
+ });
284
+ ws.on('error', () => {
285
+ // WebSocket error — no action needed
286
+ });
287
+ });
288
+ // Heartbeat mechanism
289
+ this.heartbeatTimer = setInterval(() => {
290
+ const now = Date.now();
291
+ this.sessions.forEach((session, sessionId) => {
292
+ if (now - session.lastPing > 30000) {
293
+ session.ws.close();
294
+ this.sessions.delete(sessionId);
295
+ }
296
+ else {
297
+ // Send ping
298
+ session.ws.send(JSON.stringify({ type: 'ping', timestamp: now }));
299
+ }
300
+ });
301
+ }, 10000);
302
+ this.heartbeatTimer.unref();
303
+ }
304
+ handleWebSocketMessage(sessionId, message) {
305
+ const session = this.sessions.get(sessionId);
306
+ if (!session)
307
+ return;
308
+ switch (message['type']) {
309
+ case 'pong':
310
+ session.lastPing = Date.now();
311
+ break;
312
+ case 'console':
313
+ this.addInternalConsoleLog(message['payload'], sessionId, message['timestamp']);
314
+ break;
315
+ case 'network':
316
+ this.addInternalNetworkLog(message['payload'], sessionId, message['timestamp']);
317
+ break;
318
+ default:
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ //# sourceMappingURL=index.js.map
@@ -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,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@google/gemini-cli-devtools",
3
+ "version": "0.2.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "main": "dist/src/index.js",
7
+ "types": "dist/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/src/index.d.ts",
11
+ "default": "./dist/src/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "npm run build:client && tsc -p tsconfig.build.json",
16
+ "build:client": "node esbuild.client.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "client/index.html"
21
+ ],
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.11.24",
27
+ "react": "^19.2.0",
28
+ "react-dom": "^19.2.0",
29
+ "typescript": "^5.3.3"
30
+ },
31
+ "dependencies": {
32
+ "ws": "^8.16.0"
33
+ }
34
+ }
@@ -0,0 +1,19 @@
1
+ [modes.plan]
2
+ network = false
3
+ readonly = true
4
+ approvedTools = []
5
+ allowOverrides = true
6
+
7
+ [modes.default]
8
+ network = false
9
+ readonly = false
10
+ approvedTools = ['cat', 'ls', 'grep', 'head', 'tail', 'less', 'Get-Content', 'dir', 'type', 'findstr', 'Get-ChildItem', 'echo']
11
+ allowOverrides = true
12
+
13
+ [modes.accepting_edits]
14
+ network = false
15
+ readonly = false
16
+ approvedTools = ['sed', 'grep', 'awk', 'perl', 'cat', 'echo', 'Add-Content', 'Set-Content']
17
+ allowOverrides = true
18
+
19
+ [commands]