@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.6

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.
Files changed (45) hide show
  1. package/README.md +3 -3
  2. package/bin/copilot-session-viewer +2 -2
  3. package/dist/server.min.js +99 -0
  4. package/package.json +5 -17
  5. package/public/js/homepage.min.js +9 -9
  6. package/public/js/session-detail.min.js +36 -7
  7. package/public/vendor/marked.umd.min.js +8 -0
  8. package/public/vendor/purify.min.js +3 -0
  9. package/public/vendor/vue-virtual-scroller.css +1 -0
  10. package/public/vendor/vue-virtual-scroller.min.js +2 -0
  11. package/public/vendor/vue.global.prod.min.js +19 -0
  12. package/views/session-vue.ejs +31 -6
  13. package/views/time-analyze.ejs +2 -2
  14. package/lib/parsers/README.md +0 -239
  15. package/lib/parsers/base-parser.js +0 -53
  16. package/lib/parsers/claude-parser.js +0 -181
  17. package/lib/parsers/copilot-parser.js +0 -143
  18. package/lib/parsers/index.js +0 -15
  19. package/lib/parsers/parser-factory.js +0 -77
  20. package/lib/parsers/pi-mono-parser.js +0 -119
  21. package/lib/parsers/vscode-parser.js +0 -591
  22. package/server.js +0 -29
  23. package/src/app.js +0 -129
  24. package/src/config/index.js +0 -27
  25. package/src/controllers/insightController.js +0 -136
  26. package/src/controllers/sessionController.js +0 -449
  27. package/src/controllers/tagController.js +0 -113
  28. package/src/controllers/uploadController.js +0 -648
  29. package/src/middleware/common.js +0 -67
  30. package/src/middleware/rateLimiting.js +0 -62
  31. package/src/models/Session.js +0 -146
  32. package/src/routes/api.js +0 -11
  33. package/src/routes/insights.js +0 -12
  34. package/src/routes/pages.js +0 -12
  35. package/src/routes/uploads.js +0 -14
  36. package/src/schemas/event.schema.js +0 -73
  37. package/src/services/eventNormalizer.js +0 -291
  38. package/src/services/insightService.js +0 -535
  39. package/src/services/sessionRepository.js +0 -1092
  40. package/src/services/sessionService.js +0 -1919
  41. package/src/services/tagService.js +0 -205
  42. package/src/telemetry.js +0 -152
  43. package/src/utils/fileUtils.js +0 -305
  44. package/src/utils/helpers.js +0 -45
  45. package/src/utils/processManager.js +0 -85
@@ -1,205 +0,0 @@
1
- const fs = require('fs').promises;
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- /**
6
- * Service for managing session tags
7
- * - Per-session tags: stored in {session.directory}/tags.json as ["tag1", "tag2"]
8
- * - Global known tags: stored in ~/.session-viewer/known-tags.json as ["tag1", "tag2", ...]
9
- */
10
- class TagService {
11
- constructor() {
12
- this.knownTagsDir = path.join(os.homedir(), '.session-viewer');
13
- this.knownTagsFilePath = path.join(this.knownTagsDir, 'known-tags.json');
14
- }
15
-
16
- /**
17
- * Ensure known-tags directory and file exist
18
- */
19
- async ensureKnownTagsFile() {
20
- try {
21
- await fs.access(this.knownTagsFilePath);
22
- } catch (err) {
23
- // File doesn't exist, create directory and empty array
24
- await fs.mkdir(this.knownTagsDir, { recursive: true });
25
- await fs.writeFile(this.knownTagsFilePath, JSON.stringify([]), 'utf8');
26
- }
27
- }
28
-
29
- /**
30
- * Read known tags from global file
31
- * @returns {Promise<string[]>} Array of known tags
32
- */
33
- async readKnownTagsFile() {
34
- await this.ensureKnownTagsFile();
35
- try {
36
- const content = await fs.readFile(this.knownTagsFilePath, 'utf8');
37
- return JSON.parse(content);
38
- } catch (err) {
39
- console.error('Error reading known tags file:', err);
40
- return [];
41
- }
42
- }
43
-
44
- /**
45
- * Write known tags to global file
46
- * @param {string[]} tags - Array of known tags
47
- */
48
- async writeKnownTagsFile(tags) {
49
- await this.ensureKnownTagsFile();
50
- await fs.writeFile(this.knownTagsFilePath, JSON.stringify(tags, null, 2), 'utf8');
51
- }
52
-
53
- /**
54
- * Get tags file path for a session
55
- * @param {Session} session - Session object with directory field
56
- * @returns {string} Path to tags.json
57
- */
58
- getSessionTagsFilePath(session) {
59
- // File-based sessions (e.g. Claude .jsonl): per-file tags to avoid sharing
60
- if (session.filePath) {
61
- const dir = path.dirname(session.filePath);
62
- const base = path.basename(session.filePath, path.extname(session.filePath));
63
- return path.join(dir, `${base}.tags.json`);
64
- }
65
- // Directory-based sessions (e.g. Copilot CLI): tags.json inside session dir
66
- if (session.directory) {
67
- return path.join(session.directory, 'tags.json');
68
- }
69
- // Fallback: store in central location by session id
70
- return path.join(this.knownTagsDir, 'session-tags', `${session.id}.tags.json`);
71
- }
72
-
73
- /**
74
- * Normalize tag name (lowercase, trim, max 30 chars)
75
- * @param {string} tag - Tag name
76
- * @returns {string} Normalized tag
77
- */
78
- normalizeTag(tag) {
79
- return tag.trim().toLowerCase().substring(0, 30);
80
- }
81
-
82
- /**
83
- * Get all known tags for autocomplete
84
- * @returns {Promise<string[]>} Array of unique tags
85
- */
86
- async getAllKnownTags() {
87
- const tags = await this.readKnownTagsFile();
88
- return tags.sort();
89
- }
90
-
91
- /**
92
- * Get tags for a specific session
93
- * @param {Session} session - Session object with directory field
94
- * @returns {Promise<string[]>} Array of tags
95
- */
96
- async getSessionTags(session) {
97
- const tagsFilePath = this.getSessionTagsFilePath(session);
98
-
99
- try {
100
- await fs.access(tagsFilePath);
101
- const content = await fs.readFile(tagsFilePath, 'utf8');
102
- return JSON.parse(content);
103
- } catch (err) {
104
- // File doesn't exist or can't be read, return empty array
105
- return [];
106
- }
107
- }
108
-
109
- /**
110
- * Set tags for a specific session
111
- * @param {Session} session - Session object with directory field
112
- * @param {string[]} tags - Array of tag names
113
- * @returns {Promise<string[]>} Normalized and saved tags
114
- */
115
- async setSessionTags(session, tags) {
116
- if (!Array.isArray(tags)) {
117
- throw new Error('Tags must be an array');
118
- }
119
-
120
- // Normalize tags (lowercase, trim, max 30 chars)
121
- const normalizedTags = tags
122
- .map(tag => this.normalizeTag(tag))
123
- .filter(tag => tag.length > 0)
124
- .filter((tag, index, self) => self.indexOf(tag) === index); // Remove duplicates
125
-
126
- // Enforce max 10 tags per session
127
- if (normalizedTags.length > 10) {
128
- throw new Error('Maximum 10 tags per session');
129
- }
130
-
131
- const tagsFilePath = this.getSessionTagsFilePath(session);
132
-
133
- if (normalizedTags.length === 0) {
134
- // Remove tags file if no tags
135
- try {
136
- await fs.unlink(tagsFilePath);
137
- } catch (err) {
138
- // File doesn't exist, ignore
139
- }
140
- } else {
141
- // Ensure directory exists for tags file
142
- await fs.mkdir(path.dirname(tagsFilePath), { recursive: true });
143
- // Write tags to session directory
144
- await fs.writeFile(tagsFilePath, JSON.stringify(normalizedTags, null, 2), 'utf8');
145
-
146
- // Update known tags (append and deduplicate)
147
- await this.updateKnownTags(normalizedTags);
148
- }
149
-
150
- return normalizedTags;
151
- }
152
-
153
- /**
154
- * Update known tags by appending new tags and deduplicating
155
- * @param {string[]} newTags - New tags to add to known tags
156
- */
157
- async updateKnownTags(newTags) {
158
- const knownTags = await this.readKnownTagsFile();
159
- const allTags = [...knownTags, ...newTags];
160
- const uniqueTags = [...new Set(allTags)];
161
- await this.writeKnownTagsFile(uniqueTags);
162
- }
163
-
164
- /**
165
- * Add tags to a session (merge with existing)
166
- * @param {Session} session - Session object with directory field
167
- * @param {string[]} newTags - Tags to add
168
- * @returns {Promise<string[]>} Updated tags array
169
- */
170
- async addSessionTags(session, newTags) {
171
- const existingTags = await this.getSessionTags(session);
172
- const mergedTags = [...existingTags, ...newTags];
173
- return await this.setSessionTags(session, mergedTags);
174
- }
175
-
176
- /**
177
- * Remove tags from a session
178
- * @param {Session} session - Session object with directory field
179
- * @param {string[]} tagsToRemove - Tags to remove
180
- * @returns {Promise<string[]>} Updated tags array
181
- */
182
- async removeSessionTags(session, tagsToRemove) {
183
- const existingTags = await this.getSessionTags(session);
184
- const normalizedToRemove = tagsToRemove.map(tag => this.normalizeTag(tag));
185
- const updatedTags = existingTags.filter(tag => !normalizedToRemove.includes(tag));
186
- return await this.setSessionTags(session, updatedTags);
187
- }
188
-
189
- /**
190
- * Get tags for multiple sessions (batch)
191
- * @param {Session[]} sessions - Array of session objects
192
- * @returns {Promise<Object>} Map of sessionId -> tags array
193
- */
194
- async getMultipleSessionTags(sessions) {
195
- const result = {};
196
-
197
- for (const session of sessions) {
198
- result[session.id] = await this.getSessionTags(session);
199
- }
200
-
201
- return result;
202
- }
203
- }
204
-
205
- module.exports = TagService;
package/src/telemetry.js DELETED
@@ -1,152 +0,0 @@
1
- /**
2
- * Application Insights Telemetry Module
3
- *
4
- * This module initializes and configures Application Insights for telemetry tracking.
5
- * Must be required BEFORE any other modules (especially Express) in server.js.
6
- *
7
- * Features:
8
- * - Auto-collection of requests, dependencies, exceptions, and performance counters
9
- * - Custom event and metric tracking
10
- * - Automatic disabling in test environments
11
- * - Support for manual disabling via DISABLE_TELEMETRY env var
12
- */
13
-
14
- const appInsights = require('applicationinsights');
15
-
16
- // Determine if telemetry should be disabled
17
- const isTestEnvironment = process.env.NODE_ENV === 'test';
18
- const isDisabled = process.env.DISABLE_TELEMETRY === 'true' || isTestEnvironment;
19
-
20
- // Default connection string (can be overridden via env var)
21
- const DEFAULT_CONNECTION_STRING = 'InstrumentationKey=39f4fbf1-d82f-42c3-b4ef-ea92a1fd82cb;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=7d4bb432-f2f5-4526-a5e6-31901e5a2db2';
22
-
23
- let client = null;
24
-
25
- if (!isDisabled) {
26
- try {
27
- // Get connection string from environment or use default
28
- const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || DEFAULT_CONNECTION_STRING;
29
-
30
- // Setup and start Application Insights
31
- appInsights.setup(connectionString)
32
- .setAutoDependencyCorrelation(true)
33
- .setAutoCollectRequests(true)
34
- .setAutoCollectPerformance(true, true)
35
- .setAutoCollectExceptions(true)
36
- .setAutoCollectDependencies(true)
37
- .setAutoCollectConsole(false) // Disable console tracking to avoid noise
38
- .setUseDiskRetryCaching(true)
39
- .setSendLiveMetrics(false) // Disable live metrics for local dev tool
40
- .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
41
- .start();
42
-
43
- client = appInsights.defaultClient;
44
-
45
- // Set context properties
46
- client.context.tags[client.context.keys.cloudRole] = 'copilot-session-viewer';
47
- client.context.tags[client.context.keys.cloudRoleInstance] = require('os').hostname();
48
-
49
- console.log('✅ Application Insights telemetry initialized');
50
- } catch (error) {
51
- console.error('❌ Failed to initialize Application Insights:', error.message);
52
- // Continue without telemetry rather than crashing
53
- client = createNoOpClient();
54
- }
55
- } else {
56
- // Return no-op client for test environment or when disabled
57
- client = createNoOpClient();
58
-
59
- if (isTestEnvironment) {
60
- console.log('📊 Telemetry disabled (test environment)');
61
- } else {
62
- console.log('📊 Telemetry disabled (DISABLE_TELEMETRY=true)');
63
- }
64
- }
65
-
66
- /**
67
- * Creates a no-op client that safely ignores all telemetry calls
68
- * Used when telemetry is disabled or in test environments
69
- */
70
- function createNoOpClient() {
71
- return {
72
- trackEvent: () => {},
73
- trackMetric: () => {},
74
- trackException: () => {},
75
- trackTrace: () => {},
76
- trackDependency: () => {},
77
- trackRequest: () => {},
78
- flush: (callback) => {
79
- if (callback) callback();
80
- }
81
- };
82
- }
83
-
84
- /**
85
- * Track a custom event
86
- * @param {string} name - Event name
87
- * @param {Object} properties - Event properties
88
- */
89
- function trackEvent(name, properties = {}) {
90
- if (client && client.trackEvent) {
91
- client.trackEvent({
92
- name,
93
- properties
94
- });
95
- }
96
- }
97
-
98
- /**
99
- * Track a custom metric
100
- * @param {string} name - Metric name
101
- * @param {number} value - Metric value
102
- * @param {Object} properties - Additional properties
103
- */
104
- function trackMetric(name, value, properties = {}) {
105
- if (client && client.trackMetric) {
106
- client.trackMetric({
107
- name,
108
- value,
109
- properties
110
- });
111
- }
112
- }
113
-
114
- /**
115
- * Track an exception
116
- * @param {Error} error - Error object
117
- * @param {Object} properties - Additional properties
118
- */
119
- function trackException(error, properties = {}) {
120
- if (client && client.trackException) {
121
- client.trackException({
122
- exception: error,
123
- properties
124
- });
125
- }
126
- }
127
-
128
- /**
129
- * Flush telemetry data (useful for short-lived processes)
130
- * @returns {Promise<void>}
131
- */
132
- function flush() {
133
- return new Promise((resolve) => {
134
- if (client && client.flush) {
135
- client.flush({
136
- callback: () => resolve()
137
- });
138
- } else {
139
- resolve();
140
- }
141
- });
142
- }
143
-
144
- // Export the client and helper functions
145
- module.exports = {
146
- client,
147
- trackEvent,
148
- trackMetric,
149
- trackException,
150
- flush,
151
- isEnabled: !isDisabled
152
- };
@@ -1,305 +0,0 @@
1
- const fs = require('fs').promises;
2
- const fsSync = require('fs');
3
- const readline = require('readline');
4
-
5
- /**
6
- * File utility functions
7
- */
8
-
9
- /**
10
- * Check if a file exists
11
- * @param {string} filePath - File path
12
- * @returns {Promise<boolean>}
13
- */
14
- async function fileExists(filePath) {
15
- try {
16
- await fs.access(filePath);
17
- return true;
18
- } catch {
19
- return false;
20
- }
21
- }
22
-
23
- /**
24
- * Count lines in a file (non-empty lines only)
25
- * @param {string} filePath - File path
26
- * @returns {Promise<number>}
27
- */
28
- async function countLines(filePath) {
29
- try {
30
- const stream = fsSync.createReadStream(filePath, { encoding: 'utf-8' });
31
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
32
- let count = 0;
33
- for await (const line of rl) {
34
- if (line.trim()) count++;
35
- }
36
- return count;
37
- } catch (err) {
38
- console.error(`Error counting lines in ${filePath}:`, err.message);
39
- return 0;
40
- }
41
- }
42
-
43
- /**
44
- * Read and parse YAML file (simple key: value format)
45
- * @param {string} filePath - YAML file path
46
- * @returns {Promise<object>}
47
- */
48
- async function parseYAML(filePath) {
49
- try {
50
- const content = await fs.readFile(filePath, 'utf-8');
51
- const lines = content.split('\n');
52
- const result = {};
53
-
54
- for (const line of lines) {
55
- const match = line.match(/^(\w+):\s*(.+)$/);
56
- if (match) {
57
- result[match[1]] = match[2].trim();
58
- }
59
- }
60
-
61
- return result;
62
- } catch (err) {
63
- console.error(`Error parsing YAML ${filePath}:`, err.message);
64
- return {};
65
- }
66
- }
67
-
68
- /**
69
- * Efficiently read session metadata in a single pass
70
- * Combines getFirstUserMessage, getSessionDuration, getSessionMetadata
71
- * @param {string} filePath - Path to .jsonl file
72
- * @param {number} maxMessageLength - Max characters for first message (default 200)
73
- * @returns {Promise<Object>} Combined metadata object
74
- */
75
- async function getSessionMetadataOptimized(filePath, maxMessageLength = 200) {
76
- try {
77
- const stream = fsSync.createReadStream(filePath, { encoding: 'utf-8' });
78
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
79
-
80
- let firstUserMessage = '';
81
- let firstTimestamp = null;
82
- let lastTimestamp = null;
83
- let copilotVersion = null;
84
- let selectedModel = null;
85
- let hasSessionEnd = false;
86
-
87
- for await (const line of rl) {
88
- if (!line.trim()) continue;
89
-
90
- try {
91
- const event = JSON.parse(line);
92
-
93
- // Extract timestamp for duration calculation
94
- if (event.timestamp) {
95
- const ts = new Date(event.timestamp).getTime();
96
- if (!isNaN(ts)) {
97
- if (!firstTimestamp) firstTimestamp = ts;
98
- lastTimestamp = ts;
99
- }
100
- }
101
-
102
- // Get first user message
103
- if (!firstUserMessage && event.type === 'user.message') {
104
- const msg = event.data?.message || event.data?.content || event.data?.text || '';
105
- if (msg) {
106
- firstUserMessage = msg.length > maxMessageLength ? msg.substring(0, maxMessageLength) + '...' : msg;
107
- }
108
- }
109
-
110
- // Check for session.end event
111
- if (event.type === 'session.end') {
112
- hasSessionEnd = true;
113
- }
114
-
115
- // Get copilot version from session start
116
- if (event.type === 'session.start' && event.data?.copilotVersion && !copilotVersion) {
117
- copilotVersion = event.data.copilotVersion;
118
- }
119
-
120
- // Get selected model
121
- if ((event.type === 'session.start' || event.type === 'session.model_change') && !selectedModel) {
122
- if (event.data?.selectedModel) {
123
- selectedModel = event.data.selectedModel;
124
- } else if (event.data?.newModel) {
125
- selectedModel = event.data.newModel;
126
- } else if (event.data?.model) {
127
- selectedModel = event.data.model;
128
- }
129
- }
130
- } catch {
131
- // Skip malformed JSON lines
132
- }
133
- }
134
-
135
- rl.close();
136
- stream.destroy();
137
-
138
- // Calculate duration
139
- const duration = firstTimestamp && lastTimestamp && lastTimestamp > firstTimestamp
140
- ? lastTimestamp - firstTimestamp
141
- : null;
142
-
143
- return {
144
- firstUserMessage: firstUserMessage || '',
145
- duration,
146
- copilotVersion: copilotVersion || null,
147
- selectedModel: selectedModel || null,
148
- hasSessionEnd,
149
- lastEventTime: lastTimestamp,
150
- firstEventTime: firstTimestamp
151
- };
152
- } catch (err) {
153
- console.error(`Error reading session metadata from ${filePath}:`, err.message);
154
- return {
155
- firstUserMessage: '',
156
- duration: null,
157
- copilotVersion: null,
158
- selectedModel: null,
159
- hasSessionEnd: false,
160
- lastEventTime: null
161
- };
162
- }
163
- }
164
-
165
- /**
166
- * Get the first user message from a .jsonl events file
167
- * Reads line by line and stops at the first user.message event
168
- * @param {string} filePath - Path to .jsonl file
169
- * @param {number} maxLength - Max characters to return (default 200)
170
- * @returns {Promise<string>} First user message or empty string
171
- */
172
- async function getFirstUserMessage(filePath, maxLength = 200) {
173
- try {
174
- const stream = fsSync.createReadStream(filePath, { encoding: 'utf-8' });
175
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
176
-
177
- for await (const line of rl) {
178
- if (!line.trim()) continue;
179
- try {
180
- const event = JSON.parse(line);
181
- if (event.type === 'user.message') {
182
- const msg = event.data?.message || event.data?.content || event.data?.text || '';
183
- if (msg) {
184
- rl.close();
185
- stream.destroy();
186
- return msg.length > maxLength ? msg.substring(0, maxLength) + '...' : msg;
187
- }
188
- }
189
- } catch {
190
- // Skip malformed JSON lines
191
- }
192
- }
193
- return '';
194
- } catch (_err) {
195
- return '';
196
- }
197
- }
198
-
199
- /**
200
- * Get session duration by reading first and last event timestamps
201
- * @param {string} filePath - Path to .jsonl events file
202
- * @returns {Promise<number|null>} Duration in milliseconds, or null if unable to calculate
203
- */
204
- async function getSessionDuration(filePath) {
205
- try {
206
- const stream = fsSync.createReadStream(filePath, { encoding: 'utf-8' });
207
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
208
-
209
- let firstTimestamp = null;
210
- let lastTimestamp = null;
211
-
212
- for await (const line of rl) {
213
- if (!line.trim()) continue;
214
- try {
215
- const event = JSON.parse(line);
216
- if (event.timestamp) {
217
- const ts = new Date(event.timestamp).getTime();
218
- if (!firstTimestamp) {
219
- firstTimestamp = ts;
220
- }
221
- lastTimestamp = ts;
222
- }
223
- } catch {
224
- // Skip malformed JSON lines
225
- }
226
- }
227
-
228
- if (firstTimestamp && lastTimestamp && lastTimestamp >= firstTimestamp) {
229
- return lastTimestamp - firstTimestamp;
230
- }
231
- return null;
232
- } catch (err) {
233
- console.error(`Error calculating session duration for ${filePath}:`, err.message);
234
- return null;
235
- }
236
- }
237
-
238
- /**
239
- * Get session metadata from session.start event
240
- * @param {string} filePath - Path to .jsonl events file
241
- * @returns {Promise<{copilotVersion: string|null, selectedModel: string|null}>}
242
- */
243
- async function getSessionMetadata(filePath) {
244
- try {
245
- const stream = fsSync.createReadStream(filePath, { encoding: 'utf-8' });
246
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
247
-
248
- let copilotVersion = null;
249
- let selectedModel = null;
250
-
251
- for await (const line of rl) {
252
- if (!line.trim()) continue;
253
- try {
254
- const event = JSON.parse(line);
255
-
256
- // Extract copilotVersion and selectedModel from session.start
257
- if (event.type === 'session.start' && event.data) {
258
- copilotVersion = event.data.copilotVersion || null;
259
- selectedModel = event.data.selectedModel || null;
260
-
261
- // If we have selectedModel, we're done
262
- if (selectedModel) {
263
- rl.close();
264
- stream.destroy();
265
- return { copilotVersion, selectedModel };
266
- }
267
- }
268
-
269
- // If no selectedModel in session.start, check for model_change
270
- if (!selectedModel && event.type === 'session.model_change' && event.data) {
271
- selectedModel = event.data.newModel || null;
272
- rl.close();
273
- stream.destroy();
274
- return { copilotVersion, selectedModel };
275
- }
276
- } catch {
277
- // Skip malformed JSON lines
278
- }
279
- }
280
- return { copilotVersion, selectedModel };
281
- } catch (err) {
282
- console.error(`Error reading session metadata from ${filePath}:`, err.message);
283
- return { copilotVersion: null, selectedModel: null };
284
- }
285
- }
286
-
287
- /**
288
- * Check if entry should be skipped
289
- * @param {string} entry - Directory/file name
290
- * @returns {boolean}
291
- */
292
- function shouldSkipEntry(entry) {
293
- return entry === '.DS_Store' || entry.startsWith('.');
294
- }
295
-
296
- module.exports = {
297
- fileExists,
298
- countLines,
299
- parseYAML,
300
- getFirstUserMessage,
301
- getSessionDuration,
302
- getSessionMetadata,
303
- getSessionMetadataOptimized, // New optimized function
304
- shouldSkipEntry
305
- };
@@ -1,45 +0,0 @@
1
- /**
2
- * Helper utilities for server routes
3
- */
4
-
5
- /**
6
- * Build metadata object from session
7
- * @param {Session} session - Session model instance
8
- * @returns {Object} Metadata object
9
- */
10
- function buildMetadata(session) {
11
- const json = session.toJSON ? session.toJSON() : {};
12
- return {
13
- type: session.type,
14
- source: session.source, // 'copilot' or 'claude'
15
- sourceName: json.sourceName || session.source,
16
- sourceBadgeClass: json.sourceBadgeClass || 'source-unknown',
17
- summary: session.summary,
18
- model: session.selectedModel || session.model,
19
- repo: session.workspace?.repository,
20
- branch: session.workspace?.branch,
21
- cwd: session.workspace?.cwd,
22
- created: session.createdAt,
23
- updated: session.updatedAt,
24
- copilotVersion: session.copilotVersion,
25
- sessionStatus: session.sessionStatus
26
- };
27
- }
28
-
29
- /**
30
- * Validate session ID to prevent path traversal
31
- * @param {string} sessionId - Session identifier
32
- * @returns {boolean} True if valid
33
- */
34
- function isValidSessionId(sessionId) {
35
- // Allow alphanumeric, underscore, and hyphen (common in UUIDs and session IDs)
36
- // Length limit prevents abuse
37
- return typeof sessionId === 'string' &&
38
- /^[a-zA-Z0-9_-]+$/.test(sessionId) &&
39
- sessionId.length < 256;
40
- }
41
-
42
- module.exports = {
43
- buildMetadata,
44
- isValidSessionId
45
- };