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,71 @@
|
|
|
1
|
+
import type { PrivacyConfig } from "../types/privacy.js";
|
|
2
|
+
import type { Provider } from "../types/provider.js";
|
|
3
|
+
import type { ProjectSummary, SessionListFilter } from "../types/aggregations.js";
|
|
4
|
+
import type { SessionInfo, ToolCallSummary } from "../types/session.js";
|
|
5
|
+
import { readHistory } from "../readers/history-reader.js";
|
|
6
|
+
import { parseSessionDetail } from "../parsers/session-detail.js";
|
|
7
|
+
import { resolveDateRange } from "../utils/dates.js";
|
|
8
|
+
import { projectName } from "../utils/paths.js";
|
|
9
|
+
import { estimateHours } from "./time.js";
|
|
10
|
+
|
|
11
|
+
/** Build per-project summaries from session data. */
|
|
12
|
+
export async function buildProjectSummaries(
|
|
13
|
+
provider: Provider,
|
|
14
|
+
filter: SessionListFilter,
|
|
15
|
+
historyFile: string,
|
|
16
|
+
paths: { projectsDir: string; sessionsDir: string },
|
|
17
|
+
privacy: PrivacyConfig,
|
|
18
|
+
): Promise<ProjectSummary[]> {
|
|
19
|
+
const { from, to } = resolveDateRange(filter);
|
|
20
|
+
const sessions = await readHistory(historyFile, from, to, privacy, {
|
|
21
|
+
provider,
|
|
22
|
+
sessionsDir: paths.sessionsDir,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Group by project
|
|
26
|
+
const byProject = new Map<string, SessionInfo[]>();
|
|
27
|
+
for (const session of sessions) {
|
|
28
|
+
const name = session.projectName;
|
|
29
|
+
if (filter.project && !name.toLowerCase().includes(filter.project.toLowerCase())) continue;
|
|
30
|
+
const existing = byProject.get(name);
|
|
31
|
+
if (existing) {
|
|
32
|
+
existing.push(session);
|
|
33
|
+
} else {
|
|
34
|
+
byProject.set(name, [session]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const summaries: ProjectSummary[] = [];
|
|
39
|
+
|
|
40
|
+
for (const [name, projectSessions] of byProject) {
|
|
41
|
+
const allToolCalls: ToolCallSummary[] = [];
|
|
42
|
+
const allFiles: string[] = [];
|
|
43
|
+
const allBranches: string[] = [];
|
|
44
|
+
const allModels: string[] = [];
|
|
45
|
+
|
|
46
|
+
// Parse detailed sessions for rich data
|
|
47
|
+
for (const session of projectSessions) {
|
|
48
|
+
if (session.prompts.length >= 3) {
|
|
49
|
+
const detail = await parseSessionDetail(provider, session, paths, privacy);
|
|
50
|
+
allToolCalls.push(...detail.toolCalls);
|
|
51
|
+
allFiles.push(...detail.filesReferenced);
|
|
52
|
+
if (detail.gitBranch) allBranches.push(detail.gitBranch);
|
|
53
|
+
if (detail.model) allModels.push(detail.model);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
summaries.push({
|
|
58
|
+
project: projectSessions[0].project,
|
|
59
|
+
projectName: name,
|
|
60
|
+
sessionCount: projectSessions.length,
|
|
61
|
+
promptCount: projectSessions.reduce((sum, s) => sum + s.prompts.length, 0),
|
|
62
|
+
estimatedHours: estimateHours(projectSessions),
|
|
63
|
+
branches: [...new Set(allBranches)],
|
|
64
|
+
filesReferenced: [...new Set(allFiles)],
|
|
65
|
+
toolCalls: allToolCalls,
|
|
66
|
+
models: [...new Set(allModels)],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return summaries.sort((a, b) => b.promptCount - a.promptCount);
|
|
71
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SessionInfo } from "../types/session.js";
|
|
2
|
+
|
|
3
|
+
/** Gap cap in ms — if gap between consecutive prompts exceeds this, cap it. */
|
|
4
|
+
const GAP_CAP_MS = 15 * 60 * 1000; // 15 minutes
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Estimate hours of active work from session timestamp data.
|
|
8
|
+
* Merges all timestamps across sessions into a single sorted timeline,
|
|
9
|
+
* then applies gap-capping. This deduplicates overlapping intervals so
|
|
10
|
+
* concurrent sessions (e.g. multiple terminal tabs) share wall-clock time
|
|
11
|
+
* instead of stacking it.
|
|
12
|
+
*/
|
|
13
|
+
export function estimateHours(sessions: SessionInfo[]): number {
|
|
14
|
+
if (sessions.length === 0) return 0;
|
|
15
|
+
|
|
16
|
+
// Merge all timestamps across sessions into a single timeline
|
|
17
|
+
const allTimestamps: number[] = [];
|
|
18
|
+
|
|
19
|
+
for (const session of sessions) {
|
|
20
|
+
if (session.promptTimestamps.length > 0) {
|
|
21
|
+
allTimestamps.push(...session.promptTimestamps);
|
|
22
|
+
} else {
|
|
23
|
+
// Sessions with no prompt timestamps: use start/end
|
|
24
|
+
const ts = [session.timeRange.start, session.timeRange.end].filter(t => t > 0);
|
|
25
|
+
allTimestamps.push(...ts);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (allTimestamps.length === 0) return 0;
|
|
30
|
+
|
|
31
|
+
allTimestamps.sort((a, b) => a - b);
|
|
32
|
+
|
|
33
|
+
if (allTimestamps.length === 1) {
|
|
34
|
+
return 5 / 60; // Single timestamp → 5 minutes
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let totalMs = 0;
|
|
38
|
+
for (let i = 1; i < allTimestamps.length; i++) {
|
|
39
|
+
const gap = allTimestamps[i] - allTimestamps[i - 1];
|
|
40
|
+
totalMs += Math.min(gap, GAP_CAP_MS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return totalMs / (1000 * 60 * 60);
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PrivacyConfig } from "../types/privacy.js";
|
|
2
|
+
import type { Provider } from "../types/provider.js";
|
|
3
|
+
import type { ToolUsageReport, SessionListFilter } from "../types/aggregations.js";
|
|
4
|
+
import type { ToolCategory, ToolCallSummary } from "../types/session.js";
|
|
5
|
+
import { readHistory } from "../readers/history-reader.js";
|
|
6
|
+
import { parseSessionDetail } from "../parsers/session-detail.js";
|
|
7
|
+
import { resolveDateRange } from "../utils/dates.js";
|
|
8
|
+
|
|
9
|
+
/** Build a tool usage report from session data. */
|
|
10
|
+
export async function buildToolUsageReport(
|
|
11
|
+
provider: Provider,
|
|
12
|
+
filter: SessionListFilter,
|
|
13
|
+
historyFile: string,
|
|
14
|
+
paths: { projectsDir: string; sessionsDir: string },
|
|
15
|
+
privacy: PrivacyConfig,
|
|
16
|
+
): Promise<ToolUsageReport> {
|
|
17
|
+
const { from, to } = resolveDateRange(filter);
|
|
18
|
+
const sessions = await readHistory(historyFile, from, to, privacy, {
|
|
19
|
+
provider,
|
|
20
|
+
sessionsDir: paths.sessionsDir,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const byTool = new Map<string, number>();
|
|
24
|
+
const byCategory = new Map<ToolCategory, number>();
|
|
25
|
+
const fileCounts = new Map<string, number>();
|
|
26
|
+
const commandCounts = new Map<string, number>();
|
|
27
|
+
let total = 0;
|
|
28
|
+
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
if (session.prompts.length < 2) continue; // skip trivial sessions
|
|
31
|
+
const detail = await parseSessionDetail(provider, session, paths, privacy);
|
|
32
|
+
|
|
33
|
+
for (const tc of detail.toolCalls) {
|
|
34
|
+
byTool.set(tc.name, (byTool.get(tc.name) ?? 0) + 1);
|
|
35
|
+
byCategory.set(tc.category, (byCategory.get(tc.category) ?? 0) + 1);
|
|
36
|
+
total++;
|
|
37
|
+
|
|
38
|
+
if (tc.target) {
|
|
39
|
+
if (tc.category === "file_read" || tc.category === "file_write") {
|
|
40
|
+
fileCounts.set(tc.target, (fileCounts.get(tc.target) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
if (tc.category === "shell") {
|
|
43
|
+
commandCounts.set(tc.target, (commandCounts.get(tc.target) ?? 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const topFiles = [...fileCounts.entries()]
|
|
50
|
+
.sort((a, b) => b[1] - a[1])
|
|
51
|
+
.slice(0, 20)
|
|
52
|
+
.map(([path, count]) => ({ path, count }));
|
|
53
|
+
|
|
54
|
+
const topCommands = [...commandCounts.entries()]
|
|
55
|
+
.sort((a, b) => b[1] - a[1])
|
|
56
|
+
.slice(0, 20)
|
|
57
|
+
.map(([command, count]) => ({ command, count }));
|
|
58
|
+
|
|
59
|
+
return { byTool, byCategory, topFiles, topCommands, total };
|
|
60
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createHistory } from "../agent-optic.js";
|
|
4
|
+
import type { PrivacyProfile } from "../types/privacy.js";
|
|
5
|
+
import type { Provider } from "../types/provider.js";
|
|
6
|
+
import { today } from "../utils/dates.js";
|
|
7
|
+
import { defaultProviderDir, isProvider } from "../utils/providers.js";
|
|
8
|
+
|
|
9
|
+
const SCHEMA_VERSION = "1.0";
|
|
10
|
+
|
|
11
|
+
type OutputFormat = "json" | "jsonl";
|
|
12
|
+
|
|
13
|
+
const HELP = `agent-optic — Read AI assistant session data from local provider directories
|
|
14
|
+
|
|
15
|
+
USAGE
|
|
16
|
+
agent-optic <command> [options]
|
|
17
|
+
|
|
18
|
+
COMMANDS
|
|
19
|
+
sessions <optional-id> List sessions with metadata
|
|
20
|
+
detail <session-id> Show full detail for one session
|
|
21
|
+
transcript <session-id> Stream/print transcript entries
|
|
22
|
+
tool-usage Show aggregated tool usage
|
|
23
|
+
projects List all projects
|
|
24
|
+
stats Show pre-computed stats
|
|
25
|
+
daily Show daily summary
|
|
26
|
+
export Export session data with privacy controls
|
|
27
|
+
|
|
28
|
+
OPTIONS
|
|
29
|
+
--date YYYY-MM-DD Filter to specific date (default: today)
|
|
30
|
+
--from YYYY-MM-DD Start of date range
|
|
31
|
+
--to YYYY-MM-DD End of date range
|
|
32
|
+
--project <name> Filter by project name
|
|
33
|
+
--provider <name> Data provider: claude (default), codex, openai, cursor, windsurf
|
|
34
|
+
--provider-dir <path> Override provider data directory (default: ~/.<provider>)
|
|
35
|
+
--privacy <profile> Privacy profile: local (default), shareable, strict
|
|
36
|
+
--format <mode> Output mode: json (default), jsonl
|
|
37
|
+
--fields <a,b,c> Select object fields (top-level)
|
|
38
|
+
--limit <n> Limit array/stream length
|
|
39
|
+
--pretty Pretty-print JSON output
|
|
40
|
+
--raw Disable output envelope (data only)
|
|
41
|
+
--help Show this help
|
|
42
|
+
|
|
43
|
+
EXAMPLES
|
|
44
|
+
agent-optic sessions --provider codex --format jsonl
|
|
45
|
+
agent-optic detail 019c9aea-484d-7200-87fd-07a545276ac4 --provider openai
|
|
46
|
+
agent-optic transcript 019c9aea-484d-7200-87fd-07a545276ac4 --provider openai --format jsonl --limit 50
|
|
47
|
+
agent-optic tool-usage --provider codex --from 2026-02-01 --to 2026-02-26
|
|
48
|
+
agent-optic sessions --provider codex --date 2026-02-09
|
|
49
|
+
agent-optic sessions --provider openai --date 2026-02-09
|
|
50
|
+
|
|
51
|
+
SECURITY
|
|
52
|
+
Provider home directories contain highly sensitive data including API keys, source code,
|
|
53
|
+
and personal information. See SECURITY.md for details.
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
interface CliArgs {
|
|
57
|
+
command: string;
|
|
58
|
+
commandArg?: string;
|
|
59
|
+
date?: string;
|
|
60
|
+
from?: string;
|
|
61
|
+
to?: string;
|
|
62
|
+
project?: string;
|
|
63
|
+
provider: Provider;
|
|
64
|
+
providerDir?: string;
|
|
65
|
+
privacy: PrivacyProfile;
|
|
66
|
+
format: OutputFormat;
|
|
67
|
+
fields?: string[];
|
|
68
|
+
limit?: number;
|
|
69
|
+
pretty: boolean;
|
|
70
|
+
raw: boolean;
|
|
71
|
+
help: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class CliError extends Error {
|
|
75
|
+
constructor(
|
|
76
|
+
public code: string,
|
|
77
|
+
message: string,
|
|
78
|
+
public exitCode = 1,
|
|
79
|
+
public details?: Record<string, unknown>,
|
|
80
|
+
) {
|
|
81
|
+
super(message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseArgs(args: string[]): CliArgs {
|
|
86
|
+
const result: CliArgs = {
|
|
87
|
+
command: "",
|
|
88
|
+
provider: "claude",
|
|
89
|
+
privacy: "local",
|
|
90
|
+
format: "json",
|
|
91
|
+
pretty: false,
|
|
92
|
+
raw: false,
|
|
93
|
+
help: false,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i < args.length) {
|
|
98
|
+
const arg = args[i];
|
|
99
|
+
|
|
100
|
+
if (arg === "--help" || arg === "-h") {
|
|
101
|
+
result.help = true;
|
|
102
|
+
} else if (arg === "--date" && args[i + 1]) {
|
|
103
|
+
result.date = args[++i];
|
|
104
|
+
} else if (arg === "--from" && args[i + 1]) {
|
|
105
|
+
result.from = args[++i];
|
|
106
|
+
} else if (arg === "--to" && args[i + 1]) {
|
|
107
|
+
result.to = args[++i];
|
|
108
|
+
} else if (arg === "--project" && args[i + 1]) {
|
|
109
|
+
result.project = args[++i];
|
|
110
|
+
} else if (arg === "--provider" && args[i + 1]) {
|
|
111
|
+
result.provider = args[++i] as Provider;
|
|
112
|
+
} else if (arg === "--provider-dir" && args[i + 1]) {
|
|
113
|
+
result.providerDir = args[++i];
|
|
114
|
+
} else if (arg === "--privacy" && args[i + 1]) {
|
|
115
|
+
result.privacy = args[++i] as PrivacyProfile;
|
|
116
|
+
} else if (arg === "--format" && args[i + 1]) {
|
|
117
|
+
result.format = args[++i] as OutputFormat;
|
|
118
|
+
} else if (arg === "--fields" && args[i + 1]) {
|
|
119
|
+
result.fields = args[++i]
|
|
120
|
+
.split(",")
|
|
121
|
+
.map((f) => f.trim())
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
} else if (arg === "--limit" && args[i + 1]) {
|
|
124
|
+
const parsed = Number.parseInt(args[++i], 10);
|
|
125
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
126
|
+
result.limit = parsed;
|
|
127
|
+
}
|
|
128
|
+
} else if (arg === "--pretty") {
|
|
129
|
+
result.pretty = true;
|
|
130
|
+
} else if (arg === "--raw") {
|
|
131
|
+
result.raw = true;
|
|
132
|
+
} else if (!arg.startsWith("-") && !result.command) {
|
|
133
|
+
result.command = arg;
|
|
134
|
+
} else if (!arg.startsWith("-") && !result.commandArg) {
|
|
135
|
+
result.commandArg = arg;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function applyFieldSelection(data: unknown, fields?: string[]): unknown {
|
|
145
|
+
if (!fields || fields.length === 0) return data;
|
|
146
|
+
|
|
147
|
+
if (Array.isArray(data)) {
|
|
148
|
+
return data.map((item) => applyFieldSelection(item, fields));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!data || typeof data !== "object") return data;
|
|
152
|
+
const obj = data as Record<string, unknown>;
|
|
153
|
+
const selected: Record<string, unknown> = {};
|
|
154
|
+
for (const field of fields) {
|
|
155
|
+
if (field in obj) selected[field] = obj[field];
|
|
156
|
+
}
|
|
157
|
+
return selected;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function applyLimit(data: unknown, limit?: number): unknown {
|
|
161
|
+
if (!limit) return data;
|
|
162
|
+
if (Array.isArray(data)) return data.slice(0, limit);
|
|
163
|
+
return data;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeOutput(
|
|
167
|
+
command: string,
|
|
168
|
+
provider: Provider,
|
|
169
|
+
data: unknown,
|
|
170
|
+
args: CliArgs,
|
|
171
|
+
): void {
|
|
172
|
+
const transformed = applyLimit(applyFieldSelection(data, args.fields), args.limit);
|
|
173
|
+
const generatedAt = new Date().toISOString();
|
|
174
|
+
|
|
175
|
+
if (args.format === "json") {
|
|
176
|
+
const payload = args.raw
|
|
177
|
+
? transformed
|
|
178
|
+
: {
|
|
179
|
+
schemaVersion: SCHEMA_VERSION,
|
|
180
|
+
command,
|
|
181
|
+
provider,
|
|
182
|
+
generatedAt,
|
|
183
|
+
data: transformed,
|
|
184
|
+
};
|
|
185
|
+
console.log(
|
|
186
|
+
JSON.stringify(payload, mapReplacer, args.pretty ? 2 : 0),
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const rows = Array.isArray(transformed) ? transformed : [transformed];
|
|
192
|
+
for (const row of rows) {
|
|
193
|
+
const payload = args.raw
|
|
194
|
+
? row
|
|
195
|
+
: {
|
|
196
|
+
schemaVersion: SCHEMA_VERSION,
|
|
197
|
+
command,
|
|
198
|
+
provider,
|
|
199
|
+
generatedAt,
|
|
200
|
+
data: row,
|
|
201
|
+
};
|
|
202
|
+
console.log(JSON.stringify(payload, mapReplacer));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** JSON.stringify replacer that converts Maps to plain objects. */
|
|
207
|
+
function mapReplacer(_key: string, value: unknown): unknown {
|
|
208
|
+
if (value instanceof Map) {
|
|
209
|
+
return Object.fromEntries(value);
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function printError(error: CliError, args?: CliArgs): void {
|
|
215
|
+
const format = args?.format ?? "json";
|
|
216
|
+
const payload = {
|
|
217
|
+
schemaVersion: SCHEMA_VERSION,
|
|
218
|
+
error: {
|
|
219
|
+
code: error.code,
|
|
220
|
+
message: error.message,
|
|
221
|
+
details: error.details,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const text =
|
|
225
|
+
format === "json" && args?.pretty
|
|
226
|
+
? JSON.stringify(payload, null, 2)
|
|
227
|
+
: JSON.stringify(payload);
|
|
228
|
+
console.error(text);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function assertValidArgs(args: CliArgs): void {
|
|
232
|
+
if (!["local", "shareable", "strict"].includes(args.privacy)) {
|
|
233
|
+
throw new CliError(
|
|
234
|
+
"INVALID_PRIVACY_PROFILE",
|
|
235
|
+
`Invalid privacy profile: ${args.privacy}. Use: local, shareable, strict`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!isProvider(args.provider)) {
|
|
240
|
+
throw new CliError(
|
|
241
|
+
"INVALID_PROVIDER",
|
|
242
|
+
`Invalid provider: ${args.provider}. Use: claude, codex, openai, cursor, windsurf`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!["json", "jsonl"].includes(args.format)) {
|
|
247
|
+
throw new CliError(
|
|
248
|
+
"INVALID_FORMAT",
|
|
249
|
+
`Invalid format: ${args.format}. Use: json, jsonl`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function run(args: CliArgs): Promise<void> {
|
|
255
|
+
if (args.help || !args.command) {
|
|
256
|
+
console.log(HELP);
|
|
257
|
+
process.exit(args.help ? 0 : 1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
assertValidArgs(args);
|
|
261
|
+
|
|
262
|
+
const providerDir = args.providerDir ?? defaultProviderDir(args.provider);
|
|
263
|
+
const ch = createHistory({
|
|
264
|
+
provider: args.provider,
|
|
265
|
+
providerDir,
|
|
266
|
+
privacy: args.privacy,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const filter = {
|
|
270
|
+
date: args.date,
|
|
271
|
+
from: args.from,
|
|
272
|
+
to: args.to,
|
|
273
|
+
project: args.project,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
switch (args.command) {
|
|
277
|
+
case "sessions": {
|
|
278
|
+
const sessionsFilter =
|
|
279
|
+
args.commandArg && !args.date && !args.from && !args.to
|
|
280
|
+
? { ...filter, from: "2000-01-01", to: "2099-12-31" }
|
|
281
|
+
: filter;
|
|
282
|
+
let sessions = await ch.sessions.listWithMeta(sessionsFilter);
|
|
283
|
+
if (args.commandArg) {
|
|
284
|
+
sessions = sessions.filter((s) => s.sessionId === args.commandArg);
|
|
285
|
+
}
|
|
286
|
+
writeOutput("sessions", args.provider, sessions, args);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case "detail": {
|
|
291
|
+
if (!args.commandArg) {
|
|
292
|
+
throw new CliError(
|
|
293
|
+
"MISSING_ARGUMENT",
|
|
294
|
+
"Missing session ID. Usage: agent-optic detail <session-id>",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const detail = await ch.sessions.detail(args.commandArg, args.project);
|
|
298
|
+
writeOutput("detail", args.provider, detail, args);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case "transcript": {
|
|
303
|
+
if (!args.commandArg) {
|
|
304
|
+
throw new CliError(
|
|
305
|
+
"MISSING_ARGUMENT",
|
|
306
|
+
"Missing session ID. Usage: agent-optic transcript <session-id>",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (args.format === "jsonl") {
|
|
311
|
+
const generatedAt = new Date().toISOString();
|
|
312
|
+
let count = 0;
|
|
313
|
+
for await (const entry of ch.sessions.transcript(args.commandArg, args.project)) {
|
|
314
|
+
if (args.limit && count >= args.limit) break;
|
|
315
|
+
const transformed = applyFieldSelection(entry, args.fields);
|
|
316
|
+
const payload = args.raw
|
|
317
|
+
? transformed
|
|
318
|
+
: {
|
|
319
|
+
schemaVersion: SCHEMA_VERSION,
|
|
320
|
+
command: "transcript",
|
|
321
|
+
provider: args.provider,
|
|
322
|
+
generatedAt,
|
|
323
|
+
data: transformed,
|
|
324
|
+
};
|
|
325
|
+
console.log(JSON.stringify(payload, mapReplacer));
|
|
326
|
+
count++;
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const entries: unknown[] = [];
|
|
332
|
+
for await (const entry of ch.sessions.transcript(args.commandArg, args.project)) {
|
|
333
|
+
entries.push(entry);
|
|
334
|
+
if (args.limit && entries.length >= args.limit) break;
|
|
335
|
+
}
|
|
336
|
+
writeOutput("transcript", args.provider, entries, args);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case "tool-usage": {
|
|
341
|
+
const usage = await ch.aggregate.toolUsage(filter);
|
|
342
|
+
writeOutput("tool-usage", args.provider, usage, args);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case "projects": {
|
|
347
|
+
const projects = await ch.projects.list();
|
|
348
|
+
writeOutput("projects", args.provider, projects, args);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
case "stats": {
|
|
353
|
+
const stats = await ch.stats.get();
|
|
354
|
+
if (!stats) {
|
|
355
|
+
throw new CliError(
|
|
356
|
+
"STATS_NOT_FOUND",
|
|
357
|
+
`No stats cache found at ${providerDir}/stats-cache.json`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
writeOutput("stats", args.provider, stats, args);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case "daily": {
|
|
365
|
+
const date = args.date ?? today();
|
|
366
|
+
const summary = await ch.aggregate.daily(date);
|
|
367
|
+
writeOutput("daily", args.provider, summary, args);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case "export": {
|
|
372
|
+
const date = args.date;
|
|
373
|
+
const from = args.from ?? date ?? today();
|
|
374
|
+
const to = args.to ?? date ?? today();
|
|
375
|
+
const summaries = await ch.aggregate.dailyRange(from, to);
|
|
376
|
+
writeOutput("export", args.provider, summaries, args);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
default:
|
|
381
|
+
throw new CliError(
|
|
382
|
+
"UNKNOWN_COMMAND",
|
|
383
|
+
`Unknown command: ${args.command}`,
|
|
384
|
+
2,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function main() {
|
|
390
|
+
const args = parseArgs(process.argv.slice(2));
|
|
391
|
+
try {
|
|
392
|
+
await run(args);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (err instanceof CliError) {
|
|
395
|
+
printError(err, args);
|
|
396
|
+
process.exit(err.exitCode);
|
|
397
|
+
}
|
|
398
|
+
const fallback = new CliError(
|
|
399
|
+
"INTERNAL_ERROR",
|
|
400
|
+
err instanceof Error ? err.message : "Unknown error",
|
|
401
|
+
);
|
|
402
|
+
printError(fallback, args);
|
|
403
|
+
process.exit(fallback.exitCode);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
main();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Main factory
|
|
2
|
+
export { createHistory, createClaudeHistory } from "./agent-optic.js";
|
|
3
|
+
export type { History, HistoryConfig, ClaudeHistory, ClaudeHistoryConfig } from "./agent-optic.js";
|
|
4
|
+
|
|
5
|
+
// Provider types and utils
|
|
6
|
+
export type { Provider } from "./types/provider.js";
|
|
7
|
+
export { SUPPORTED_PROVIDERS } from "./types/provider.js";
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
HistoryEntry,
|
|
12
|
+
SessionInfo,
|
|
13
|
+
SessionMeta,
|
|
14
|
+
SessionDetail,
|
|
15
|
+
ToolCategory,
|
|
16
|
+
ToolCallSummary,
|
|
17
|
+
} from "./types/session.js";
|
|
18
|
+
|
|
19
|
+
export type { ContentBlock, TranscriptEntry } from "./types/transcript.js";
|
|
20
|
+
|
|
21
|
+
export type { TaskInfo, TodoItem } from "./types/task.js";
|
|
22
|
+
|
|
23
|
+
export type { PlanInfo } from "./types/plan.js";
|
|
24
|
+
|
|
25
|
+
export type { ProjectInfo, ProjectMemory } from "./types/project.js";
|
|
26
|
+
|
|
27
|
+
export type { StatsCache } from "./types/stats.js";
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
DailySummary,
|
|
31
|
+
ProjectSummary,
|
|
32
|
+
ToolUsageReport,
|
|
33
|
+
DateFilter,
|
|
34
|
+
SessionListFilter,
|
|
35
|
+
} from "./types/aggregations.js";
|
|
36
|
+
|
|
37
|
+
export type { PrivacyConfig, PrivacyProfile } from "./types/privacy.js";
|
|
38
|
+
|
|
39
|
+
// Privacy profiles (runtime values, not just types)
|
|
40
|
+
export { PRIVACY_PROFILES, resolvePrivacyConfig } from "./privacy/config.js";
|
|
41
|
+
|
|
42
|
+
// Utilities (for advanced users)
|
|
43
|
+
export {
|
|
44
|
+
encodeProjectPath,
|
|
45
|
+
decodeProjectPath,
|
|
46
|
+
projectName,
|
|
47
|
+
providerPaths,
|
|
48
|
+
claudePaths,
|
|
49
|
+
} from "./utils/paths.js";
|
|
50
|
+
export {
|
|
51
|
+
DEFAULT_PROVIDER,
|
|
52
|
+
defaultProviderDir,
|
|
53
|
+
providerHomeDirName,
|
|
54
|
+
isProvider,
|
|
55
|
+
} from "./utils/providers.js";
|
|
56
|
+
export { toLocalDate, today, formatTime, resolveDateRange } from "./utils/dates.js";
|
|
57
|
+
export { parseJsonl, streamJsonl, peekJsonl, readJsonl } from "./utils/jsonl.js";
|
|
58
|
+
|
|
59
|
+
// Parsers (for advanced users building custom pipelines)
|
|
60
|
+
export { parseSessionDetail, parseSessions } from "./parsers/session-detail.js";
|
|
61
|
+
export { categorizeToolName, toolDisplayName } from "./parsers/tool-categories.js";
|
|
62
|
+
export { extractText, extractToolCalls, extractFilePaths, countThinkingBlocks } from "./parsers/content-blocks.js";
|
|
63
|
+
|
|
64
|
+
// Pricing
|
|
65
|
+
export type { ModelPricing } from "./pricing.js";
|
|
66
|
+
export { MODEL_PRICING, getModelPricing, estimateCost, setPricing } from "./pricing.js";
|
|
67
|
+
|
|
68
|
+
// Readers (for advanced users)
|
|
69
|
+
export { readProjectMemories } from "./readers/project-reader.js";
|