@qoder-ai/qodercli 0.2.2-beta.9 → 0.2.3

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.
@@ -1,48 +0,0 @@
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
- }
@@ -1,323 +0,0 @@
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
@@ -1,36 +0,0 @@
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
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- export {};
7
- //# sourceMappingURL=types.js.map
@@ -1,34 +0,0 @@
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
- }
package/bin/run.js DELETED
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env node
2
- const { spawnSync } = require('child_process');
3
- const path = require('path');
4
- const fs = require('fs');
5
-
6
- const platform = process.platform;
7
- const arch = process.arch;
8
- const target = `${platform}-${arch}`;
9
- const pkgName = `@qoder-ai/qodercli-${target}`;
10
- const subPkgDir = pkgName.split('/')[1]; // e.g. "qodercli-win32-x64"
11
-
12
- // 查找平台子包中的二进制
13
- const BINARY_NAME = platform === 'win32' ? 'qodercli.exe' : 'qodercli';
14
-
15
- // __dirname = .../node_modules/@qoder-ai/qodercli/bin/
16
- // npm global install 结构:
17
- // prefix/node_modules/@qoder-ai/qodercli/ (主包)
18
- // prefix/node_modules/@qoder-ai/qodercli-win32-x64/ (子包, 同级)
19
- // npm local install 结构:
20
- // project/node_modules/@qoder-ai/qodercli/
21
- // project/node_modules/@qoder-ai/qodercli-win32-x64/ (hoisted)
22
- // 或 project/node_modules/@qoder-ai/qodercli/node_modules/@qoder-ai/qodercli-win32-x64/ (nested)
23
-
24
- const candidates = [
25
- // 同级 scope 目录下 (global + hoisted local)
26
- path.join(__dirname, '..', '..', subPkgDir, 'bin', BINARY_NAME),
27
- // nested in main package's node_modules
28
- path.join(__dirname, '..', 'node_modules', pkgName, 'bin', BINARY_NAME),
29
- // 3 levels up (some pnpm/yarn structures)
30
- path.join(__dirname, '..', '..', '..', 'node_modules', pkgName, 'bin', BINARY_NAME),
31
- // npm global on Windows may place under node_modules directly
32
- path.join(__dirname, '..', '..', '..', subPkgDir, 'bin', BINARY_NAME),
33
- // resolve from require (most reliable for complex layouts)
34
- ];
35
-
36
- // Also try require.resolve to find the sub-package
37
- try {
38
- const subPkgJson = require.resolve(`${pkgName}/package.json`, { paths: [path.join(__dirname, '..')] });
39
- const subPkgBin = path.join(path.dirname(subPkgJson), 'bin', BINARY_NAME);
40
- candidates.push(subPkgBin);
41
- } catch {}
42
-
43
- let binPath = null;
44
- for (const p of candidates) {
45
- try {
46
- if (fs.existsSync(p)) {
47
- binPath = p;
48
- break;
49
- }
50
- } catch {}
51
- }
52
-
53
- if (!binPath) {
54
- console.error(`Error: Cannot find qodercli binary for your platform (${target})`);
55
- console.error('');
56
- console.error(`The platform-specific package "${pkgName}" does not appear to be installed.`);
57
- console.error('');
58
- console.error('__dirname:', __dirname);
59
- console.error('Searched paths:');
60
- candidates.forEach(p => console.error(` - ${p} [${fs.existsSync(p) ? 'EXISTS' : 'NOT FOUND'}]`));
61
- console.error('');
62
- // List what's actually in the scope directory
63
- const scopeDir = path.join(__dirname, '..', '..');
64
- try {
65
- console.error('Contents of scope dir (' + scopeDir + '):');
66
- fs.readdirSync(scopeDir).forEach(f => console.error(' ' + f));
67
- } catch (e) { console.error(' (cannot read: ' + e.message + ')'); }
68
- console.error('');
69
- console.error('Supported platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64');
70
- console.error('Try: npm install -g @qoder-ai/qodercli --include=optional');
71
- process.exit(1);
72
- }
73
-
74
- // Use spawnSync for reliable cross-platform behavior
75
- const result = spawnSync(binPath, process.argv.slice(2), {
76
- stdio: 'inherit',
77
- windowsHide: false,
78
- });
79
-
80
- if (result.error) {
81
- console.error('Failed to execute qodercli:', result.error.message);
82
- process.exit(1);
83
- }
84
-
85
- process.exit(result.status !== null ? result.status : 1);