Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **Default to the *current* session, not "most recently modified file".** Run with no
args, `cctime` now resolves `CLAUDE_CODE_SESSION_ID` (which Claude Code exports to
subprocesses) via the authoritative by-id lookup, so it reports the session you're
actually in — previously it picked whichever session's JSONL was written last, which
loses to any concurrently-active session (and silently showed the wrong one). The by-id
path also skips the `messageCount>2` "main session" filter, which could drop an active
session whose cached index count is stale. When not running inside Claude Code and ≥2
sessions were active in the last 5 min, it now prints which one it chose + a `--session`
hint instead of choosing silently.

## [1.0.0] - 2026-02-18

### Added
Expand Down
24 changes: 24 additions & 0 deletions src/finder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,27 @@ describe('finder: session filtering', () => {
expect(count).toBeLessThanOrEqual(2);
});
});

describe('finder: current session detection', () => {
const KEY = 'CLAUDE_CODE_SESSION_ID';
let saved: string | undefined;
beforeEach(() => { saved = process.env[KEY]; });
afterEach(() => {
if (saved === undefined) delete process.env[KEY];
else process.env[KEY] = saved;
});

it('returns CLAUDE_CODE_SESSION_ID when running inside a Claude Code session', async () => {
const { getCurrentSessionId } = await import('./finder.js');
process.env[KEY] = 'abc123-def';
expect(getCurrentSessionId()).toBe('abc123-def');
});

it('returns undefined when the env var is unset or blank', async () => {
const { getCurrentSessionId } = await import('./finder.js');
delete process.env[KEY];
expect(getCurrentSessionId()).toBeUndefined();
process.env[KEY] = ' ';
expect(getCurrentSessionId()).toBeUndefined();
});
});
22 changes: 19 additions & 3 deletions src/finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,19 @@ function filterMainSessions(entries: SessionIndexEntry[]): SessionIndexEntry[] {
return entries.filter(e => !e.isSidechain && e.messageCount > 2);
}

export async function getLastSession(projectFilter?: string): Promise<SessionIndexEntry | null> {
/** The session id of the Claude Code session this process is running inside,
* if any. Claude Code exposes it to subprocesses as CLAUDE_CODE_SESSION_ID —
* this lets `cctime` (no args) default to the ACTUAL current session instead
* of "the most recently modified file", which loses to any concurrently-active
* session. Returns undefined when run outside Claude Code (e.g. a plain shell). */
export function getCurrentSessionId(): string | undefined {
const id = process.env.CLAUDE_CODE_SESSION_ID;
return id && id.trim() ? id.trim() : undefined;
}

/** All main (non-sidechain) sessions across projects, most-recently-modified
* first. Shared by getLastSession and the no-arg default selection. */
export async function getAllSessions(projectFilter?: string): Promise<SessionIndexEntry[]> {
const dirs = await listProjectDirs();
let allEntries: SessionIndexEntry[] = [];

Expand All @@ -107,9 +119,13 @@ export async function getLastSession(projectFilter?: string): Promise<SessionInd
allEntries.push(...entries);
}

if (allEntries.length === 0) return null;
allEntries.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
return allEntries[0];
return allEntries;
}

export async function getLastSession(projectFilter?: string): Promise<SessionIndexEntry | null> {
const all = await getAllSessions(projectFilter);
return all[0] ?? null;
}

export async function getTodaySessions(projectFilter?: string): Promise<SessionIndexEntry[]> {
Expand Down
35 changes: 29 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { program } from 'commander';
import { getLastSession, getTodaySessions, getWeekSessions, getMonthSessions, getSessionsSince, getSessionById } from './finder.js';
import { getLastSession, getAllSessions, getCurrentSessionId, getTodaySessions, getWeekSessions, getMonthSessions, getSessionsSince, getSessionById } from './finder.js';
import { parseSession } from './parser.js';
import { analyzeSession } from './analyzer.js';
import { formatSession, formatAggregate, formatCompact, formatCsv, formatMarkdown, formatJsonAggregate } from './formatter.js';
Expand Down Expand Up @@ -191,12 +191,35 @@ program
entries = await getTodaySessions(opts.project);
label = 'Today';
} else {
// Default: last session
const entry = await getLastSession(opts.project);
// Default: the CURRENT session if we're running inside one, else the
// most recently active. Prefer CLAUDE_CODE_SESSION_ID so `cctime` (no
// args) reports the session you're actually in — not whichever
// concurrent session happened to write its file last. Resolve the
// current id via getSessionById (the same authoritative lookup --session
// uses): it matches by id and skips the messageCount>2 "main session"
// filter, which can otherwise drop an active session whose cached index
// count is stale/low.
const currentId = getCurrentSessionId();
let entry: SessionIndexEntry | null = currentId
? await getSessionById(currentId, opts.project)
: null;
if (!entry) {
console.error('No sessions found.');
console.error('Try running Claude Code first to create session files.');
process.exit(1);
const all = await getAllSessions(opts.project);
if (all.length === 0) {
console.error('No sessions found.');
console.error('Try running Claude Code first to create session files.');
process.exit(1);
}
entry = all[0];
// Heads-up when the pick is ambiguous: several sessions were written
// to in the last few minutes, so "most recent" may not be the one you
// mean. (Silent when we matched the current session — that's exact.)
const ACTIVE_WINDOW_MS = 5 * 60 * 1000;
const now = Date.now();
const recentlyActive = all.filter(e => now - new Date(e.modified).getTime() < ACTIVE_WINDOW_MS);
if (recentlyActive.length > 1) {
console.error(`cctime: ${recentlyActive.length} sessions were active in the last 5 min; showing the most recent (${entry.sessionId.slice(0, 8)}). Use --session <id> to pick another.`);
}
}
const analysis = await analyzeEntry(entry);
if (opts.csv || opts.compact || opts.markdown) {
Expand Down