agent-optic 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +337 -0
- package/examples/commit-tracker.ts +389 -0
- package/examples/cost-per-feature.ts +182 -0
- package/examples/match-git-commits.ts +171 -0
- package/examples/model-costs.ts +131 -0
- package/examples/pipe-match.ts +177 -0
- package/examples/prompt-history.ts +119 -0
- package/examples/session-digest.ts +89 -0
- package/examples/timesheet.ts +127 -0
- package/examples/work-patterns.ts +124 -0
- package/package.json +41 -0
- package/src/agent-optic.ts +325 -0
- package/src/aggregations/daily.ts +90 -0
- package/src/aggregations/project.ts +71 -0
- package/src/aggregations/time.ts +44 -0
- package/src/aggregations/tools.ts +60 -0
- package/src/claude-optic.ts +7 -0
- package/src/cli/index.ts +407 -0
- package/src/index.ts +69 -0
- package/src/parsers/content-blocks.ts +58 -0
- package/src/parsers/session-detail.ts +323 -0
- package/src/parsers/tool-categories.ts +86 -0
- package/src/pricing.ts +62 -0
- package/src/privacy/config.ts +67 -0
- package/src/privacy/redact.ts +99 -0
- package/src/readers/codex-rollout-reader.ts +145 -0
- package/src/readers/history-reader.ts +205 -0
- package/src/readers/plan-reader.ts +60 -0
- package/src/readers/project-reader.ts +101 -0
- package/src/readers/session-reader.ts +280 -0
- package/src/readers/skill-reader.ts +28 -0
- package/src/readers/stats-reader.ts +12 -0
- package/src/readers/task-reader.ts +117 -0
- package/src/types/aggregations.ts +47 -0
- package/src/types/plan.ts +6 -0
- package/src/types/privacy.ts +18 -0
- package/src/types/project.ts +13 -0
- package/src/types/provider.ts +9 -0
- package/src/types/session.ts +56 -0
- package/src/types/stats.ts +15 -0
- package/src/types/task.ts +16 -0
- package/src/types/transcript.ts +36 -0
- package/src/utils/dates.ts +40 -0
- package/src/utils/jsonl.ts +83 -0
- package/src/utils/paths.ts +57 -0
- package/src/utils/providers.ts +30 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* timesheet.ts — Generate a weekly timesheet from Claude session timestamps.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun examples/timesheet.ts [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
7
|
+
*
|
|
8
|
+
* Groups sessions by day and project, uses gap-capped hour estimation,
|
|
9
|
+
* and outputs a table suitable for time tracking or invoicing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createClaudeHistory, today, toLocalDate, type SessionMeta } from "../src/index.js";
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
function getArg(name: string): string | undefined {
|
|
16
|
+
const idx = args.indexOf(name);
|
|
17
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const dayOfWeek = new Date(now).getDay();
|
|
22
|
+
const mondayOffset = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
23
|
+
|
|
24
|
+
const from = getArg("--from") ?? toLocalDate(now - mondayOffset * 86400000);
|
|
25
|
+
const to = getArg("--to") ?? today();
|
|
26
|
+
|
|
27
|
+
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const ch = createClaudeHistory();
|
|
31
|
+
const sessions = await ch.sessions.listWithMeta({ from, to });
|
|
32
|
+
|
|
33
|
+
const byDateProject = new Map<string, Map<string, SessionMeta[]>>();
|
|
34
|
+
|
|
35
|
+
for (const s of sessions) {
|
|
36
|
+
const date = toLocalDate(s.timeRange.start);
|
|
37
|
+
const project = s.projectName || "unknown";
|
|
38
|
+
|
|
39
|
+
let projectMap = byDateProject.get(date);
|
|
40
|
+
if (!projectMap) {
|
|
41
|
+
projectMap = new Map();
|
|
42
|
+
byDateProject.set(date, projectMap);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const list = projectMap.get(project) ?? [];
|
|
46
|
+
list.push(s);
|
|
47
|
+
projectMap.set(project, list);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dates = [...byDateProject.keys()].sort();
|
|
51
|
+
|
|
52
|
+
console.log(`Timesheet: ${from} → ${to}`);
|
|
53
|
+
console.log("=".repeat(90));
|
|
54
|
+
console.log(
|
|
55
|
+
"Day".padEnd(6),
|
|
56
|
+
"Date".padEnd(12),
|
|
57
|
+
"Project".padEnd(30),
|
|
58
|
+
"Hours".padStart(8),
|
|
59
|
+
"Sessions".padStart(10),
|
|
60
|
+
"Prompts".padStart(10),
|
|
61
|
+
);
|
|
62
|
+
console.log("-".repeat(90));
|
|
63
|
+
|
|
64
|
+
let totalHours = 0;
|
|
65
|
+
let totalSessions = 0;
|
|
66
|
+
let totalPrompts = 0;
|
|
67
|
+
|
|
68
|
+
for (const date of dates) {
|
|
69
|
+
const projectMap = byDateProject.get(date)!;
|
|
70
|
+
const dayName = DAYS[new Date(date + "T12:00:00").getDay()];
|
|
71
|
+
let first = true;
|
|
72
|
+
|
|
73
|
+
const projects = [...projectMap.entries()].map(([project, sess]) => ({
|
|
74
|
+
project,
|
|
75
|
+
sessions: sess,
|
|
76
|
+
hours: ch.aggregate.estimateHours(sess),
|
|
77
|
+
prompts: sess.reduce((s, x) => s + x.prompts.length, 0),
|
|
78
|
+
}));
|
|
79
|
+
projects.sort((a, b) => b.hours - a.hours);
|
|
80
|
+
|
|
81
|
+
for (const p of projects) {
|
|
82
|
+
console.log(
|
|
83
|
+
(first ? dayName : "").padEnd(6),
|
|
84
|
+
(first ? date : "").padEnd(12),
|
|
85
|
+
p.project.slice(0, 29).padEnd(30),
|
|
86
|
+
p.hours.toFixed(1).padStart(8),
|
|
87
|
+
String(p.sessions.length).padStart(10),
|
|
88
|
+
String(p.prompts).padStart(10),
|
|
89
|
+
);
|
|
90
|
+
totalHours += p.hours;
|
|
91
|
+
totalSessions += p.sessions.length;
|
|
92
|
+
totalPrompts += p.prompts;
|
|
93
|
+
first = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log("-".repeat(90));
|
|
98
|
+
console.log(
|
|
99
|
+
"".padEnd(6),
|
|
100
|
+
"TOTAL".padEnd(12),
|
|
101
|
+
"".padEnd(30),
|
|
102
|
+
totalHours.toFixed(1).padStart(8),
|
|
103
|
+
String(totalSessions).padStart(10),
|
|
104
|
+
String(totalPrompts).padStart(10),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const allByProject = new Map<string, SessionMeta[]>();
|
|
108
|
+
for (const s of sessions) {
|
|
109
|
+
const project = s.projectName || "unknown";
|
|
110
|
+
const list = allByProject.get(project) ?? [];
|
|
111
|
+
list.push(s);
|
|
112
|
+
allByProject.set(project, list);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`\nSummary by Project`);
|
|
116
|
+
console.log("-".repeat(50));
|
|
117
|
+
|
|
118
|
+
const projectTotals = [...allByProject.entries()]
|
|
119
|
+
.map(([project, sess]) => ({ project, hours: ch.aggregate.estimateHours(sess) }))
|
|
120
|
+
.sort((a, b) => b.hours - a.hours);
|
|
121
|
+
|
|
122
|
+
for (const p of projectTotals) {
|
|
123
|
+
console.log(` ${p.project.padEnd(30)} ${p.hours.toFixed(1).padStart(8)}h`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* work-patterns.ts — Export aggregated work pattern metrics as JSON.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun examples/work-patterns.ts [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
7
|
+
*
|
|
8
|
+
* Outputs hour distribution, late-night/weekend counts, longest and most
|
|
9
|
+
* expensive sessions — all as JSON for an LLM to interpret.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createClaudeHistory, estimateCost, toLocalDate, type SessionMeta } from "../src/index.js";
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
function getArg(name: string, fallback: string): string {
|
|
16
|
+
const idx = args.indexOf(name);
|
|
17
|
+
return idx !== -1 ? (args[idx + 1] ?? fallback) : fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const from = getArg("--from", toLocalDate(Date.now() - 30 * 86400000));
|
|
21
|
+
const to = getArg("--to", toLocalDate(Date.now()));
|
|
22
|
+
|
|
23
|
+
function durationMinutes(s: SessionMeta): number {
|
|
24
|
+
return (s.timeRange.end - s.timeRange.start) / 60000;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
const ch = createClaudeHistory();
|
|
29
|
+
const sessions = await ch.sessions.listWithMeta({ from, to });
|
|
30
|
+
|
|
31
|
+
if (sessions.length === 0) {
|
|
32
|
+
console.log(JSON.stringify({ period: { from, to }, totalSessions: 0 }, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const totalCost = sessions.reduce((s, x) => s + estimateCost(x), 0);
|
|
37
|
+
const totalHours = ch.aggregate.estimateHours(sessions);
|
|
38
|
+
const totalTokens = sessions.reduce(
|
|
39
|
+
(s, x) => s + x.totalInputTokens + x.totalOutputTokens + x.cacheCreationInputTokens + x.cacheReadInputTokens,
|
|
40
|
+
0,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const hourBuckets = new Array(24).fill(0);
|
|
44
|
+
for (const s of sessions) {
|
|
45
|
+
hourBuckets[new Date(s.timeRange.start).getHours()]++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lateNightCount = sessions.filter((s) => {
|
|
49
|
+
const h = new Date(s.timeRange.start).getHours();
|
|
50
|
+
return h >= 22 || h < 5;
|
|
51
|
+
}).length;
|
|
52
|
+
|
|
53
|
+
const weekendCount = sessions.filter((s) => {
|
|
54
|
+
const day = new Date(s.timeRange.start).getDay();
|
|
55
|
+
return day === 0 || day === 6;
|
|
56
|
+
}).length;
|
|
57
|
+
|
|
58
|
+
const byDate = new Map<string, number>();
|
|
59
|
+
for (const s of sessions) {
|
|
60
|
+
const date = toLocalDate(s.timeRange.start);
|
|
61
|
+
byDate.set(date, (byDate.get(date) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
const busiestDay = [...byDate.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
64
|
+
|
|
65
|
+
const peakHour = hourBuckets.indexOf(Math.max(...hourBuckets));
|
|
66
|
+
|
|
67
|
+
const byDuration = [...sessions].sort((a, b) => durationMinutes(b) - durationMinutes(a));
|
|
68
|
+
const byCost = [...sessions].sort((a, b) => estimateCost(b) - estimateCost(a));
|
|
69
|
+
|
|
70
|
+
const longestSessions = byDuration.slice(0, 5).map((s) => ({
|
|
71
|
+
date: toLocalDate(s.timeRange.start),
|
|
72
|
+
project: s.projectName,
|
|
73
|
+
durationMinutes: Math.round(durationMinutes(s)),
|
|
74
|
+
prompts: s.prompts.length,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const mostExpensiveSessions = byCost.slice(0, 5).map((s) => ({
|
|
78
|
+
date: toLocalDate(s.timeRange.start),
|
|
79
|
+
project: s.projectName,
|
|
80
|
+
model: s.model ?? null,
|
|
81
|
+
estimatedCostUsd: +estimateCost(s).toFixed(4),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Per-project breakdown (top 10)
|
|
85
|
+
const projectMap = new Map<string, SessionMeta[]>();
|
|
86
|
+
for (const s of sessions) {
|
|
87
|
+
const arr = projectMap.get(s.projectName) ?? [];
|
|
88
|
+
arr.push(s);
|
|
89
|
+
projectMap.set(s.projectName, arr);
|
|
90
|
+
}
|
|
91
|
+
const byProject = [...projectMap.entries()]
|
|
92
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
93
|
+
.slice(0, 10)
|
|
94
|
+
.map(([project, projectSessions]) => ({
|
|
95
|
+
project,
|
|
96
|
+
sessions: projectSessions.length,
|
|
97
|
+
estimatedHours: +ch.aggregate.estimateHours(projectSessions).toFixed(1),
|
|
98
|
+
estimatedCostUsd: +projectSessions.reduce((s, x) => s + estimateCost(x), 0).toFixed(2),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
JSON.stringify(
|
|
103
|
+
{
|
|
104
|
+
period: { from, to },
|
|
105
|
+
totalSessions: sessions.length,
|
|
106
|
+
totalEstimatedHours: +totalHours.toFixed(1),
|
|
107
|
+
totalTokens,
|
|
108
|
+
totalEstimatedCostUsd: +totalCost.toFixed(2),
|
|
109
|
+
peakHour,
|
|
110
|
+
hourDistribution: Object.fromEntries(hourBuckets.map((count, hour) => [hour, count]).filter(([, c]) => c > 0)),
|
|
111
|
+
lateNightSessions: lateNightCount,
|
|
112
|
+
weekendSessions: weekendCount,
|
|
113
|
+
busiestDay: busiestDay ? { date: busiestDay[0], sessions: busiestDay[1] } : null,
|
|
114
|
+
byProject,
|
|
115
|
+
longestSessions,
|
|
116
|
+
mostExpensiveSessions,
|
|
117
|
+
},
|
|
118
|
+
null,
|
|
119
|
+
2,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-optic",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Zero-dependency, local-first framework for reading AI assistant session data from provider home directories",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"agent-optic": "./src/cli/index.ts",
|
|
11
|
+
"claude-optic": "./src/cli/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"files": ["src", "examples"],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"assistant",
|
|
17
|
+
"claude",
|
|
18
|
+
"codex",
|
|
19
|
+
"cursor",
|
|
20
|
+
"windsurf",
|
|
21
|
+
"session-history",
|
|
22
|
+
"developer-tools",
|
|
23
|
+
"productivity"
|
|
24
|
+
],
|
|
25
|
+
"author": "Kristoffer",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/Kristoffer88/agent-optic"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"knip": "knip"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"bun": ">=1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/bun": "^1.2.2",
|
|
39
|
+
"knip": "^5.83.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { PrivacyConfig, PrivacyProfile } from "./types/privacy.js";
|
|
2
|
+
import type { SessionInfo, SessionMeta, SessionDetail } from "./types/session.js";
|
|
3
|
+
import type { TranscriptEntry } from "./types/transcript.js";
|
|
4
|
+
import type { TaskInfo, TodoItem } from "./types/task.js";
|
|
5
|
+
import type { PlanInfo } from "./types/plan.js";
|
|
6
|
+
import type { ProjectInfo, ProjectMemory } from "./types/project.js";
|
|
7
|
+
import type { StatsCache } from "./types/stats.js";
|
|
8
|
+
import type { Provider } from "./types/provider.js";
|
|
9
|
+
import type {
|
|
10
|
+
DailySummary,
|
|
11
|
+
ProjectSummary,
|
|
12
|
+
ToolUsageReport,
|
|
13
|
+
DateFilter,
|
|
14
|
+
SessionListFilter,
|
|
15
|
+
} from "./types/aggregations.js";
|
|
16
|
+
|
|
17
|
+
import { providerPaths } from "./utils/paths.js";
|
|
18
|
+
import { resolveDateRange } from "./utils/dates.js";
|
|
19
|
+
import { canonicalProvider } from "./utils/providers.js";
|
|
20
|
+
import { resolvePrivacyConfig } from "./privacy/config.js";
|
|
21
|
+
import { setPricing, type ModelPricing } from "./pricing.js";
|
|
22
|
+
|
|
23
|
+
import { readHistory } from "./readers/history-reader.js";
|
|
24
|
+
import { peekSession, streamTranscript } from "./readers/session-reader.js";
|
|
25
|
+
import { readTasks, readTodos } from "./readers/task-reader.js";
|
|
26
|
+
import { readPlans } from "./readers/plan-reader.js";
|
|
27
|
+
import { readProjects, readProjectMemory } from "./readers/project-reader.js";
|
|
28
|
+
import { readStats } from "./readers/stats-reader.js";
|
|
29
|
+
import { readSkills, readSkillContent } from "./readers/skill-reader.js";
|
|
30
|
+
|
|
31
|
+
import { parseSessionDetail } from "./parsers/session-detail.js";
|
|
32
|
+
|
|
33
|
+
import { buildDailySummary, buildDailyRange } from "./aggregations/daily.js";
|
|
34
|
+
import { buildProjectSummaries } from "./aggregations/project.js";
|
|
35
|
+
import { buildToolUsageReport } from "./aggregations/tools.js";
|
|
36
|
+
import { estimateHours } from "./aggregations/time.js";
|
|
37
|
+
|
|
38
|
+
export interface HistoryConfig {
|
|
39
|
+
/** Assistant provider. Defaults to "claude". */
|
|
40
|
+
provider?: Provider;
|
|
41
|
+
/** Path to provider data directory. Defaults to provider-specific home folder. */
|
|
42
|
+
providerDir?: string;
|
|
43
|
+
/** Privacy profile name or partial config. Defaults to "local" */
|
|
44
|
+
privacy?: PrivacyProfile | Partial<PrivacyConfig>;
|
|
45
|
+
/** Enable caching of repeated reads. Defaults to true */
|
|
46
|
+
cache?: boolean;
|
|
47
|
+
/** Override or extend model pricing (USD per million tokens). Merges with built-in defaults. */
|
|
48
|
+
pricing?: Record<string, ModelPricing>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface History {
|
|
52
|
+
sessions: {
|
|
53
|
+
/** Fast: reads only history.jsonl */
|
|
54
|
+
list(filter?: SessionListFilter): Promise<SessionInfo[]>;
|
|
55
|
+
/** Medium: also peeks session files for branch/model/tokens */
|
|
56
|
+
listWithMeta(filter?: SessionListFilter): Promise<SessionMeta[]>;
|
|
57
|
+
/** Full: parses entire session JSONL */
|
|
58
|
+
detail(sessionId: string, projectPath?: string): Promise<SessionDetail>;
|
|
59
|
+
/** Streaming: yields transcript entries one at a time */
|
|
60
|
+
transcript(sessionId: string, projectPath?: string): AsyncGenerator<TranscriptEntry>;
|
|
61
|
+
/** Count sessions matching filter */
|
|
62
|
+
count(filter?: SessionListFilter): Promise<number>;
|
|
63
|
+
};
|
|
64
|
+
projects: {
|
|
65
|
+
list(): Promise<ProjectInfo[]>;
|
|
66
|
+
memory(projectPath: string): Promise<ProjectMemory | null>;
|
|
67
|
+
};
|
|
68
|
+
tasks: {
|
|
69
|
+
list(filter: DateFilter): Promise<TaskInfo[]>;
|
|
70
|
+
};
|
|
71
|
+
todos: {
|
|
72
|
+
list(filter: DateFilter): Promise<TodoItem[]>;
|
|
73
|
+
};
|
|
74
|
+
plans: {
|
|
75
|
+
list(filter: DateFilter): Promise<PlanInfo[]>;
|
|
76
|
+
};
|
|
77
|
+
skills: {
|
|
78
|
+
list(): Promise<string[]>;
|
|
79
|
+
read(name: string): Promise<string>;
|
|
80
|
+
};
|
|
81
|
+
stats: {
|
|
82
|
+
get(): Promise<StatsCache | null>;
|
|
83
|
+
};
|
|
84
|
+
aggregate: {
|
|
85
|
+
daily(date: string): Promise<DailySummary>;
|
|
86
|
+
dailyRange(from: string, to: string): Promise<DailySummary[]>;
|
|
87
|
+
byProject(filter?: SessionListFilter): Promise<ProjectSummary[]>;
|
|
88
|
+
toolUsage(filter?: SessionListFilter): Promise<ToolUsageReport>;
|
|
89
|
+
estimateHours(sessions: SessionInfo[]): number;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Create a provider-aware History instance for reading session data. */
|
|
94
|
+
export function createHistory(config?: HistoryConfig): History {
|
|
95
|
+
const requestedProvider = config?.provider ?? "claude";
|
|
96
|
+
const provider = canonicalProvider(requestedProvider);
|
|
97
|
+
const paths = providerPaths({
|
|
98
|
+
provider: requestedProvider,
|
|
99
|
+
providerDir: config?.providerDir,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (config?.pricing) {
|
|
103
|
+
setPricing(config.pricing);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const privacy: PrivacyConfig =
|
|
107
|
+
typeof config?.privacy === "string"
|
|
108
|
+
? resolvePrivacyConfig(config.privacy)
|
|
109
|
+
: resolvePrivacyConfig(undefined, config?.privacy);
|
|
110
|
+
|
|
111
|
+
const dailyPaths = {
|
|
112
|
+
historyFile: paths.historyFile,
|
|
113
|
+
projectsDir: paths.projectsDir,
|
|
114
|
+
sessionsDir: paths.sessionsDir,
|
|
115
|
+
tasksDir: paths.tasksDir,
|
|
116
|
+
plansDir: paths.plansDir,
|
|
117
|
+
todosDir: paths.todosDir,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
sessions: {
|
|
122
|
+
async list(filter?: SessionListFilter): Promise<SessionInfo[]> {
|
|
123
|
+
const { from, to } = resolveDateRange(filter);
|
|
124
|
+
let sessions = await readHistory(paths.historyFile, from, to, privacy, {
|
|
125
|
+
provider,
|
|
126
|
+
sessionsDir: paths.sessionsDir,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (filter?.project) {
|
|
130
|
+
const f = filter.project.toLowerCase();
|
|
131
|
+
sessions = sessions.filter((s) => s.projectName.toLowerCase().includes(f));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return sessions;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async listWithMeta(filter?: SessionListFilter): Promise<SessionMeta[]> {
|
|
138
|
+
const { from, to } = resolveDateRange(filter);
|
|
139
|
+
let sessions = await readHistory(paths.historyFile, from, to, privacy, {
|
|
140
|
+
provider,
|
|
141
|
+
sessionsDir: paths.sessionsDir,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (filter?.project) {
|
|
145
|
+
const f = filter.project.toLowerCase();
|
|
146
|
+
sessions = sessions.filter((s) => s.projectName.toLowerCase().includes(f));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Promise.all(
|
|
150
|
+
sessions.map((s) =>
|
|
151
|
+
peekSession(provider, s, {
|
|
152
|
+
projectsDir: paths.projectsDir,
|
|
153
|
+
sessionsDir: paths.sessionsDir,
|
|
154
|
+
}, privacy),
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async detail(sessionId: string, projectPath?: string): Promise<SessionDetail> {
|
|
160
|
+
let session: SessionInfo | undefined;
|
|
161
|
+
|
|
162
|
+
if (!projectPath || provider === "codex") {
|
|
163
|
+
const all = await readHistory(
|
|
164
|
+
paths.historyFile,
|
|
165
|
+
"2000-01-01",
|
|
166
|
+
"2099-12-31",
|
|
167
|
+
privacy,
|
|
168
|
+
{
|
|
169
|
+
provider,
|
|
170
|
+
sessionsDir: paths.sessionsDir,
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
session = all.find((s) => s.sessionId === sessionId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!session) {
|
|
177
|
+
const effectiveProjectPath = projectPath ?? `(unknown)/${sessionId}`;
|
|
178
|
+
session = {
|
|
179
|
+
sessionId,
|
|
180
|
+
project: effectiveProjectPath,
|
|
181
|
+
projectName:
|
|
182
|
+
effectiveProjectPath.split("/").pop() || effectiveProjectPath,
|
|
183
|
+
prompts: [],
|
|
184
|
+
promptTimestamps: [],
|
|
185
|
+
timeRange: { start: 0, end: 0 },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return parseSessionDetail(provider, session, {
|
|
190
|
+
projectsDir: paths.projectsDir,
|
|
191
|
+
sessionsDir: paths.sessionsDir,
|
|
192
|
+
}, privacy);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
async *transcript(
|
|
196
|
+
sessionId: string,
|
|
197
|
+
projectPath?: string,
|
|
198
|
+
): AsyncGenerator<TranscriptEntry> {
|
|
199
|
+
yield* streamTranscript(
|
|
200
|
+
provider,
|
|
201
|
+
sessionId,
|
|
202
|
+
projectPath ?? `(unknown)/${sessionId}`,
|
|
203
|
+
{
|
|
204
|
+
projectsDir: paths.projectsDir,
|
|
205
|
+
sessionsDir: paths.sessionsDir,
|
|
206
|
+
},
|
|
207
|
+
privacy,
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
async count(filter?: SessionListFilter): Promise<number> {
|
|
212
|
+
const { from, to } = resolveDateRange(filter);
|
|
213
|
+
const sessions = await readHistory(paths.historyFile, from, to, privacy, {
|
|
214
|
+
provider,
|
|
215
|
+
sessionsDir: paths.sessionsDir,
|
|
216
|
+
});
|
|
217
|
+
if (filter?.project) {
|
|
218
|
+
const f = filter.project.toLowerCase();
|
|
219
|
+
return sessions.filter((s) => s.projectName.toLowerCase().includes(f)).length;
|
|
220
|
+
}
|
|
221
|
+
return sessions.length;
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
projects: {
|
|
226
|
+
async list(): Promise<ProjectInfo[]> {
|
|
227
|
+
return readProjects(paths.projectsDir, privacy);
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async memory(projectPath: string): Promise<ProjectMemory | null> {
|
|
231
|
+
return readProjectMemory(projectPath, paths.projectsDir);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
tasks: {
|
|
236
|
+
async list(filter: DateFilter): Promise<TaskInfo[]> {
|
|
237
|
+
const { from, to } = resolveDateRange(filter);
|
|
238
|
+
return readTasks(paths.tasksDir, from, to);
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
todos: {
|
|
243
|
+
async list(filter: DateFilter): Promise<TodoItem[]> {
|
|
244
|
+
const { from, to } = resolveDateRange(filter);
|
|
245
|
+
return readTodos(paths.todosDir, from, to);
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
plans: {
|
|
250
|
+
async list(filter: DateFilter): Promise<PlanInfo[]> {
|
|
251
|
+
const { from, to } = resolveDateRange(filter);
|
|
252
|
+
return readPlans(paths.plansDir, from, to);
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
skills: {
|
|
257
|
+
async list(): Promise<string[]> {
|
|
258
|
+
return readSkills(paths.skillsDir);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async read(name: string): Promise<string> {
|
|
262
|
+
return readSkillContent(paths.skillsDir, name);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
stats: {
|
|
267
|
+
async get(): Promise<StatsCache | null> {
|
|
268
|
+
return readStats(paths.statsCache);
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
aggregate: {
|
|
273
|
+
async daily(date: string): Promise<DailySummary> {
|
|
274
|
+
return buildDailySummary(provider, date, dailyPaths, privacy);
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async dailyRange(from: string, to: string): Promise<DailySummary[]> {
|
|
278
|
+
return buildDailyRange(provider, from, to, dailyPaths, privacy);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async byProject(filter?: SessionListFilter): Promise<ProjectSummary[]> {
|
|
282
|
+
return buildProjectSummaries(
|
|
283
|
+
provider,
|
|
284
|
+
filter ?? {},
|
|
285
|
+
paths.historyFile,
|
|
286
|
+
{ projectsDir: paths.projectsDir, sessionsDir: paths.sessionsDir },
|
|
287
|
+
privacy,
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
async toolUsage(filter?: SessionListFilter): Promise<ToolUsageReport> {
|
|
292
|
+
return buildToolUsageReport(
|
|
293
|
+
provider,
|
|
294
|
+
filter ?? {},
|
|
295
|
+
paths.historyFile,
|
|
296
|
+
{ projectsDir: paths.projectsDir, sessionsDir: paths.sessionsDir },
|
|
297
|
+
privacy,
|
|
298
|
+
);
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
estimateHours(sessions: SessionInfo[]): number {
|
|
302
|
+
return estimateHours(sessions);
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Backward-compatible Claude-specific config. */
|
|
309
|
+
export interface ClaudeHistoryConfig extends Omit<HistoryConfig, "provider" | "providerDir"> {
|
|
310
|
+
/** Path to ~/.claude directory. Defaults to ~/.claude */
|
|
311
|
+
claudeDir?: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Backward-compatible Claude-specific alias. */
|
|
315
|
+
export type ClaudeHistory = History;
|
|
316
|
+
|
|
317
|
+
/** Create a ClaudeHistory instance for reading session data. */
|
|
318
|
+
export function createClaudeHistory(config?: ClaudeHistoryConfig): ClaudeHistory {
|
|
319
|
+
const { claudeDir, ...rest } = config ?? {};
|
|
320
|
+
return createHistory({
|
|
321
|
+
...rest,
|
|
322
|
+
provider: "claude",
|
|
323
|
+
providerDir: claudeDir,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { PrivacyConfig } from "../types/privacy.js";
|
|
2
|
+
import type { Provider } from "../types/provider.js";
|
|
3
|
+
import type { DailySummary } from "../types/aggregations.js";
|
|
4
|
+
import { readHistory } from "../readers/history-reader.js";
|
|
5
|
+
import { readTasks, readTodos } from "../readers/task-reader.js";
|
|
6
|
+
import { readPlans } from "../readers/plan-reader.js";
|
|
7
|
+
import { readProjectMemories } from "../readers/project-reader.js";
|
|
8
|
+
import { parseSessions } from "../parsers/session-detail.js";
|
|
9
|
+
|
|
10
|
+
interface DailyPaths {
|
|
11
|
+
historyFile: string;
|
|
12
|
+
projectsDir: string;
|
|
13
|
+
sessionsDir: string;
|
|
14
|
+
tasksDir: string;
|
|
15
|
+
plansDir: string;
|
|
16
|
+
todosDir: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Build a complete DailySummary for a single date. */
|
|
20
|
+
export async function buildDailySummary(
|
|
21
|
+
provider: Provider,
|
|
22
|
+
date: string,
|
|
23
|
+
paths: DailyPaths,
|
|
24
|
+
privacy: PrivacyConfig,
|
|
25
|
+
): Promise<DailySummary> {
|
|
26
|
+
// Read history (fast)
|
|
27
|
+
const sessionInfos = await readHistory(paths.historyFile, date, date, privacy, {
|
|
28
|
+
provider,
|
|
29
|
+
sessionsDir: paths.sessionsDir,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Parse sessions into detailed + short
|
|
33
|
+
const { detailed, short } = await parseSessions(
|
|
34
|
+
provider,
|
|
35
|
+
sessionInfos,
|
|
36
|
+
{ projectsDir: paths.projectsDir, sessionsDir: paths.sessionsDir },
|
|
37
|
+
privacy,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Read tasks, plans, todos in parallel
|
|
41
|
+
const [tasks, plans, todos] = await Promise.all([
|
|
42
|
+
readTasks(paths.tasksDir, date, date),
|
|
43
|
+
readPlans(paths.plansDir, date, date),
|
|
44
|
+
readTodos(paths.todosDir, date, date),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// Read project memories
|
|
48
|
+
const projectPaths = [...new Set(sessionInfos.map((s) => s.project))];
|
|
49
|
+
const projectMemory = await readProjectMemories(projectPaths, paths.projectsDir);
|
|
50
|
+
|
|
51
|
+
const projects = [...new Set(sessionInfos.map((s) => s.projectName))];
|
|
52
|
+
const totalPrompts = sessionInfos.reduce((sum, s) => sum + s.prompts.length, 0);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
date,
|
|
56
|
+
sessions: detailed,
|
|
57
|
+
shortSessions: short,
|
|
58
|
+
tasks,
|
|
59
|
+
plans,
|
|
60
|
+
todos,
|
|
61
|
+
totalPrompts,
|
|
62
|
+
totalSessions: sessionInfos.length,
|
|
63
|
+
projects,
|
|
64
|
+
projectMemory,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Build DailySummary objects for a date range. */
|
|
69
|
+
export async function buildDailyRange(
|
|
70
|
+
provider: Provider,
|
|
71
|
+
from: string,
|
|
72
|
+
to: string,
|
|
73
|
+
paths: DailyPaths,
|
|
74
|
+
privacy: PrivacyConfig,
|
|
75
|
+
): Promise<DailySummary[]> {
|
|
76
|
+
const summaries: DailySummary[] = [];
|
|
77
|
+
const start = new Date(from);
|
|
78
|
+
const end = new Date(to);
|
|
79
|
+
|
|
80
|
+
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
81
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
82
|
+
const summary = await buildDailySummary(provider, dateStr, paths, privacy);
|
|
83
|
+
// Only include days that have activity
|
|
84
|
+
if (summary.totalSessions > 0 || summary.tasks.length > 0 || summary.plans.length > 0) {
|
|
85
|
+
summaries.push(summary);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return summaries;
|
|
90
|
+
}
|