import * as fs from "fs";
import * as path from "path";
import * as os from "os";
export interface SessionInfo {
sessionId: string;
sessionFilePath: string;
cwd: string;
projectDir: string;
}
export interface ContextStats {
sessionId: string;
fileSizeBytes: number;
lineCount: number;
userTurns: number;
assistantTurns: number;
summaryCount: number;
toolUseCalls: number;
estimatedTokens: number;
createdAt: string | null;
lastActivityAt: string | null;
}
/**
* Escape a path for use in Claude's project directory structure
* /private/tmp/foo -> -private-tmp-foo
*/
function escapePathForProject(p: string): string {
return p.replace(/\//g, "-");
}
/**
* Get session ID from environment, debug symlink, or PWD-based discovery
*/
export function getSessionId(): string | null {
// Primary: environment variable
if (process.env.CLAUDE_SESSION_ID) {
return process.env.CLAUDE_SESSION_ID;
}
// Secondary: debug symlink (most reliable for "current" session)
const debugLink = path.join(os.homedir(), ".claude", "debug", "latest");
try {
const target = fs.readlinkSync(debugLink);
return path.basename(target, ".txt");
} catch {
// Fall through to PWD-based discovery
}
// Tertiary: PWD-based discovery (find most recent session in current project)
const cwd = process.env.PWD || process.cwd();
const escapedPath = escapePathForProject(cwd);
const projectDir = path.join(getProjectsDir(), escapedPath);
if (fs.existsSync(projectDir)) {
try {
const files = fs.readdirSync(projectDir);
const sessionFiles = files
.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"))
.map((f) => ({
name: f,
path: path.join(projectDir, f),
mtime: fs.statSync(path.join(projectDir, f)).mtime,
}))
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
if (sessionFiles.length > 0) {
return path.basename(sessionFiles[0].name, ".jsonl");
}
} catch {
// Fall through
}
}
return null;
}
/**
* Get the Claude projects directory
*/
export function getProjectsDir(): string {
return (
process.env.CLAUDE_PROJECTS_DIR ||
path.join(os.homedir(), ".claude", "projects")
);
}
/**
* Get the checkpoints directory
*/
export function getCheckpointsDir(): string {
return (
process.env.CLAUDE_CHECKPOINTS_DIR ||
path.join(os.homedir(), ".claude", "checkpoints")
);
}
/**
* Find the session file for a given session ID
* Searches across all project directories
*/
export function findSessionFile(sessionId: string): SessionInfo | null {
const projectsDir = getProjectsDir();
if (!fs.existsSync(projectsDir)) {
return null;
}
// Search through project directories
const projectDirs = fs.readdirSync(projectsDir);
for (const dir of projectDirs) {
const projectPath = path.join(projectsDir, dir);
const stat = fs.statSync(projectPath);
if (!stat.isDirectory()) continue;
const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
if (fs.existsSync(sessionFile)) {
// Reconstruct cwd from escaped path
const cwd = dir.replace(/^-/, "/").replace(/-/g, "/");
return {
sessionId,
sessionFilePath: sessionFile,
cwd,
projectDir: projectPath,
};
}
}
return null;
}
/**
* Get current session info
*/
export function getCurrentSession(): SessionInfo | null {
const sessionId = getSessionId();
if (!sessionId) return null;
return findSessionFile(sessionId);
}
/**
* Calculate context statistics from a session file
*/
export function getContextStats(sessionFilePath: string): ContextStats | null {
if (!fs.existsSync(sessionFilePath)) {
return null;
}
const content = fs.readFileSync(sessionFilePath, "utf-8");
const lines = content.trim().split("\n").filter(Boolean);
let userTurns = 0;
let assistantTurns = 0;
let summaryCount = 0;
let toolUseCalls = 0;
let createdAt: string | null = null;
let lastActivityAt: string | null = null;
for (const line of lines) {
try {
const entry = JSON.parse(line);
// Track timestamps
if (entry.timestamp) {
if (!createdAt) createdAt = entry.timestamp;
lastActivityAt = entry.timestamp;
}
// Count by type
if (entry.type === "user") {
userTurns++;
} else if (entry.type === "assistant") {
assistantTurns++;
// Count tool uses within assistant messages
const msgContent = entry.message?.content;
if (Array.isArray(msgContent)) {
toolUseCalls += msgContent.filter(
(c: any) => c.type === "tool_use"
).length;
}
} else if (entry.type === "summary") {
summaryCount++;
}
} catch {
// Skip malformed lines
}
}
const fileSizeBytes = fs.statSync(sessionFilePath).size;
// Rough estimate: ~4 chars per token on average
const estimatedTokens = Math.round(fileSizeBytes / 4);
const sessionId = path.basename(sessionFilePath, ".jsonl");
return {
sessionId,
fileSizeBytes,
lineCount: lines.length,
userTurns,
assistantTurns,
summaryCount,
toolUseCalls,
estimatedTokens,
createdAt,
lastActivityAt,
};
}