export type Tool = 'claude-code' | 'opencode' | 'crush' | 'pi' | 'kimi' | string
export type Host = 'laptop' | 'workpc' | string
export interface SessionDTO {
owner: string
tool: string
host: string
session_id: string
started_at: number // unix epoch seconds
ended_at: number
working_dir?: string
source_file: string
summary: string
turn_count: number
tokens_in_total: number
tokens_out_total: number
model?: string
metadata?: unknown
}
export interface Session {
id: string // `${tool}/${host}/${session_id}`
tool: string
host: string
cwd: string // working_dir ?? ''
model?: string
started: string // ISO 8601
ended: string | null // null if ended_at <= started_at
summary: string
turns: number
tokensIn: number
tokensOut: number
hasError: boolean // TODO: always false until error tracking is implemented
}
export function adaptSession(d: SessionDTO): Session {
return {
id: `${d.tool}/${d.host}/${d.session_id}`,
tool: d.tool,
host: d.host,
cwd: d.working_dir ?? '',
model: d.model,
started: new Date(d.started_at * 1000).toISOString(),
ended: d.ended_at > d.started_at ? new Date(d.ended_at * 1000).toISOString() : null,
summary: d.summary,
turns: d.turn_count,
tokensIn: d.tokens_in_total,
tokensOut: d.tokens_out_total,
hasError: false,
}
}
// ── Projects ─────────────────────────────────────────────────────────────────
export interface ProjectDTO {
cwd: string
sessions: number
turn_count: number
tokens_in_total: number
tokens_out_total: number
last_active: number // unix epoch seconds; 0 = never
hosts: string[]
tools: string[]
top_tool: string
}
export interface Project {
cwd: string
sessions: number
turns: number
tokensIn: number
tokensOut: number
lastActive: string // ISO 8601, or '' when last_active === 0
hosts: string[]
tools: string[]
topTool: string
}
export function adaptProject(d: ProjectDTO): Project {
return {
cwd: d.cwd,
sessions: d.sessions,
turns: d.turn_count,
tokensIn: d.tokens_in_total,
tokensOut: d.tokens_out_total,
lastActive: d.last_active === 0 ? '' : new Date(d.last_active * 1000).toISOString(),
hosts: d.hosts,
tools: d.tools,
topTool: d.top_tool,
}
}
// ── Stats ─────────────────────────────────────────────────────────────────────
export interface ToolRollupDTO {
tool: string
sessions: number
turns: number
tokens_in: number
tokens_out: number
daily_sparkline: number[]
}
export interface DailyBucketDTO {
date_unix: number
per_tool: Record<string, number>
}
export interface HeatmapCellDTO {
date_unix: number
count: number
}
export interface CwdRowDTO {
cwd: string
count: number
}
export interface HourBucketDTO {
hour: number
count: number
}
export interface HostRowDTO {
host: string
count: number
}
export interface StatsDTO {
per_tool: ToolRollupDTO[]
daily: DailyBucketDTO[]
heatmap: HeatmapCellDTO[]
top_cwd: CwdRowDTO[]
hour_of_day: HourBucketDTO[]
host_split: HostRowDTO[]
}
export interface ToolRollup {
tool: string
sessions: number
turns: number
tokensIn: number
tokensOut: number
dailySparkline: number[]
}
export interface DailyBucket {
date: string // ISO 8601
perTool: Record<string, number>
}
export interface HeatmapCell {
date: string // ISO 8601
count: number
}
export interface CwdRow {
cwd: string
count: number
}
export interface HourBucket {
hour: number
count: number
}
export interface HostRow {
host: string
count: number
}
export interface Stats {
perTool: ToolRollup[]
daily: DailyBucket[]
heatmap: HeatmapCell[]
topCwd: CwdRow[]
hourOfDay: HourBucket[]
hostSplit: HostRow[]
}
export function adaptStats(d: StatsDTO): Stats {
return {
perTool: d.per_tool.map(r => ({
tool: r.tool,
sessions: r.sessions,
turns: r.turns,
tokensIn: r.tokens_in,
tokensOut: r.tokens_out,
dailySparkline: r.daily_sparkline,
})),
daily: d.daily.map(b => ({
date: new Date(b.date_unix * 1000).toISOString(),
perTool: b.per_tool,
})),
heatmap: d.heatmap.map(c => ({
date: new Date(c.date_unix * 1000).toISOString(),
count: c.count,
})),
topCwd: d.top_cwd,
hourOfDay: d.hour_of_day,
hostSplit: d.host_split,
}
}