@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 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-report.md (AI analysis) │
114
+ │ • copilot-insight.md (AI analysis) │
115
115
  └─────────────────────────────────────────────────┘
116
116
  ```
117
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qiaolei81/copilot-session-viewer",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Web UI for viewing GitHub Copilot CLI session logs",
5
5
  "author": "Lei Qiao <qiaolei81@gmail.com>",
6
6
  "license": "MIT",
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, insightLimiter, uploadLimiter } = require('./middleware/rateLimiting');
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', insightLimiter, insightController.generateInsight.bind(insightController));
80
- app.get('/session/:id/insight', insightLimiter, insightController.getInsightStatus.bind(insightController));
81
- app.delete('/session/:id/insight', insightLimiter, insightController.deleteInsight.bind(insightController));
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);
@@ -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) => req.path.startsWith('/public') // Skip static files
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 generation (stricter)
15
- const insightLimiter = rateLimit({
16
- windowMs: config.RATE_LIMIT_WINDOW_MS,
17
- max: config.RATE_LIMIT_MAX_REQUESTS,
18
- message: { error: 'Too many insight generation requests. Please try again later.' },
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
- insightLimiter,
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-report.md');
28
- const lockFile = path.join(sessionPath, 'insight-report.md.lock');
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-report.md.tmp');
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, one event per line) and generate a deep, actionable insight report.
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
- 3. **Performance Bottlenecks**:
218
- - Slowest operations with timing data
219
- - Where LLM is spending most time
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
- 4. **Sub-Agent Effectiveness**:
223
- - Which sub-agents succeeded/failed and why
224
- - Completion patterns and failure points
225
- - Resource utilization (tool calls per sub-agent)
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
- 5. **Tool Usage Intelligence**:
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-report.md');
265
- const lockFile = path.join(sessionPath, 'insight-report.md.lock');
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-report.md');
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-report.md');
88
+ const insightReportFile = path.join(fullPath, 'copilot-insight.md');
89
89
 
90
90
  // Check if workspace.yaml exists
91
91
  if (!await fileExists(workspaceFile)) {