mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-08-21 08:25:14 +00:00
Tabs -> Spaces
This commit is contained in:
+300
-300
@@ -4,9 +4,9 @@ import { globSync } from 'tinyglobby';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import type {
|
||||
BlockMetrics,
|
||||
TokenMetrics,
|
||||
TranscriptLine
|
||||
BlockMetrics,
|
||||
TokenMetrics,
|
||||
TranscriptLine
|
||||
} from '../types';
|
||||
|
||||
// Ensure fs.promises compatibility for older Node versions
|
||||
@@ -15,185 +15,185 @@ const readFileSync = fs.readFileSync;
|
||||
const statSync = fs.statSync;
|
||||
|
||||
export async function getSessionDuration(transcriptPath: string): Promise<string | null> {
|
||||
try {
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter((line: string) => line.trim());
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter((line: string) => line.trim());
|
||||
|
||||
if (lines.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let firstTimestamp: Date | null = null;
|
||||
let lastTimestamp: Date | null = null;
|
||||
let firstTimestamp: Date | null = null;
|
||||
let lastTimestamp: Date | null = null;
|
||||
|
||||
// Find first valid timestamp
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
firstTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
// Find first valid timestamp
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
firstTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
// Find last valid timestamp (iterate backwards)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const data = JSON.parse(lines[i] ?? '') as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
lastTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
// Find last valid timestamp (iterate backwards)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const data = JSON.parse(lines[i] ?? '') as { timestamp?: string };
|
||||
if (data.timestamp) {
|
||||
lastTimestamp = new Date(data.timestamp);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstTimestamp || !lastTimestamp) {
|
||||
return null;
|
||||
}
|
||||
if (!firstTimestamp || !lastTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate duration in milliseconds
|
||||
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
|
||||
// Calculate duration in milliseconds
|
||||
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
|
||||
|
||||
// Convert to minutes
|
||||
const totalMinutes = Math.floor(durationMs / (1000 * 60));
|
||||
// Convert to minutes
|
||||
const totalMinutes = Math.floor(durationMs / (1000 * 60));
|
||||
|
||||
if (totalMinutes < 1) {
|
||||
return '<1m';
|
||||
}
|
||||
if (totalMinutes < 1) {
|
||||
return '<1m';
|
||||
}
|
||||
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes}m`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours}hr`;
|
||||
} else {
|
||||
return `${hours}hr ${minutes}m`;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (hours === 0) {
|
||||
return `${minutes}m`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours}hr`;
|
||||
} else {
|
||||
return `${hours}hr ${minutes}m`;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTokenMetrics(transcriptPath: string): Promise<TokenMetrics> {
|
||||
try {
|
||||
// Use Node.js-compatible file reading
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
try {
|
||||
// Use Node.js-compatible file reading
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n');
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n');
|
||||
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let cachedTokens = 0;
|
||||
let contextLength = 0;
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let cachedTokens = 0;
|
||||
let contextLength = 0;
|
||||
|
||||
// Parse each line and sum up token usage for totals
|
||||
let mostRecentMainChainEntry: TranscriptLine | null = null;
|
||||
let mostRecentTimestamp: Date | null = null;
|
||||
// Parse each line and sum up token usage for totals
|
||||
let mostRecentMainChainEntry: TranscriptLine | null = null;
|
||||
let mostRecentTimestamp: Date | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as TranscriptLine;
|
||||
if (data.message?.usage) {
|
||||
inputTokens += data.message.usage.input_tokens || 0;
|
||||
outputTokens += data.message.usage.output_tokens || 0;
|
||||
cachedTokens += data.message.usage.cache_read_input_tokens ?? 0;
|
||||
cachedTokens += data.message.usage.cache_creation_input_tokens ?? 0;
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line) as TranscriptLine;
|
||||
if (data.message?.usage) {
|
||||
inputTokens += data.message.usage.input_tokens || 0;
|
||||
outputTokens += data.message.usage.output_tokens || 0;
|
||||
cachedTokens += data.message.usage.cache_read_input_tokens ?? 0;
|
||||
cachedTokens += data.message.usage.cache_creation_input_tokens ?? 0;
|
||||
|
||||
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
|
||||
if (data.isSidechain !== true && data.timestamp) {
|
||||
const entryTime = new Date(data.timestamp);
|
||||
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
|
||||
mostRecentTimestamp = entryTime;
|
||||
mostRecentMainChainEntry = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
|
||||
if (data.isSidechain !== true && data.timestamp) {
|
||||
const entryTime = new Date(data.timestamp);
|
||||
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
|
||||
mostRecentTimestamp = entryTime;
|
||||
mostRecentMainChainEntry = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate context length from the most recent main chain message
|
||||
if (mostRecentMainChainEntry?.message?.usage) {
|
||||
const usage = mostRecentMainChainEntry.message.usage;
|
||||
contextLength = (usage.input_tokens || 0)
|
||||
+ (usage.cache_read_input_tokens ?? 0)
|
||||
+ (usage.cache_creation_input_tokens ?? 0);
|
||||
}
|
||||
// Calculate context length from the most recent main chain message
|
||||
if (mostRecentMainChainEntry?.message?.usage) {
|
||||
const usage = mostRecentMainChainEntry.message.usage;
|
||||
contextLength = (usage.input_tokens || 0)
|
||||
+ (usage.cache_read_input_tokens ?? 0)
|
||||
+ (usage.cache_creation_input_tokens ?? 0);
|
||||
}
|
||||
|
||||
const totalTokens = inputTokens + outputTokens + cachedTokens;
|
||||
const totalTokens = inputTokens + outputTokens + cachedTokens;
|
||||
|
||||
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
|
||||
} catch {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
|
||||
} catch {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets block metrics for the current 5-hour block from JSONL files
|
||||
*/
|
||||
export function getBlockMetrics(transcriptPath: string | undefined): BlockMetrics | null {
|
||||
if (!transcriptPath || typeof transcriptPath !== 'string') {
|
||||
return null;
|
||||
}
|
||||
if (!transcriptPath || typeof transcriptPath !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let claudePath: string | null = null;
|
||||
let claudePath: string | null = null;
|
||||
|
||||
// On Windows, always use the user's home directory .claude folder
|
||||
if (process.platform === 'win32') {
|
||||
const homeDir = process.env.USERPROFILE ?? process.env.HOME;
|
||||
if (homeDir) {
|
||||
claudePath = path.join(homeDir, '.claude');
|
||||
// Verify the .claude folder exists
|
||||
if (!fs.existsSync(claudePath)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// On other platforms, walk up the directory tree to find .claude folder
|
||||
let currentPath = path.dirname(transcriptPath);
|
||||
const visitedPaths = new Set<string>();
|
||||
// On Windows, always use the user's home directory .claude folder
|
||||
if (process.platform === 'win32') {
|
||||
const homeDir = process.env.USERPROFILE ?? process.env.HOME;
|
||||
if (homeDir) {
|
||||
claudePath = path.join(homeDir, '.claude');
|
||||
// Verify the .claude folder exists
|
||||
if (!fs.existsSync(claudePath)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// On other platforms, walk up the directory tree to find .claude folder
|
||||
let currentPath = path.dirname(transcriptPath);
|
||||
const visitedPaths = new Set<string>();
|
||||
|
||||
while (currentPath && !visitedPaths.has(currentPath)) {
|
||||
visitedPaths.add(currentPath);
|
||||
while (currentPath && !visitedPaths.has(currentPath)) {
|
||||
visitedPaths.add(currentPath);
|
||||
|
||||
const baseName = path.basename(currentPath);
|
||||
if (baseName === '.claude') {
|
||||
claudePath = currentPath;
|
||||
break;
|
||||
}
|
||||
const baseName = path.basename(currentPath);
|
||||
if (baseName === '.claude') {
|
||||
claudePath = currentPath;
|
||||
break;
|
||||
}
|
||||
|
||||
const parentPath = path.dirname(currentPath);
|
||||
// Stop if we've reached the root (parent is same as current)
|
||||
if (parentPath === currentPath) {
|
||||
break;
|
||||
}
|
||||
currentPath = parentPath;
|
||||
}
|
||||
}
|
||||
const parentPath = path.dirname(currentPath);
|
||||
// Stop if we've reached the root (parent is same as current)
|
||||
if (parentPath === currentPath) {
|
||||
break;
|
||||
}
|
||||
currentPath = parentPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (!claudePath)
|
||||
return null;
|
||||
if (!claudePath)
|
||||
return null;
|
||||
|
||||
try {
|
||||
return findMostRecentBlockStartTime(claudePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return findMostRecentBlockStartTime(claudePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,204 +201,204 @@ export function getBlockMetrics(transcriptPath: string | undefined): BlockMetric
|
||||
* Uses file modification times as hints to avoid unnecessary reads
|
||||
*/
|
||||
function findMostRecentBlockStartTime(
|
||||
rootDir: string,
|
||||
sessionDurationHours = 5
|
||||
rootDir: string,
|
||||
sessionDurationHours = 5
|
||||
): BlockMetrics | null {
|
||||
const sessionDurationMs = sessionDurationHours * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
const sessionDurationMs = sessionDurationHours * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
|
||||
// Step 1: Find all JSONL files with their modification times
|
||||
// Use forward slashes for glob patterns on all platforms (tinyglobby requirement)
|
||||
const pattern = path.posix.join(rootDir.replace(/\\/g, '/'), 'projects', '**', '*.jsonl');
|
||||
const files = globSync([pattern], {
|
||||
absolute: true, // Ensure we get absolute paths
|
||||
cwd: rootDir // Set working directory to rootDir
|
||||
});
|
||||
// Step 1: Find all JSONL files with their modification times
|
||||
// Use forward slashes for glob patterns on all platforms (tinyglobby requirement)
|
||||
const pattern = path.posix.join(rootDir.replace(/\\/g, '/'), 'projects', '**', '*.jsonl');
|
||||
const files = globSync([pattern], {
|
||||
absolute: true, // Ensure we get absolute paths
|
||||
cwd: rootDir // Set working directory to rootDir
|
||||
});
|
||||
|
||||
if (files.length === 0)
|
||||
return null;
|
||||
if (files.length === 0)
|
||||
return null;
|
||||
|
||||
// Step 2: Get file stats and sort by modification time (most recent first)
|
||||
const filesWithStats = files.map((file) => {
|
||||
const stats = statSync(file);
|
||||
return { file, mtime: stats.mtime };
|
||||
});
|
||||
// Step 2: Get file stats and sort by modification time (most recent first)
|
||||
const filesWithStats = files.map((file) => {
|
||||
const stats = statSync(file);
|
||||
return { file, mtime: stats.mtime };
|
||||
});
|
||||
|
||||
filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
// Step 3: Progressive lookback - start small and expand if needed
|
||||
// Start with 2x session duration (10 hours), expand to 48 hours if needed
|
||||
const lookbackChunks = [
|
||||
10, // 2x session duration - catches most cases
|
||||
20, // 4x session duration - catches longer sessions
|
||||
48 // Maximum lookback for marathon sessions
|
||||
];
|
||||
// Step 3: Progressive lookback - start small and expand if needed
|
||||
// Start with 2x session duration (10 hours), expand to 48 hours if needed
|
||||
const lookbackChunks = [
|
||||
10, // 2x session duration - catches most cases
|
||||
20, // 4x session duration - catches longer sessions
|
||||
48 // Maximum lookback for marathon sessions
|
||||
];
|
||||
|
||||
let timestamps: Date[] = [];
|
||||
let mostRecentTimestamp: Date | null = null;
|
||||
let continuousWorkStart: Date | null = null;
|
||||
let foundSessionGap = false;
|
||||
let timestamps: Date[] = [];
|
||||
let mostRecentTimestamp: Date | null = null;
|
||||
let continuousWorkStart: Date | null = null;
|
||||
let foundSessionGap = false;
|
||||
|
||||
for (const lookbackHours of lookbackChunks) {
|
||||
const cutoffTime = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000);
|
||||
timestamps = [];
|
||||
for (const lookbackHours of lookbackChunks) {
|
||||
const cutoffTime = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000);
|
||||
timestamps = [];
|
||||
|
||||
// Collect timestamps for this lookback period
|
||||
for (const { file, mtime } of filesWithStats) {
|
||||
if (mtime.getTime() < cutoffTime.getTime()) {
|
||||
break;
|
||||
}
|
||||
const fileTimestamps = getAllTimestampsFromFile(file);
|
||||
timestamps.push(...fileTimestamps);
|
||||
}
|
||||
// Collect timestamps for this lookback period
|
||||
for (const { file, mtime } of filesWithStats) {
|
||||
if (mtime.getTime() < cutoffTime.getTime()) {
|
||||
break;
|
||||
}
|
||||
const fileTimestamps = getAllTimestampsFromFile(file);
|
||||
timestamps.push(...fileTimestamps);
|
||||
}
|
||||
|
||||
if (timestamps.length === 0) {
|
||||
continue; // Try next chunk
|
||||
}
|
||||
if (timestamps.length === 0) {
|
||||
continue; // Try next chunk
|
||||
}
|
||||
|
||||
// Sort timestamps (most recent first)
|
||||
timestamps.sort((a, b) => b.getTime() - a.getTime());
|
||||
// Sort timestamps (most recent first)
|
||||
timestamps.sort((a, b) => b.getTime() - a.getTime());
|
||||
|
||||
// Get most recent timestamp (only set once)
|
||||
if (!mostRecentTimestamp && timestamps[0]) {
|
||||
mostRecentTimestamp = timestamps[0];
|
||||
// Get most recent timestamp (only set once)
|
||||
if (!mostRecentTimestamp && timestamps[0]) {
|
||||
mostRecentTimestamp = timestamps[0];
|
||||
|
||||
// Check if the most recent activity is within the current session period
|
||||
const timeSinceLastActivity = now.getTime() - mostRecentTimestamp.getTime();
|
||||
if (timeSinceLastActivity > sessionDurationMs) {
|
||||
// No activity within the current session period
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Check if the most recent activity is within the current session period
|
||||
const timeSinceLastActivity = now.getTime() - mostRecentTimestamp.getTime();
|
||||
if (timeSinceLastActivity > sessionDurationMs) {
|
||||
// No activity within the current session period
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for a session gap in this chunk
|
||||
continuousWorkStart = mostRecentTimestamp;
|
||||
for (let i = 1; i < timestamps.length; i++) {
|
||||
const currentTimestamp = timestamps[i];
|
||||
const previousTimestamp = timestamps[i - 1];
|
||||
// Look for a session gap in this chunk
|
||||
continuousWorkStart = mostRecentTimestamp;
|
||||
for (let i = 1; i < timestamps.length; i++) {
|
||||
const currentTimestamp = timestamps[i];
|
||||
const previousTimestamp = timestamps[i - 1];
|
||||
|
||||
if (!currentTimestamp || !previousTimestamp)
|
||||
continue;
|
||||
if (!currentTimestamp || !previousTimestamp)
|
||||
continue;
|
||||
|
||||
const gap = previousTimestamp.getTime() - currentTimestamp.getTime();
|
||||
const gap = previousTimestamp.getTime() - currentTimestamp.getTime();
|
||||
|
||||
if (gap >= sessionDurationMs) {
|
||||
// Found a true session boundary
|
||||
foundSessionGap = true;
|
||||
break;
|
||||
}
|
||||
if (gap >= sessionDurationMs) {
|
||||
// Found a true session boundary
|
||||
foundSessionGap = true;
|
||||
break;
|
||||
}
|
||||
|
||||
continuousWorkStart = currentTimestamp;
|
||||
}
|
||||
continuousWorkStart = currentTimestamp;
|
||||
}
|
||||
|
||||
// If we found a gap, we're done
|
||||
if (foundSessionGap) {
|
||||
break;
|
||||
}
|
||||
// If we found a gap, we're done
|
||||
if (foundSessionGap) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If this was our last chunk, use what we have
|
||||
if (lookbackHours === lookbackChunks[lookbackChunks.length - 1]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If this was our last chunk, use what we have
|
||||
if (lookbackHours === lookbackChunks[lookbackChunks.length - 1]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mostRecentTimestamp || !continuousWorkStart) {
|
||||
return null;
|
||||
}
|
||||
if (!mostRecentTimestamp || !continuousWorkStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build actual blocks from timestamps going forward
|
||||
const blocks: { start: Date; end: Date }[] = [];
|
||||
const sortedTimestamps = timestamps.slice().sort((a, b) => a.getTime() - b.getTime());
|
||||
// Build actual blocks from timestamps going forward
|
||||
const blocks: { start: Date; end: Date }[] = [];
|
||||
const sortedTimestamps = timestamps.slice().sort((a, b) => a.getTime() - b.getTime());
|
||||
|
||||
let currentBlockStart: Date | null = null;
|
||||
let currentBlockEnd: Date | null = null;
|
||||
let currentBlockStart: Date | null = null;
|
||||
let currentBlockEnd: Date | null = null;
|
||||
|
||||
for (const timestamp of sortedTimestamps) {
|
||||
if (timestamp.getTime() < continuousWorkStart.getTime())
|
||||
continue;
|
||||
for (const timestamp of sortedTimestamps) {
|
||||
if (timestamp.getTime() < continuousWorkStart.getTime())
|
||||
continue;
|
||||
|
||||
if (!currentBlockStart || (currentBlockEnd && timestamp.getTime() > currentBlockEnd.getTime())) {
|
||||
// Start new block
|
||||
currentBlockStart = floorToHour(timestamp);
|
||||
currentBlockEnd = new Date(currentBlockStart.getTime() + sessionDurationMs);
|
||||
blocks.push({ start: currentBlockStart, end: currentBlockEnd });
|
||||
}
|
||||
}
|
||||
if (!currentBlockStart || (currentBlockEnd && timestamp.getTime() > currentBlockEnd.getTime())) {
|
||||
// Start new block
|
||||
currentBlockStart = floorToHour(timestamp);
|
||||
currentBlockEnd = new Date(currentBlockStart.getTime() + sessionDurationMs);
|
||||
blocks.push({ start: currentBlockStart, end: currentBlockEnd });
|
||||
}
|
||||
}
|
||||
|
||||
// Find current block
|
||||
for (const block of blocks) {
|
||||
if (now.getTime() >= block.start.getTime() && now.getTime() <= block.end.getTime()) {
|
||||
// Verify we have activity in this block
|
||||
const hasActivity = timestamps.some(t => t.getTime() >= block.start.getTime()
|
||||
&& t.getTime() <= block.end.getTime()
|
||||
);
|
||||
// Find current block
|
||||
for (const block of blocks) {
|
||||
if (now.getTime() >= block.start.getTime() && now.getTime() <= block.end.getTime()) {
|
||||
// Verify we have activity in this block
|
||||
const hasActivity = timestamps.some(t => t.getTime() >= block.start.getTime()
|
||||
&& t.getTime() <= block.end.getTime()
|
||||
);
|
||||
|
||||
if (hasActivity) {
|
||||
return {
|
||||
startTime: block.start,
|
||||
lastActivity: mostRecentTimestamp
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasActivity) {
|
||||
return {
|
||||
startTime: block.start,
|
||||
lastActivity: mostRecentTimestamp
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all timestamps from a JSONL file
|
||||
*/
|
||||
function getAllTimestampsFromFile(filePath: string): Date[] {
|
||||
const timestamps: Date[] = [];
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.length > 0);
|
||||
const timestamps: Date[] = [];
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.length > 0);
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line) as {
|
||||
timestamp?: string;
|
||||
isSidechain?: boolean;
|
||||
message?: { usage?: { input_tokens?: number; output_tokens?: number } };
|
||||
};
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line) as {
|
||||
timestamp?: string;
|
||||
isSidechain?: boolean;
|
||||
message?: { usage?: { input_tokens?: number; output_tokens?: number } };
|
||||
};
|
||||
|
||||
// Only treat entries with real token usage as block activity
|
||||
const usage = json.message?.usage;
|
||||
if (!usage)
|
||||
continue;
|
||||
// Only treat entries with real token usage as block activity
|
||||
const usage = json.message?.usage;
|
||||
if (!usage)
|
||||
continue;
|
||||
|
||||
const hasInputTokens = typeof usage.input_tokens === 'number';
|
||||
const hasOutputTokens = typeof usage.output_tokens === 'number';
|
||||
if (!hasInputTokens || !hasOutputTokens)
|
||||
continue;
|
||||
const hasInputTokens = typeof usage.input_tokens === 'number';
|
||||
const hasOutputTokens = typeof usage.output_tokens === 'number';
|
||||
if (!hasInputTokens || !hasOutputTokens)
|
||||
continue;
|
||||
|
||||
if (json.isSidechain === true)
|
||||
continue;
|
||||
if (json.isSidechain === true)
|
||||
continue;
|
||||
|
||||
const timestamp = json.timestamp;
|
||||
if (typeof timestamp !== 'string')
|
||||
continue;
|
||||
const timestamp = json.timestamp;
|
||||
if (typeof timestamp !== 'string')
|
||||
continue;
|
||||
|
||||
const date = new Date(timestamp);
|
||||
if (!Number.isNaN(date.getTime()))
|
||||
timestamps.push(date);
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const date = new Date(timestamp);
|
||||
if (!Number.isNaN(date.getTime()))
|
||||
timestamps.push(date);
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return timestamps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return timestamps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Floors a timestamp to the beginning of the hour (matching existing logic)
|
||||
*/
|
||||
function floorToHour(timestamp: Date): Date {
|
||||
const floored = new Date(timestamp);
|
||||
floored.setUTCMinutes(0, 0, 0);
|
||||
return floored;
|
||||
const floored = new Date(timestamp);
|
||||
floored.setUTCMinutes(0, 0, 0);
|
||||
return floored;
|
||||
}
|
||||
Reference in New Issue
Block a user