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,40 @@
|
|
|
1
|
+
/** Convert a timestamp (ms) to YYYY-MM-DD in local time. */
|
|
2
|
+
export function toLocalDate(timestamp: number): string {
|
|
3
|
+
const d = new Date(timestamp);
|
|
4
|
+
const year = d.getFullYear();
|
|
5
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
6
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
7
|
+
return `${year}-${month}-${day}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Get today's date as YYYY-MM-DD. */
|
|
11
|
+
export function today(): string {
|
|
12
|
+
return toLocalDate(Date.now());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Format a timestamp to HH:MM (24h). */
|
|
16
|
+
export function formatTime(timestamp: number): string {
|
|
17
|
+
const d = new Date(timestamp);
|
|
18
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
19
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
20
|
+
return `${h}:${m}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolve a DateFilter to concrete from/to strings. */
|
|
24
|
+
export function resolveDateRange(filter?: {
|
|
25
|
+
date?: string;
|
|
26
|
+
from?: string;
|
|
27
|
+
to?: string;
|
|
28
|
+
}): { from: string; to: string } {
|
|
29
|
+
if (filter?.date) {
|
|
30
|
+
return { from: filter.date, to: filter.date };
|
|
31
|
+
}
|
|
32
|
+
if (filter?.from && filter?.to) {
|
|
33
|
+
return { from: filter.from, to: filter.to };
|
|
34
|
+
}
|
|
35
|
+
if (filter?.from) {
|
|
36
|
+
return { from: filter.from, to: today() };
|
|
37
|
+
}
|
|
38
|
+
const t = today();
|
|
39
|
+
return { from: t, to: t };
|
|
40
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Parse all lines from a JSONL string, skipping malformed lines. */
|
|
2
|
+
export function parseJsonl<T>(text: string): T[] {
|
|
3
|
+
const results: T[] = [];
|
|
4
|
+
for (const line of text.split("\n")) {
|
|
5
|
+
if (!line.trim()) continue;
|
|
6
|
+
try {
|
|
7
|
+
results.push(JSON.parse(line) as T);
|
|
8
|
+
} catch {
|
|
9
|
+
// skip malformed
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return results;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Stream-parse JSONL from a file, yielding each parsed line. */
|
|
16
|
+
export async function* streamJsonl<T>(filePath: string): AsyncGenerator<T> {
|
|
17
|
+
const file = Bun.file(filePath);
|
|
18
|
+
if (!(await file.exists())) return;
|
|
19
|
+
|
|
20
|
+
const text = await file.text();
|
|
21
|
+
for (const line of text.split("\n")) {
|
|
22
|
+
if (!line.trim()) continue;
|
|
23
|
+
try {
|
|
24
|
+
yield JSON.parse(line) as T;
|
|
25
|
+
} catch {
|
|
26
|
+
// skip malformed
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Peek a JSONL file — read only first N and last N bytes for metadata extraction. */
|
|
32
|
+
export async function peekJsonl(
|
|
33
|
+
filePath: string,
|
|
34
|
+
bytes: number = 4096,
|
|
35
|
+
): Promise<{ first: unknown[]; last: unknown[]; totalBytes: number }> {
|
|
36
|
+
const file = Bun.file(filePath);
|
|
37
|
+
if (!(await file.exists())) return { first: [], last: [], totalBytes: 0 };
|
|
38
|
+
|
|
39
|
+
const size = file.size;
|
|
40
|
+
|
|
41
|
+
if (size <= bytes * 2) {
|
|
42
|
+
// Small file: just read the whole thing
|
|
43
|
+
const text = await file.text();
|
|
44
|
+
const lines = parseJsonl<unknown>(text);
|
|
45
|
+
return { first: lines, last: [], totalBytes: size };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read first chunk
|
|
49
|
+
const firstChunk = await file.slice(0, bytes).text();
|
|
50
|
+
const firstLines = firstChunk.split("\n").filter((l) => l.trim());
|
|
51
|
+
const first: unknown[] = [];
|
|
52
|
+
for (const line of firstLines.slice(0, -1)) {
|
|
53
|
+
// Skip last line of chunk (may be truncated)
|
|
54
|
+
try {
|
|
55
|
+
first.push(JSON.parse(line));
|
|
56
|
+
} catch {
|
|
57
|
+
// skip
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read last chunk
|
|
62
|
+
const lastChunk = await file.slice(Math.max(0, size - bytes), size).text();
|
|
63
|
+
const lastLines = lastChunk.split("\n").filter((l) => l.trim());
|
|
64
|
+
const last: unknown[] = [];
|
|
65
|
+
for (const line of lastLines.slice(1)) {
|
|
66
|
+
// Skip first line of chunk (may be truncated)
|
|
67
|
+
try {
|
|
68
|
+
last.push(JSON.parse(line));
|
|
69
|
+
} catch {
|
|
70
|
+
// skip
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { first, last, totalBytes: size };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Read and parse an entire JSONL file. */
|
|
78
|
+
export async function readJsonl<T>(filePath: string): Promise<T[]> {
|
|
79
|
+
const file = Bun.file(filePath);
|
|
80
|
+
if (!(await file.exists())) return [];
|
|
81
|
+
const text = await file.text();
|
|
82
|
+
return parseJsonl<T>(text);
|
|
83
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { Provider } from "../types/provider.js";
|
|
3
|
+
import { DEFAULT_PROVIDER, defaultProviderDir } from "./providers.js";
|
|
4
|
+
|
|
5
|
+
/** Encode a project path for filesystem storage (/ → -). */
|
|
6
|
+
export function encodeProjectPath(projectPath: string): string {
|
|
7
|
+
return projectPath.replace(/\//g, "-");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Decode an encoded project path back to original (- → /). Best-effort: ambiguous. */
|
|
11
|
+
export function decodeProjectPath(encoded: string): string {
|
|
12
|
+
return encoded.replace(/-/g, "/");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Extract a short project name from a full path. */
|
|
16
|
+
export function projectName(projectPath: string): string {
|
|
17
|
+
return projectPath.split("/").pop() || projectPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProviderPaths {
|
|
21
|
+
base: string;
|
|
22
|
+
historyFile: string;
|
|
23
|
+
projectsDir: string;
|
|
24
|
+
sessionsDir: string;
|
|
25
|
+
globalStateFile: string;
|
|
26
|
+
tasksDir: string;
|
|
27
|
+
plansDir: string;
|
|
28
|
+
todosDir: string;
|
|
29
|
+
skillsDir: string;
|
|
30
|
+
statsCache: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Build all standard paths relative to a provider directory. */
|
|
34
|
+
export function providerPaths(config?: {
|
|
35
|
+
provider?: Provider;
|
|
36
|
+
providerDir?: string;
|
|
37
|
+
}): ProviderPaths {
|
|
38
|
+
const provider = config?.provider ?? DEFAULT_PROVIDER;
|
|
39
|
+
const base = config?.providerDir ?? defaultProviderDir(provider);
|
|
40
|
+
return {
|
|
41
|
+
base,
|
|
42
|
+
historyFile: join(base, "history.jsonl"),
|
|
43
|
+
projectsDir: join(base, "projects"),
|
|
44
|
+
sessionsDir: join(base, "sessions"),
|
|
45
|
+
globalStateFile: join(base, ".codex-global-state.json"),
|
|
46
|
+
tasksDir: join(base, "tasks"),
|
|
47
|
+
plansDir: join(base, "plans"),
|
|
48
|
+
todosDir: join(base, "todos"),
|
|
49
|
+
skillsDir: join(base, "skills"),
|
|
50
|
+
statsCache: join(base, "stats-cache.json"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Backward-compatible alias for Claude-specific default paths. */
|
|
55
|
+
export function claudePaths(claudeDir?: string): ProviderPaths {
|
|
56
|
+
return providerPaths({ provider: "claude", providerDir: claudeDir });
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Provider } from "../types/provider.js";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_HOME_DIR: Record<Provider, string> = {
|
|
6
|
+
claude: ".claude",
|
|
7
|
+
codex: ".codex",
|
|
8
|
+
openai: ".codex",
|
|
9
|
+
cursor: ".cursor",
|
|
10
|
+
windsurf: ".windsurf",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_PROVIDER: Provider = "claude";
|
|
14
|
+
|
|
15
|
+
export function defaultProviderDir(provider: Provider): string {
|
|
16
|
+
return join(homedir(), PROVIDER_HOME_DIR[provider]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function providerHomeDirName(provider: Provider): string {
|
|
20
|
+
return PROVIDER_HOME_DIR[provider];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isProvider(value: string): value is Provider {
|
|
24
|
+
return value in PROVIDER_HOME_DIR;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function canonicalProvider(provider: Provider): Exclude<Provider, "openai"> {
|
|
28
|
+
if (provider === "openai") return "codex";
|
|
29
|
+
return provider;
|
|
30
|
+
}
|