@qiaolei81/copilot-session-viewer 0.1.3 → 0.1.5
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/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/app.js +5 -5
- package/src/config/index.js +2 -2
- package/src/middleware/rateLimiting.js +28 -8
- package/src/services/insightService.js +28 -42
- package/src/services/sessionRepository.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.5] - 2026-02-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- EPIPE crash when copilot process exits before events file is fully piped to stdin
|
|
12
|
+
- Server no longer crashes with uncaught exception during concurrent insight generation
|
|
13
|
+
|
|
14
|
+
## [0.1.4] - 2026-02-16
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Rate limiting configuration for insight operations - resolved 429 "Too Many Requests" errors
|
|
18
|
+
- Removed rate limiting from insight status checks (GET requests) - status checks are now unlimited
|
|
19
|
+
- Improved rate limiting differentiation: strict for generation (POST), lenient for access (DELETE)
|
|
20
|
+
- Fixed "Age: NaNs" timestamp display issue in insight generation progress
|
|
21
|
+
- Added missing `ageMs` calculation to backend insight service responses
|
|
22
|
+
- ESLint configuration migration from deprecated `.eslintignore` to modern flat config
|
|
23
|
+
- Minimal `.npmignore` configuration for optimized package publishing (82% size reduction)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Insight output file renamed from `insight-report.md` to `copilot-insight.md`
|
|
27
|
+
- Insight prompt rewritten to enforce ≤500 character output (down from ~2000 words)
|
|
28
|
+
- Insight now focuses on three essentials: health score, top issue, key recommendation
|
|
29
|
+
- Insight generation rate limiting: 3 requests per 5 minutes (more user-friendly window)
|
|
30
|
+
- Insight access operations: 50 requests per minute (very lenient for status checks)
|
|
31
|
+
- Package size optimized from 298kB to 52kB for npm publishing
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
- Deprecated `.eslintignore` file in favor of `eslint.config.mjs` ignores property
|
|
35
|
+
- Verbose `.npmignore` entries - simplified to essential exclusions only
|
|
36
|
+
|
|
8
37
|
## [0.1.3] - 2026-02-16
|
|
9
38
|
|
|
10
39
|
### Added
|
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ npx @qiaolei81/copilot-session-viewer
|
|
|
111
111
|
│ Data Layer (~/.copilot/session-state/) │
|
|
112
112
|
│ • events.jsonl (event streams) │
|
|
113
113
|
│ • workspace.yaml (metadata) │
|
|
114
|
-
│ • insight
|
|
114
|
+
│ • copilot-insight.md (AI analysis) │
|
|
115
115
|
└─────────────────────────────────────────────────┘
|
|
116
116
|
```
|
|
117
117
|
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -7,7 +7,7 @@ const helmet = require('helmet');
|
|
|
7
7
|
const config = require('./config');
|
|
8
8
|
|
|
9
9
|
// Middleware
|
|
10
|
-
const { globalLimiter,
|
|
10
|
+
const { globalLimiter, insightGenerationLimiter, insightAccessLimiter, uploadLimiter } = require('./middleware/rateLimiting');
|
|
11
11
|
const { requestTimeout, developmentCors, errorHandler, notFoundHandler } = require('./middleware/common');
|
|
12
12
|
|
|
13
13
|
// Controllers
|
|
@@ -75,10 +75,10 @@ function createApp(options = {}) {
|
|
|
75
75
|
uploadController.importSession.bind(uploadController)
|
|
76
76
|
);
|
|
77
77
|
|
|
78
|
-
// Insight routes with rate limiting
|
|
79
|
-
app.post('/session/:id/insight',
|
|
80
|
-
app.get('/session/:id/insight',
|
|
81
|
-
app.delete('/session/:id/insight',
|
|
78
|
+
// Insight routes with appropriate rate limiting
|
|
79
|
+
app.post('/session/:id/insight', insightGenerationLimiter, insightController.generateInsight.bind(insightController));
|
|
80
|
+
app.get('/session/:id/insight', insightController.getInsightStatus.bind(insightController)); // Remove rate limiting for GET
|
|
81
|
+
app.delete('/session/:id/insight', insightAccessLimiter, insightController.deleteInsight.bind(insightController));
|
|
82
82
|
|
|
83
83
|
// Upload rate limiting
|
|
84
84
|
app.use('/session/import', uploadLimiter);
|
package/src/config/index.js
CHANGED
|
@@ -20,8 +20,8 @@ module.exports = {
|
|
|
20
20
|
// Session Repository
|
|
21
21
|
SESSION_CACHE_TTL_MS: 30 * 1000, // 30 seconds
|
|
22
22
|
|
|
23
|
-
// Request Limits
|
|
23
|
+
// Request Limits - More lenient for better UX
|
|
24
24
|
RATE_LIMIT_WINDOW_MS: 15 * 60 * 1000, // 15 minutes
|
|
25
|
-
RATE_LIMIT_MAX_REQUESTS: 5
|
|
25
|
+
RATE_LIMIT_MAX_REQUESTS: 15, // Increased from 5 to 15 for better UX
|
|
26
26
|
REQUEST_TIMEOUT_MS: 30 * 1000 // 30 seconds
|
|
27
27
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const rateLimit = require('express-rate-limit');
|
|
2
|
-
const config = require('../config');
|
|
3
2
|
|
|
4
3
|
// Global rate limiting for all routes
|
|
5
4
|
const globalLimiter = rateLimit({
|
|
@@ -8,14 +7,34 @@ const globalLimiter = rateLimit({
|
|
|
8
7
|
message: { error: 'Too many requests. Please try again later.' },
|
|
9
8
|
standardHeaders: true,
|
|
10
9
|
legacyHeaders: false,
|
|
11
|
-
skip: (req) =>
|
|
10
|
+
skip: (req) => {
|
|
11
|
+
// Skip static files
|
|
12
|
+
if (req.path.startsWith('/public')) return true;
|
|
13
|
+
|
|
14
|
+
// Skip insight status checks (GET requests)
|
|
15
|
+
if (req.method === 'GET' && req.path.includes('/insight')) return true;
|
|
16
|
+
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Rate limiting for insight generation (stricter for POST)
|
|
22
|
+
const insightGenerationLimiter = rateLimit({
|
|
23
|
+
windowMs: 5 * 60 * 1000, // 5 minutes (shorter window)
|
|
24
|
+
max: 3, // 3 generations per 5-minute window (expensive operations)
|
|
25
|
+
message: {
|
|
26
|
+
error: 'Too many insight generation requests. Please wait 5 minutes before generating another insight.',
|
|
27
|
+
retryAfter: 5 * 60 // 5 minutes in seconds
|
|
28
|
+
},
|
|
29
|
+
standardHeaders: true,
|
|
30
|
+
legacyHeaders: false
|
|
12
31
|
});
|
|
13
32
|
|
|
14
|
-
// Rate limiting for insight
|
|
15
|
-
const
|
|
16
|
-
windowMs:
|
|
17
|
-
max:
|
|
18
|
-
message: { error: 'Too many insight
|
|
33
|
+
// Rate limiting for insight status/retrieval (very lenient for GET/DELETE)
|
|
34
|
+
const insightAccessLimiter = rateLimit({
|
|
35
|
+
windowMs: 1 * 60 * 1000, // 1 minute (shorter window)
|
|
36
|
+
max: 50, // 50 requests per minute (very lenient)
|
|
37
|
+
message: { error: 'Too many insight requests. Please try again in a minute.' },
|
|
19
38
|
standardHeaders: true,
|
|
20
39
|
legacyHeaders: false
|
|
21
40
|
});
|
|
@@ -31,6 +50,7 @@ const uploadLimiter = rateLimit({
|
|
|
31
50
|
|
|
32
51
|
module.exports = {
|
|
33
52
|
globalLimiter,
|
|
34
|
-
|
|
53
|
+
insightGenerationLimiter,
|
|
54
|
+
insightAccessLimiter,
|
|
35
55
|
uploadLimiter
|
|
36
56
|
};
|
|
@@ -24,8 +24,8 @@ class InsightService {
|
|
|
24
24
|
*/
|
|
25
25
|
async generateInsight(sessionId, forceRegenerate = false) {
|
|
26
26
|
const sessionPath = path.join(this.sessionDir, sessionId);
|
|
27
|
-
const insightFile = path.join(sessionPath, 'insight
|
|
28
|
-
const lockFile = path.join(sessionPath, 'insight
|
|
27
|
+
const insightFile = path.join(sessionPath, 'copilot-insight.md');
|
|
28
|
+
const lockFile = path.join(sessionPath, 'copilot-insight.md.lock');
|
|
29
29
|
const eventsFile = path.join(sessionPath, 'events.jsonl');
|
|
30
30
|
|
|
31
31
|
// Check if complete insight exists
|
|
@@ -65,7 +65,8 @@ class InsightService {
|
|
|
65
65
|
status: 'generating',
|
|
66
66
|
report: '# Generating Copilot Insight...\n\nAnother request is currently generating this insight. Please wait.',
|
|
67
67
|
startedAt: lockStats.birthtime,
|
|
68
|
-
lastUpdate: lockStats.mtime
|
|
68
|
+
lastUpdate: lockStats.mtime,
|
|
69
|
+
ageMs: Date.now() - lockStats.birthtime.getTime()
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -123,7 +124,7 @@ class InsightService {
|
|
|
123
124
|
await fs.mkdir(tmpDir, { recursive: true });
|
|
124
125
|
|
|
125
126
|
const prompt = this._buildPrompt();
|
|
126
|
-
const outputFile = path.join(sessionPath, 'insight
|
|
127
|
+
const outputFile = path.join(sessionPath, 'copilot-insight.md.tmp');
|
|
127
128
|
|
|
128
129
|
// Spawn copilot directly (no shell)
|
|
129
130
|
const copilotPath = 'copilot';
|
|
@@ -141,6 +142,14 @@ class InsightService {
|
|
|
141
142
|
|
|
142
143
|
// Pipe events file to stdin
|
|
143
144
|
const eventsStream = fsSync.createReadStream(eventsFile);
|
|
145
|
+
// Handle EPIPE: if copilot exits before stdin is fully written, suppress the error
|
|
146
|
+
copilotProcess.stdin.on('error', (err) => {
|
|
147
|
+
if (err.code === 'EPIPE') {
|
|
148
|
+
eventsStream.destroy();
|
|
149
|
+
} else {
|
|
150
|
+
console.error('❌ stdin error:', err);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
144
153
|
eventsStream.pipe(copilotProcess.stdin);
|
|
145
154
|
|
|
146
155
|
// Capture output
|
|
@@ -201,42 +210,18 @@ class InsightService {
|
|
|
201
210
|
* @private
|
|
202
211
|
*/
|
|
203
212
|
_buildPrompt() {
|
|
204
|
-
return `Analyze this GitHub Copilot CLI session data (JSONL format
|
|
205
|
-
|
|
206
|
-
CRITICAL: Output ONLY the analysis report. Do NOT include thinking blocks, reasoning steps, or meta-commentary about your analysis process. Go straight to insights.
|
|
207
|
-
|
|
208
|
-
Focus on:
|
|
209
|
-
1. **Session Health Score** (0-100): Calculate based on success rate, completion rate, and performance
|
|
210
|
-
- Red flags: error rate >50%, incomplete sub-agents, timeout patterns
|
|
211
|
-
|
|
212
|
-
2. **Critical Issues** (if any):
|
|
213
|
-
- What went wrong and why (root cause analysis)
|
|
214
|
-
- Impact on user workflow
|
|
215
|
-
- Specific failing patterns (e.g., "all 'create' calls missing file_text parameter")
|
|
213
|
+
return `Analyze this GitHub Copilot CLI session data (JSONL format) and produce a concise insight.
|
|
216
214
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- Tool execution delays vs LLM thinking time
|
|
215
|
+
CRITICAL CONSTRAINTS:
|
|
216
|
+
- Your ENTIRE output must be UNDER 500 characters (not words — characters, including spaces and punctuation).
|
|
217
|
+
- Output ONLY the analysis. No thinking blocks, no meta-commentary.
|
|
221
218
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
219
|
+
Include:
|
|
220
|
+
1. **Health Score** (0-100) based on success/error rates
|
|
221
|
+
2. **Top Issue**: The single most impactful problem with root cause
|
|
222
|
+
3. **Key Recommendation**: One specific, actionable improvement
|
|
226
223
|
|
|
227
|
-
|
|
228
|
-
- Most/least used tools
|
|
229
|
-
- Error patterns per tool type
|
|
230
|
-
- Unused but potentially helpful tools
|
|
231
|
-
|
|
232
|
-
6. **Workflow Recommendations**:
|
|
233
|
-
- Actionable improvements (specific, not generic)
|
|
234
|
-
- Configuration tuning suggestions
|
|
235
|
-
- Anti-patterns detected
|
|
236
|
-
|
|
237
|
-
Use data-driven language with specific numbers. Be critical, not descriptive. Focus on "why" and "what to do" rather than "what happened".
|
|
238
|
-
|
|
239
|
-
Output in clean Markdown with ## headers. Keep it concise but insightful (<2000 words).`;
|
|
224
|
+
Be data-driven with specific numbers. No filler or generic advice.`;
|
|
240
225
|
}
|
|
241
226
|
|
|
242
227
|
/**
|
|
@@ -261,8 +246,8 @@ Output in clean Markdown with ## headers. Keep it concise but insightful (<2000
|
|
|
261
246
|
*/
|
|
262
247
|
async getInsightStatus(sessionId) {
|
|
263
248
|
const sessionPath = path.join(this.sessionDir, sessionId);
|
|
264
|
-
const insightFile = path.join(sessionPath, 'insight
|
|
265
|
-
const lockFile = path.join(sessionPath, 'insight
|
|
249
|
+
const insightFile = path.join(sessionPath, 'copilot-insight.md');
|
|
250
|
+
const lockFile = path.join(sessionPath, 'copilot-insight.md.lock');
|
|
266
251
|
|
|
267
252
|
try {
|
|
268
253
|
const report = await fs.readFile(insightFile, 'utf-8');
|
|
@@ -280,7 +265,8 @@ Output in clean Markdown with ## headers. Keep it concise but insightful (<2000
|
|
|
280
265
|
return {
|
|
281
266
|
status: 'generating',
|
|
282
267
|
startedAt: stats.birthtime,
|
|
283
|
-
lastUpdate: stats.mtime
|
|
268
|
+
lastUpdate: stats.mtime,
|
|
269
|
+
ageMs: Date.now() - stats.birthtime.getTime()
|
|
284
270
|
};
|
|
285
271
|
} catch (_lockErr) {
|
|
286
272
|
return { status: 'not_started' };
|
|
@@ -293,8 +279,8 @@ Output in clean Markdown with ## headers. Keep it concise but insightful (<2000
|
|
|
293
279
|
*/
|
|
294
280
|
async deleteInsight(sessionId) {
|
|
295
281
|
const sessionPath = path.join(this.sessionDir, sessionId);
|
|
296
|
-
const insightFile = path.join(sessionPath, 'insight
|
|
297
|
-
|
|
282
|
+
const insightFile = path.join(sessionPath, 'copilot-insight.md');
|
|
283
|
+
|
|
298
284
|
try {
|
|
299
285
|
await fs.unlink(insightFile);
|
|
300
286
|
return { success: true };
|
|
@@ -85,7 +85,7 @@ class SessionRepository {
|
|
|
85
85
|
const workspaceFile = path.join(fullPath, 'workspace.yaml');
|
|
86
86
|
const eventsFile = path.join(fullPath, 'events.jsonl');
|
|
87
87
|
const importedMarkerFile = path.join(fullPath, '.imported');
|
|
88
|
-
const insightReportFile = path.join(fullPath, 'insight
|
|
88
|
+
const insightReportFile = path.join(fullPath, 'copilot-insight.md');
|
|
89
89
|
|
|
90
90
|
// Check if workspace.yaml exists
|
|
91
91
|
if (!await fileExists(workspaceFile)) {
|