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,280 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { PrivacyConfig } from "../types/privacy.js";
|
|
3
|
+
import type { Provider } from "../types/provider.js";
|
|
4
|
+
import type { SessionInfo, SessionMeta } from "../types/session.js";
|
|
5
|
+
import type { ContentBlock, TranscriptEntry } from "../types/transcript.js";
|
|
6
|
+
import { encodeProjectPath } from "../utils/paths.js";
|
|
7
|
+
import { canonicalProvider } from "../utils/providers.js";
|
|
8
|
+
import { filterTranscriptEntry } from "../privacy/redact.js";
|
|
9
|
+
import {
|
|
10
|
+
findRolloutFile,
|
|
11
|
+
parseCodexMessageText,
|
|
12
|
+
parseCodexToolArguments,
|
|
13
|
+
} from "./codex-rollout-reader.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Peek session metadata from a session JSONL file.
|
|
17
|
+
* Reads the entire file but only extracts lightweight metadata.
|
|
18
|
+
* This is the "medium" tier — slower than history.jsonl only, but still avoids full parsing.
|
|
19
|
+
*/
|
|
20
|
+
export async function peekSession(
|
|
21
|
+
provider: Provider,
|
|
22
|
+
session: SessionInfo,
|
|
23
|
+
paths: { projectsDir: string; sessionsDir: string },
|
|
24
|
+
privacy: PrivacyConfig,
|
|
25
|
+
): Promise<SessionMeta> {
|
|
26
|
+
const normalized = canonicalProvider(provider);
|
|
27
|
+
if (normalized === "codex") {
|
|
28
|
+
return peekCodexSession(session, paths.sessionsDir);
|
|
29
|
+
}
|
|
30
|
+
return peekClaudeSession(session, paths.projectsDir);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function peekClaudeSession(
|
|
34
|
+
session: SessionInfo,
|
|
35
|
+
projectsDir: string,
|
|
36
|
+
): Promise<SessionMeta> {
|
|
37
|
+
const meta: SessionMeta = {
|
|
38
|
+
...session,
|
|
39
|
+
totalInputTokens: 0,
|
|
40
|
+
totalOutputTokens: 0,
|
|
41
|
+
cacheCreationInputTokens: 0,
|
|
42
|
+
cacheReadInputTokens: 0,
|
|
43
|
+
messageCount: 0,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const encoded = encodeProjectPath(session.project);
|
|
47
|
+
const filePath = join(projectsDir, encoded, `${session.sessionId}.jsonl`);
|
|
48
|
+
const file = Bun.file(filePath);
|
|
49
|
+
if (!(await file.exists())) return meta;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const text = await file.text();
|
|
53
|
+
for (const line of text.split("\n")) {
|
|
54
|
+
if (!line.trim()) continue;
|
|
55
|
+
try {
|
|
56
|
+
const entry = JSON.parse(line) as TranscriptEntry;
|
|
57
|
+
|
|
58
|
+
// Extract git branch from first occurrence
|
|
59
|
+
if (!meta.gitBranch && entry.gitBranch && entry.gitBranch !== "HEAD") {
|
|
60
|
+
meta.gitBranch = entry.gitBranch;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract model from first assistant message
|
|
64
|
+
if (!meta.model && entry.message?.model) {
|
|
65
|
+
meta.model = entry.message.model;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Accumulate token usage
|
|
69
|
+
const usage = entry.message?.usage;
|
|
70
|
+
if (usage) {
|
|
71
|
+
meta.totalInputTokens += usage.input_tokens ?? 0;
|
|
72
|
+
meta.totalOutputTokens += usage.output_tokens ?? 0;
|
|
73
|
+
meta.cacheCreationInputTokens += usage.cache_creation_input_tokens ?? 0;
|
|
74
|
+
meta.cacheReadInputTokens += usage.cache_read_input_tokens ?? 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Count messages (user + assistant only)
|
|
78
|
+
if (entry.message?.role === "user" || entry.message?.role === "assistant") {
|
|
79
|
+
meta.messageCount++;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// skip malformed
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// file unreadable
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return meta;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function peekCodexSession(
|
|
93
|
+
session: SessionInfo,
|
|
94
|
+
sessionsDir: string,
|
|
95
|
+
): Promise<SessionMeta> {
|
|
96
|
+
const meta: SessionMeta = {
|
|
97
|
+
...session,
|
|
98
|
+
totalInputTokens: 0,
|
|
99
|
+
totalOutputTokens: 0,
|
|
100
|
+
cacheCreationInputTokens: 0,
|
|
101
|
+
cacheReadInputTokens: 0,
|
|
102
|
+
messageCount: 0,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const rolloutPath = await findRolloutFile(sessionsDir, session.sessionId);
|
|
106
|
+
if (!rolloutPath) return meta;
|
|
107
|
+
|
|
108
|
+
const file = Bun.file(rolloutPath);
|
|
109
|
+
if (!(await file.exists())) return meta;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const text = await file.text();
|
|
113
|
+
for (const line of text.split("\n")) {
|
|
114
|
+
if (!line.trim()) continue;
|
|
115
|
+
try {
|
|
116
|
+
const entry = JSON.parse(line) as any;
|
|
117
|
+
|
|
118
|
+
if (entry.type === "session_meta") {
|
|
119
|
+
const branch = entry.payload?.git?.branch;
|
|
120
|
+
if (!meta.gitBranch && typeof branch === "string" && branch !== "HEAD") {
|
|
121
|
+
meta.gitBranch = branch;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (entry.type === "turn_context") {
|
|
126
|
+
const model = entry.payload?.model;
|
|
127
|
+
if (!meta.model && typeof model === "string") {
|
|
128
|
+
meta.model = model;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (entry.type === "event_msg" && entry.payload?.type === "token_count") {
|
|
133
|
+
const usage = entry.payload?.info?.last_token_usage;
|
|
134
|
+
if (usage && typeof usage === "object") {
|
|
135
|
+
meta.totalInputTokens += Number(usage.input_tokens ?? 0);
|
|
136
|
+
meta.totalOutputTokens += Number(usage.output_tokens ?? 0);
|
|
137
|
+
meta.cacheReadInputTokens += Number(usage.cached_input_tokens ?? 0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
entry.type === "response_item" &&
|
|
143
|
+
entry.payload?.type === "message" &&
|
|
144
|
+
(entry.payload?.role === "user" || entry.payload?.role === "assistant")
|
|
145
|
+
) {
|
|
146
|
+
meta.messageCount++;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// skip malformed
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// file unreadable
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return meta;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Stream transcript entries from a session JSONL file with privacy filtering.
|
|
161
|
+
*/
|
|
162
|
+
export async function* streamTranscript(
|
|
163
|
+
provider: Provider,
|
|
164
|
+
sessionId: string,
|
|
165
|
+
projectPath: string,
|
|
166
|
+
paths: { projectsDir: string; sessionsDir: string },
|
|
167
|
+
privacy: PrivacyConfig,
|
|
168
|
+
): AsyncGenerator<TranscriptEntry> {
|
|
169
|
+
const normalized = canonicalProvider(provider);
|
|
170
|
+
if (normalized === "codex") {
|
|
171
|
+
yield* streamCodexTranscript(sessionId, paths.sessionsDir, privacy);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
yield* streamClaudeTranscript(sessionId, projectPath, paths.projectsDir, privacy);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function* streamClaudeTranscript(
|
|
179
|
+
sessionId: string,
|
|
180
|
+
projectPath: string,
|
|
181
|
+
projectsDir: string,
|
|
182
|
+
privacy: PrivacyConfig,
|
|
183
|
+
): AsyncGenerator<TranscriptEntry> {
|
|
184
|
+
const encoded = encodeProjectPath(projectPath);
|
|
185
|
+
const filePath = join(projectsDir, encoded, `${sessionId}.jsonl`);
|
|
186
|
+
const file = Bun.file(filePath);
|
|
187
|
+
if (!(await file.exists())) return;
|
|
188
|
+
|
|
189
|
+
const text = await file.text();
|
|
190
|
+
for (const line of text.split("\n")) {
|
|
191
|
+
if (!line.trim()) continue;
|
|
192
|
+
try {
|
|
193
|
+
const entry = JSON.parse(line) as TranscriptEntry;
|
|
194
|
+
const filtered = filterTranscriptEntry(entry, privacy);
|
|
195
|
+
if (filtered) yield filtered;
|
|
196
|
+
} catch {
|
|
197
|
+
// skip malformed
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function* streamCodexTranscript(
|
|
203
|
+
sessionId: string,
|
|
204
|
+
sessionsDir: string,
|
|
205
|
+
privacy: PrivacyConfig,
|
|
206
|
+
): AsyncGenerator<TranscriptEntry> {
|
|
207
|
+
const rolloutPath = await findRolloutFile(sessionsDir, sessionId);
|
|
208
|
+
if (!rolloutPath) return;
|
|
209
|
+
|
|
210
|
+
const file = Bun.file(rolloutPath);
|
|
211
|
+
if (!(await file.exists())) return;
|
|
212
|
+
|
|
213
|
+
let currentModel: string | undefined;
|
|
214
|
+
|
|
215
|
+
const text = await file.text();
|
|
216
|
+
for (const line of text.split("\n")) {
|
|
217
|
+
if (!line.trim()) continue;
|
|
218
|
+
let raw: any;
|
|
219
|
+
try {
|
|
220
|
+
raw = JSON.parse(line);
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (raw.type === "turn_context" && typeof raw.payload?.model === "string") {
|
|
226
|
+
currentModel = raw.payload.model;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let mapped: TranscriptEntry | null = null;
|
|
231
|
+
|
|
232
|
+
if (raw.type === "response_item" && raw.payload?.type === "message") {
|
|
233
|
+
const role = raw.payload.role;
|
|
234
|
+
if (role === "user" || role === "assistant") {
|
|
235
|
+
const textContent = parseCodexMessageText(raw.payload.content);
|
|
236
|
+
mapped = {
|
|
237
|
+
timestamp: raw.timestamp,
|
|
238
|
+
message: {
|
|
239
|
+
role,
|
|
240
|
+
model: currentModel,
|
|
241
|
+
content: textContent,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
} else if (
|
|
246
|
+
raw.type === "response_item" &&
|
|
247
|
+
raw.payload?.type === "function_call" &&
|
|
248
|
+
typeof raw.payload?.name === "string"
|
|
249
|
+
) {
|
|
250
|
+
const args = parseCodexToolArguments(raw.payload.arguments);
|
|
251
|
+
const content: ContentBlock[] = [
|
|
252
|
+
{
|
|
253
|
+
type: "tool_use",
|
|
254
|
+
name: raw.payload.name,
|
|
255
|
+
input: args,
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
mapped = {
|
|
259
|
+
timestamp: raw.timestamp,
|
|
260
|
+
message: {
|
|
261
|
+
role: "assistant",
|
|
262
|
+
model: currentModel,
|
|
263
|
+
content,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
} else if (
|
|
267
|
+
raw.type === "response_item" &&
|
|
268
|
+
raw.payload?.type === "function_call_output"
|
|
269
|
+
) {
|
|
270
|
+
mapped = {
|
|
271
|
+
timestamp: raw.timestamp,
|
|
272
|
+
toolUseResult: raw.payload?.output,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!mapped) continue;
|
|
277
|
+
const filtered = filterTranscriptEntry(mapped, privacy);
|
|
278
|
+
if (filtered) yield filtered;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** List all skill names from provider/skills/ */
|
|
4
|
+
export async function readSkills(skillsDir: string): Promise<string[]> {
|
|
5
|
+
const glob = new Bun.Glob("*/SKILL.md");
|
|
6
|
+
const skills: string[] = [];
|
|
7
|
+
try {
|
|
8
|
+
for await (const path of glob.scan({ cwd: skillsDir })) {
|
|
9
|
+
skills.push(path.split("/")[0]);
|
|
10
|
+
}
|
|
11
|
+
} catch {
|
|
12
|
+
// skills dir may not exist
|
|
13
|
+
}
|
|
14
|
+
return skills;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Read a specific skill's SKILL.md content. */
|
|
18
|
+
export async function readSkillContent(
|
|
19
|
+
skillsDir: string,
|
|
20
|
+
name: string,
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const filePath = join(skillsDir, name, "SKILL.md");
|
|
23
|
+
const file = Bun.file(filePath);
|
|
24
|
+
if (!(await file.exists())) {
|
|
25
|
+
throw new Error(`Skill not found: ${name}`);
|
|
26
|
+
}
|
|
27
|
+
return file.text();
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StatsCache } from "../types/stats.js";
|
|
2
|
+
|
|
3
|
+
/** Read the pre-computed stats cache from provider/stats-cache.json */
|
|
4
|
+
export async function readStats(statsPath: string): Promise<StatsCache | null> {
|
|
5
|
+
const file = Bun.file(statsPath);
|
|
6
|
+
if (!(await file.exists())) return null;
|
|
7
|
+
try {
|
|
8
|
+
return (await file.json()) as StatsCache;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { TaskInfo, TodoItem } from "../types/task.js";
|
|
4
|
+
|
|
5
|
+
function isSameDate(fileDate: Date, targetDate: string): boolean {
|
|
6
|
+
const year = fileDate.getFullYear();
|
|
7
|
+
const month = String(fileDate.getMonth() + 1).padStart(2, "0");
|
|
8
|
+
const day = String(fileDate.getDate()).padStart(2, "0");
|
|
9
|
+
return `${year}-${month}-${day}` === targetDate;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isInDateRange(fileDate: Date, from: string, to: string): boolean {
|
|
13
|
+
const year = fileDate.getFullYear();
|
|
14
|
+
const month = String(fileDate.getMonth() + 1).padStart(2, "0");
|
|
15
|
+
const day = String(fileDate.getDate()).padStart(2, "0");
|
|
16
|
+
const dateStr = `${year}-${month}-${day}`;
|
|
17
|
+
return dateStr >= from && dateStr <= to;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Read tasks from provider/tasks/{sessionDir}/*.json */
|
|
21
|
+
export async function readTasks(
|
|
22
|
+
tasksDir: string,
|
|
23
|
+
from: string,
|
|
24
|
+
to: string,
|
|
25
|
+
): Promise<TaskInfo[]> {
|
|
26
|
+
const tasks: TaskInfo[] = [];
|
|
27
|
+
|
|
28
|
+
let sessionDirs: string[];
|
|
29
|
+
try {
|
|
30
|
+
sessionDirs = await readdir(tasksDir);
|
|
31
|
+
} catch {
|
|
32
|
+
return tasks;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const sessionDir of sessionDirs) {
|
|
36
|
+
const dirPath = join(tasksDir, sessionDir);
|
|
37
|
+
const dirStat = await stat(dirPath).catch(() => null);
|
|
38
|
+
if (!dirStat?.isDirectory()) continue;
|
|
39
|
+
|
|
40
|
+
let files: string[];
|
|
41
|
+
try {
|
|
42
|
+
files = await readdir(dirPath);
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
if (!file.endsWith(".json")) continue;
|
|
49
|
+
const filePath = join(dirPath, file);
|
|
50
|
+
|
|
51
|
+
const fileStat = await stat(filePath).catch(() => null);
|
|
52
|
+
if (!fileStat || !isInDateRange(fileStat.mtime, from, to)) continue;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const content = await Bun.file(filePath).json();
|
|
56
|
+
if (
|
|
57
|
+
content.subject &&
|
|
58
|
+
(content.status === "completed" || content.status === "in_progress")
|
|
59
|
+
) {
|
|
60
|
+
tasks.push({
|
|
61
|
+
id: content.id,
|
|
62
|
+
subject: content.subject,
|
|
63
|
+
description: content.description || "",
|
|
64
|
+
status: content.status,
|
|
65
|
+
sessionDir,
|
|
66
|
+
blocks: content.blocks,
|
|
67
|
+
blockedBy: content.blockedBy,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// skip malformed
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return tasks;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Read todos from provider/todos/*.json */
|
|
80
|
+
export async function readTodos(
|
|
81
|
+
todosDir: string,
|
|
82
|
+
from: string,
|
|
83
|
+
to: string,
|
|
84
|
+
): Promise<TodoItem[]> {
|
|
85
|
+
const todos: TodoItem[] = [];
|
|
86
|
+
|
|
87
|
+
let files: string[];
|
|
88
|
+
try {
|
|
89
|
+
files = await readdir(todosDir);
|
|
90
|
+
} catch {
|
|
91
|
+
return todos;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
if (!file.endsWith(".json")) continue;
|
|
96
|
+
const filePath = join(todosDir, file);
|
|
97
|
+
|
|
98
|
+
const fileStat = await stat(filePath).catch(() => null);
|
|
99
|
+
if (!fileStat || !isInDateRange(fileStat.mtime, from, to)) continue;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const content = await Bun.file(filePath).json();
|
|
103
|
+
if (content.content) {
|
|
104
|
+
todos.push({
|
|
105
|
+
id: content.id || file.replace(".json", ""),
|
|
106
|
+
content: content.content,
|
|
107
|
+
status: content.status || "unknown",
|
|
108
|
+
sessionDir: content.sessionDir || "",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// skip malformed
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return todos;
|
|
117
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PlanInfo } from "./plan.js";
|
|
2
|
+
import type { ProjectMemory } from "./project.js";
|
|
3
|
+
import type { SessionDetail, SessionInfo, ToolCallSummary, ToolCategory } from "./session.js";
|
|
4
|
+
import type { TaskInfo, TodoItem } from "./task.js";
|
|
5
|
+
|
|
6
|
+
export interface DailySummary {
|
|
7
|
+
date: string;
|
|
8
|
+
sessions: SessionDetail[];
|
|
9
|
+
shortSessions: SessionInfo[];
|
|
10
|
+
tasks: TaskInfo[];
|
|
11
|
+
plans: PlanInfo[];
|
|
12
|
+
todos: TodoItem[];
|
|
13
|
+
totalPrompts: number;
|
|
14
|
+
totalSessions: number;
|
|
15
|
+
projects: string[];
|
|
16
|
+
projectMemory: Map<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProjectSummary {
|
|
20
|
+
project: string;
|
|
21
|
+
projectName: string;
|
|
22
|
+
sessionCount: number;
|
|
23
|
+
promptCount: number;
|
|
24
|
+
estimatedHours: number;
|
|
25
|
+
branches: string[];
|
|
26
|
+
filesReferenced: string[];
|
|
27
|
+
toolCalls: ToolCallSummary[];
|
|
28
|
+
models: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ToolUsageReport {
|
|
32
|
+
byTool: Map<string, number>;
|
|
33
|
+
byCategory: Map<ToolCategory, number>;
|
|
34
|
+
topFiles: Array<{ path: string; count: number }>;
|
|
35
|
+
topCommands: Array<{ command: string; count: number }>;
|
|
36
|
+
total: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DateFilter {
|
|
40
|
+
date?: string;
|
|
41
|
+
from?: string;
|
|
42
|
+
to?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SessionListFilter extends DateFilter {
|
|
46
|
+
project?: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PrivacyConfig {
|
|
2
|
+
/** Replace user prompt text with [redacted] */
|
|
3
|
+
redactPrompts: boolean;
|
|
4
|
+
/** Strip absolute paths to relative */
|
|
5
|
+
redactAbsolutePaths: boolean;
|
|
6
|
+
/** Replace $HOME with ~ */
|
|
7
|
+
redactHomeDir: boolean;
|
|
8
|
+
/** Remove thinking blocks entirely */
|
|
9
|
+
stripThinking: boolean;
|
|
10
|
+
/** Skip toolUseResult content (DEFAULT: true) */
|
|
11
|
+
stripToolResults: boolean;
|
|
12
|
+
/** Custom regex patterns to redact (emails, API keys, etc.) */
|
|
13
|
+
redactPatterns: RegExp[];
|
|
14
|
+
/** Skip these projects entirely */
|
|
15
|
+
excludeProjects: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PrivacyProfile = "local" | "shareable" | "strict";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** One line from history.jsonl — a single user prompt. */
|
|
2
|
+
export interface HistoryEntry {
|
|
3
|
+
display: string;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
project: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
pastedContents?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Lightweight session info derived from history.jsonl grouping only. Fast — no file reads. */
|
|
11
|
+
export interface SessionInfo {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
project: string;
|
|
14
|
+
projectName: string;
|
|
15
|
+
prompts: string[];
|
|
16
|
+
promptTimestamps: number[];
|
|
17
|
+
timeRange: { start: number; end: number };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Session with metadata peeked from the session JSONL file (first+last lines). */
|
|
21
|
+
export interface SessionMeta extends SessionInfo {
|
|
22
|
+
gitBranch?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
totalInputTokens: number;
|
|
25
|
+
totalOutputTokens: number;
|
|
26
|
+
cacheCreationInputTokens: number;
|
|
27
|
+
cacheReadInputTokens: number;
|
|
28
|
+
messageCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Full session detail from parsing the entire session JSONL file. */
|
|
32
|
+
export interface SessionDetail extends SessionMeta {
|
|
33
|
+
assistantSummaries: string[];
|
|
34
|
+
toolCalls: ToolCallSummary[];
|
|
35
|
+
filesReferenced: string[];
|
|
36
|
+
planReferenced: boolean;
|
|
37
|
+
thinkingBlockCount: number;
|
|
38
|
+
hasSidechains: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ToolCategory =
|
|
42
|
+
| "file_read"
|
|
43
|
+
| "file_write"
|
|
44
|
+
| "shell"
|
|
45
|
+
| "search"
|
|
46
|
+
| "web"
|
|
47
|
+
| "task"
|
|
48
|
+
| "other";
|
|
49
|
+
|
|
50
|
+
export interface ToolCallSummary {
|
|
51
|
+
name: string;
|
|
52
|
+
displayName: string;
|
|
53
|
+
category: ToolCategory;
|
|
54
|
+
/** e.g. file_path for Read/Write, command for Bash */
|
|
55
|
+
target?: string;
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Pre-computed stats from provider/stats-cache.json */
|
|
2
|
+
export interface StatsCache {
|
|
3
|
+
version: number;
|
|
4
|
+
lastComputedDate: string;
|
|
5
|
+
dailyActivity: Array<{
|
|
6
|
+
date: string;
|
|
7
|
+
messageCount: number;
|
|
8
|
+
sessionCount: number;
|
|
9
|
+
toolCallCount: number;
|
|
10
|
+
}>;
|
|
11
|
+
totalSessions: number;
|
|
12
|
+
totalMessages: number;
|
|
13
|
+
hourCounts: Record<string, number>;
|
|
14
|
+
modelUsage?: Record<string, number>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TaskInfo {
|
|
2
|
+
id: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
description: string;
|
|
5
|
+
status: string;
|
|
6
|
+
sessionDir: string;
|
|
7
|
+
blocks?: string[];
|
|
8
|
+
blockedBy?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TodoItem {
|
|
12
|
+
id: string;
|
|
13
|
+
content: string;
|
|
14
|
+
status: string;
|
|
15
|
+
sessionDir: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** A single content block inside an assistant message. */
|
|
2
|
+
export interface ContentBlock {
|
|
3
|
+
type: "text" | "thinking" | "tool_use" | "tool_result";
|
|
4
|
+
text?: string;
|
|
5
|
+
thinking?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
tool_use_id?: string;
|
|
9
|
+
input?: Record<string, unknown>;
|
|
10
|
+
content?: string | ContentBlock[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Raw line from a session JSONL file. Union of all possible shapes. */
|
|
14
|
+
export interface TranscriptEntry {
|
|
15
|
+
type?: "user" | "assistant" | "progress" | "file-history-snapshot";
|
|
16
|
+
message?: {
|
|
17
|
+
role?: "user" | "assistant";
|
|
18
|
+
content?: string | ContentBlock[];
|
|
19
|
+
model?: string;
|
|
20
|
+
usage?: {
|
|
21
|
+
input_tokens?: number;
|
|
22
|
+
output_tokens?: number;
|
|
23
|
+
cache_creation_input_tokens?: number;
|
|
24
|
+
cache_read_input_tokens?: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
timestamp?: string;
|
|
28
|
+
gitBranch?: string;
|
|
29
|
+
planContent?: string;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
isSidechain?: boolean;
|
|
33
|
+
parentUuid?: string;
|
|
34
|
+
uuid?: string;
|
|
35
|
+
toolUseResult?: unknown;
|
|
36
|
+
}
|