@pimzino/claude-code-spec-workflow
Version:
Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent task execution, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have
190 lines • 6.05 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsageTracker = void 0;
const crypto_1 = require("crypto");
const events_1 = require("events");
const logger_1 = require("../logger");
class UsageTracker extends events_1.EventEmitter {
constructor() {
super();
this.visitors = new Map();
this.totalAccesses = 0;
this.activeTimeout = 5 * 60 * 1000; // 5 minutes
this.startTime = new Date();
this.lastActivity = new Date();
}
/**
* Track a new access event
*/
trackAccess(event) {
(0, logger_1.debug)('Tracking access:', {
path: event.path,
userAgent: event.userAgent.substring(0, 50)
});
// Hash the IP for privacy
const hashedIp = this.hashIp(event.ip);
const visitorId = this.generateVisitorId(hashedIp, event.userAgent);
// Update visitor info
const now = new Date();
const visitor = this.visitors.get(visitorId);
if (visitor) {
visitor.lastSeen = now;
visitor.accessCount++;
}
else {
this.visitors.set(visitorId, {
id: visitorId,
firstSeen: now,
lastSeen: now,
accessCount: 1,
userAgent: event.userAgent,
hashedIp: hashedIp
});
// Emit new visitor event
this.emit('visitor:new', {
id: visitorId,
userAgent: event.userAgent
});
}
this.totalAccesses++;
this.lastActivity = now;
// Emit access event
this.emit('access', {
visitorId,
path: event.path,
timestamp: now
});
// Emit metrics update
this.emit('metrics:updated', this.getMetrics());
}
/**
* Get current usage metrics
*/
getMetrics() {
const now = Date.now();
const activeVisitors = Array.from(this.visitors.values())
.filter(visitor => (now - visitor.lastSeen.getTime()) < this.activeTimeout)
.length;
return {
totalVisitors: this.visitors.size,
activeVisitors,
totalAccesses: this.totalAccesses,
visitors: Array.from(this.visitors.values())
.sort((a, b) => b.lastSeen.getTime() - a.lastSeen.getTime()),
startTime: this.startTime,
lastActivity: this.lastActivity
};
}
/**
* Get metrics for a specific visitor
*/
getVisitorMetrics(visitorId) {
return this.visitors.get(visitorId) || null;
}
/**
* Clear all metrics (for privacy or reset)
*/
clearMetrics() {
(0, logger_1.debug)('Clearing all usage metrics');
this.visitors.clear();
this.totalAccesses = 0;
this.startTime = new Date();
this.lastActivity = new Date();
this.emit('metrics:cleared');
}
/**
* Remove inactive visitors (privacy cleanup)
*/
cleanupInactiveVisitors(maxAge = 24 * 60 * 60 * 1000) {
const now = Date.now();
let removed = 0;
for (const [id, visitor] of this.visitors.entries()) {
if (now - visitor.lastSeen.getTime() > maxAge) {
this.visitors.delete(id);
removed++;
}
}
if (removed > 0) {
(0, logger_1.debug)(`Cleaned up ${removed} inactive visitors`);
this.emit('metrics:updated', this.getMetrics());
}
return removed;
}
/**
* Hash IP address for privacy preservation
*/
hashIp(ip) {
// Use SHA-256 with a salt for privacy
const salt = 'claude-code-spec-workflow-v1';
return (0, crypto_1.createHash)('sha256')
.update(ip + salt)
.digest('hex')
.substring(0, 16); // Use first 16 chars for brevity
}
/**
* Generate a unique visitor ID
*/
generateVisitorId(hashedIp, userAgent) {
// Combine hashed IP and user agent for unique identification
const combined = hashedIp + userAgent;
return (0, crypto_1.createHash)('sha256')
.update(combined)
.digest('hex')
.substring(0, 12);
}
/**
* Get active visitor count
*/
getActiveVisitorCount() {
const now = Date.now();
return Array.from(this.visitors.values())
.filter(visitor => (now - visitor.lastSeen.getTime()) < this.activeTimeout)
.length;
}
/**
* Set active timeout duration
*/
setActiveTimeout(timeout) {
this.activeTimeout = timeout;
}
/**
* Export metrics as JSON
*/
exportMetrics() {
const metrics = this.getMetrics();
return JSON.stringify({
...metrics,
// Anonymize visitor data for export
visitors: metrics.visitors.map(v => ({
firstSeen: v.firstSeen,
lastSeen: v.lastSeen,
accessCount: v.accessCount,
// Don't export hashed IPs or full user agents
userAgentType: this.classifyUserAgent(v.userAgent)
}))
}, null, 2);
}
/**
* Classify user agent for privacy-preserving analytics
*/
classifyUserAgent(userAgent) {
const ua = userAgent.toLowerCase();
if (ua.includes('mobile'))
return 'mobile';
if (ua.includes('ipad') || ua.includes('tablet'))
return 'tablet';
if (ua.includes('bot') || ua.includes('crawler'))
return 'bot';
if (ua.includes('chrome'))
return 'chrome';
if (ua.includes('firefox'))
return 'firefox';
if (ua.includes('safari'))
return 'safari';
if (ua.includes('edg'))
return 'edge';
return 'other';
}
}
exports.UsageTracker = UsageTracker;
//# sourceMappingURL=usage-tracker.js.map