kodelyth-ecc 1.2.2 → 1.4.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/AGENTS.md +101 -181
- package/CHANGELOG.md +67 -0
- package/CLAUDE.md +72 -63
- package/KODELYTH.md +79 -44
- package/README.md +244 -192
- package/VERSION +1 -1
- package/agents/dependency-doctor.md +120 -0
- package/agents/env-debugger.md +154 -0
- package/agents/flake-hunter.md +142 -0
- package/agents/git-rescue.md +133 -0
- package/agents/kodelyth-memory.md +87 -0
- package/agents/release-captain.md +190 -0
- package/bin/kodelyth-ecc.js +18 -12
- package/commands/memory.md +62 -0
- package/hooks/hooks.json +26 -0
- package/hooks/memory/capture-stop.js +88 -0
- package/hooks/memory/inject-start.js +60 -0
- package/install.ps1 +28 -9
- package/install.sh +11 -97
- package/package.json +4 -2
- package/rules/common/agent-intent-routing.md +337 -0
- package/rules/common/memory-protocol.md +56 -0
- package/scripts/memory/cli.js +200 -0
- package/scripts/memory/extract.js +176 -0
- package/scripts/memory/inject.js +145 -0
- package/scripts/memory/store.js +300 -0
- package/skills/agent-handoff/SKILL.md +184 -0
- package/skills/intent-routing/SKILL.md +134 -0
- package/skills/kodelyth-memory/SKILL.md +136 -0
- package/tests/memory/store.test.js +121 -0
- package/dashboard/lib/agent-tracker.js +0 -366
- package/dashboard/lib/aggregator.js +0 -119
- package/dashboard/lib/cost-calculator.js +0 -50
- package/dashboard/lib/platform-detector.js +0 -89
- package/dashboard/lib/readers/antigravity-reader.js +0 -113
- package/dashboard/lib/readers/claude-reader.js +0 -135
- package/dashboard/lib/readers/codex-reader.js +0 -192
- package/dashboard/lib/readers/cursor-reader.js +0 -135
- package/dashboard/lib/readers/opencode-reader.js +0 -201
- package/dashboard/lib/readers/windsurf-reader.js +0 -146
- package/dashboard/package.json +0 -24
- package/dashboard/public/index.html +0 -1221
- package/dashboard/server.js +0 -119
- package/scripts/agent-tracker-hook.js +0 -81
- package/social/readme-lens.svg +0 -140
- package/social/readme-savings.svg +0 -56
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Data Aggregator
|
|
2
|
-
// Merges data from all platforms into unified stats
|
|
3
|
-
// Part of Kodelyth ECC — github.com/sifxprime/kodelyth-ecc
|
|
4
|
-
|
|
5
|
-
import { detectPlatforms } from './platform-detector.js';
|
|
6
|
-
import { readClaudeData } from './readers/claude-reader.js';
|
|
7
|
-
import { readCursorData } from './readers/cursor-reader.js';
|
|
8
|
-
import { readWindsurfData } from './readers/windsurf-reader.js';
|
|
9
|
-
import { readCodexData } from './readers/codex-reader.js';
|
|
10
|
-
import { readOpenCodeData } from './readers/opencode-reader.js';
|
|
11
|
-
import { readAntigravityData } from './readers/antigravity-reader.js';
|
|
12
|
-
import { buildAgentLeaderboard } from './agent-tracker.js';
|
|
13
|
-
import { buildDailyCosts, cacheSavings } from './cost-calculator.js';
|
|
14
|
-
|
|
15
|
-
export async function aggregateAll() {
|
|
16
|
-
const platforms = detectPlatforms();
|
|
17
|
-
|
|
18
|
-
// Read all platforms in parallel, never crash if one fails
|
|
19
|
-
const [claude, cursor, windsurf, codex, opencode, antigravity] = await Promise.all([
|
|
20
|
-
platforms.claude.installed ? readClaudeData(platforms.claude.dataDir).catch(() => ({ sessions: [] })) : { sessions: [] },
|
|
21
|
-
platforms.cursor.installed ? readCursorData(platforms.cursor.dataDir).catch(() => ({ sessions: [] })) : { sessions: [] },
|
|
22
|
-
platforms.windsurf.installed ? readWindsurfData(platforms.windsurf.dataDir).catch(() => ({ sessions: [] })) : { sessions: [] },
|
|
23
|
-
platforms.codex.installed ? readCodexData(platforms.codex.dataDir).catch(() => ({ sessions: [] })) : { sessions: [] },
|
|
24
|
-
platforms.opencode.installed ? readOpenCodeData(platforms.opencode.dataDir).catch(() => ({ sessions: [] })) : { sessions: [] },
|
|
25
|
-
platforms.antigravity.installed ? readAntigravityData(platforms.antigravity.dataDir).catch(() => ({ sessions: [] })) : { sessions: [] },
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
const allSessions = [
|
|
29
|
-
...claude.sessions,
|
|
30
|
-
...cursor.sessions,
|
|
31
|
-
...windsurf.sessions,
|
|
32
|
-
...codex.sessions,
|
|
33
|
-
...opencode.sessions,
|
|
34
|
-
...antigravity.sessions,
|
|
35
|
-
].sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
36
|
-
|
|
37
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
38
|
-
// A session counts as "today" if it started today OR was last active today
|
|
39
|
-
// (long-running sessions that span multiple days still appear in today's stats)
|
|
40
|
-
const todaySessions = allSessions.filter(s => s.date === today || s.lastDate === today);
|
|
41
|
-
|
|
42
|
-
const totalCacheSaved = allSessions.reduce((n, s) => n + (s.cacheSaved || 0), 0);
|
|
43
|
-
|
|
44
|
-
const stats = {
|
|
45
|
-
today: {
|
|
46
|
-
sessions: todaySessions.length,
|
|
47
|
-
tokens: sumField(todaySessions, 'tokens.total'),
|
|
48
|
-
cost: round(sumField(todaySessions, 'cost')),
|
|
49
|
-
agentCalls: sumField(todaySessions, 'agentCalls'),
|
|
50
|
-
},
|
|
51
|
-
allTime: {
|
|
52
|
-
sessions: allSessions.length,
|
|
53
|
-
tokens: sumField(allSessions, 'tokens.total'),
|
|
54
|
-
cost: round(sumField(allSessions, 'cost')),
|
|
55
|
-
agentCalls: sumField(allSessions, 'agentCalls'),
|
|
56
|
-
cacheSaved: round(totalCacheSaved),
|
|
57
|
-
cacheHitRate: calcCacheHitRate(allSessions),
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const platformStats = buildPlatformStats(allSessions, platforms);
|
|
62
|
-
const agents = buildAgentLeaderboard(allSessions);
|
|
63
|
-
const dailyCosts = buildDailyCosts(allSessions);
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
stats,
|
|
67
|
-
agents,
|
|
68
|
-
sessions: allSessions.slice(0, 100),
|
|
69
|
-
costs: dailyCosts,
|
|
70
|
-
platforms: platformStats,
|
|
71
|
-
windsurf: { quotaData: windsurf.quotaData || null },
|
|
72
|
-
updatedAt: new Date().toISOString(),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function sumField(arr, field) {
|
|
77
|
-
return arr.reduce((n, s) => {
|
|
78
|
-
const parts = field.split('.');
|
|
79
|
-
let val = s;
|
|
80
|
-
for (const p of parts) val = val?.[p];
|
|
81
|
-
return n + (Number(val) || 0);
|
|
82
|
-
}, 0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function round(n) { return Math.round(n * 10000) / 10000; }
|
|
86
|
-
|
|
87
|
-
function calcCacheHitRate(sessions) {
|
|
88
|
-
const totalInput = sumField(sessions, 'tokens.input');
|
|
89
|
-
const totalRead = sumField(sessions, 'tokens.cacheRead');
|
|
90
|
-
if (!totalInput && !totalRead) return 0;
|
|
91
|
-
return Math.round((totalRead / (totalInput + totalRead)) * 100);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function buildPlatformStats(sessions, platforms) {
|
|
95
|
-
const map = {};
|
|
96
|
-
for (const [key, info] of Object.entries(platforms)) {
|
|
97
|
-
map[key] = {
|
|
98
|
-
label: info.label,
|
|
99
|
-
color: info.color,
|
|
100
|
-
installed: info.installed,
|
|
101
|
-
sessions: 0,
|
|
102
|
-
tokens: 0,
|
|
103
|
-
cost: 0,
|
|
104
|
-
agents: 0,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
for (const s of sessions) {
|
|
109
|
-
const p = map[s.platform];
|
|
110
|
-
if (!p) continue;
|
|
111
|
-
p.sessions++;
|
|
112
|
-
p.tokens += s.tokens?.total || 0;
|
|
113
|
-
p.cost += s.cost || 0;
|
|
114
|
-
p.agents += s.agentCalls || 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
for (const p of Object.values(map)) p.cost = round(p.cost);
|
|
118
|
-
return map;
|
|
119
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Cost Calculator
|
|
2
|
-
// Part of Kodelyth ECC — github.com/sifxprime/kodelyth-ecc
|
|
3
|
-
|
|
4
|
-
const PRESETS = {
|
|
5
|
-
anthropic: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
|
|
6
|
-
bedrock: { input: 5.00, output: 25.00, cacheRead: 0.50, cacheWrite: 6.25 },
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export function getRates() {
|
|
10
|
-
const preset = process.env.PRICING_PRESET || 'anthropic';
|
|
11
|
-
const base = PRESETS[preset] || PRESETS.anthropic;
|
|
12
|
-
return {
|
|
13
|
-
input: parseFloat(process.env.RATE_INPUT || base.input),
|
|
14
|
-
output: parseFloat(process.env.RATE_OUTPUT || base.output),
|
|
15
|
-
cacheRead: parseFloat(process.env.RATE_CACHE_READ || base.cacheRead),
|
|
16
|
-
cacheWrite: parseFloat(process.env.RATE_CACHE_WRITE|| base.cacheWrite),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function tokensToCost(tokens) {
|
|
21
|
-
const r = getRates();
|
|
22
|
-
return (
|
|
23
|
-
((tokens.input || 0) / 1_000_000) * r.input +
|
|
24
|
-
((tokens.output || 0) / 1_000_000) * r.output +
|
|
25
|
-
((tokens.cacheRead || 0) / 1_000_000) * r.cacheRead +
|
|
26
|
-
((tokens.cacheWrite || 0) / 1_000_000) * r.cacheWrite
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function cacheSavings(tokens) {
|
|
31
|
-
const r = getRates();
|
|
32
|
-
// Savings = what cache reads would have cost as input tokens minus what they actually cost
|
|
33
|
-
const saved = ((tokens.cacheRead || 0) / 1_000_000) * (r.input - r.cacheRead);
|
|
34
|
-
return Math.max(0, saved);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function buildDailyCosts(sessions) {
|
|
38
|
-
const byDay = {};
|
|
39
|
-
for (const s of sessions) {
|
|
40
|
-
const day = s.date;
|
|
41
|
-
if (!byDay[day]) byDay[day] = { date: day, cost: 0, tokens: 0, sessions: 0, agents: 0 };
|
|
42
|
-
byDay[day].cost += s.cost || 0;
|
|
43
|
-
byDay[day].tokens += (s.tokens?.total || 0);
|
|
44
|
-
byDay[day].sessions += 1;
|
|
45
|
-
byDay[day].agents += s.agentCalls || 0;
|
|
46
|
-
}
|
|
47
|
-
return Object.values(byDay)
|
|
48
|
-
.sort((a, b) => a.date.localeCompare(b.date))
|
|
49
|
-
.slice(-30); // last 30 days
|
|
50
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Platform Detector
|
|
2
|
-
// Auto-detects installed AI coding platforms
|
|
3
|
-
// Part of Kodelyth ECC — github.com/sifxprime/kodelyth-ecc
|
|
4
|
-
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
|
|
9
|
-
const HOME = os.homedir();
|
|
10
|
-
|
|
11
|
-
function exists(p) {
|
|
12
|
-
try { return fs.existsSync(p); } catch { return false; }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function detectPlatforms() {
|
|
16
|
-
const appSupport = path.join(HOME, 'Library', 'Application Support'); // macOS
|
|
17
|
-
const appData = path.join(HOME, 'AppData', 'Roaming'); // Windows
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
claude: {
|
|
21
|
-
installed: exists(path.join(HOME, '.claude')),
|
|
22
|
-
dataDir: process.env.CLAUDE_DIR || path.join(HOME, '.claude'),
|
|
23
|
-
label: 'Claude Code',
|
|
24
|
-
color: '#f97316',
|
|
25
|
-
},
|
|
26
|
-
cursor: {
|
|
27
|
-
installed: exists(path.join(HOME, '.cursor')) ||
|
|
28
|
-
exists(path.join(appSupport, 'Cursor')) ||
|
|
29
|
-
exists(path.join(appData, 'Cursor')) ||
|
|
30
|
-
exists(path.join(HOME, '.config', 'Cursor')),
|
|
31
|
-
dataDir: process.env.CURSOR_DIR ||
|
|
32
|
-
(exists(path.join(appSupport, 'Cursor'))
|
|
33
|
-
? path.join(appSupport, 'Cursor')
|
|
34
|
-
: exists(path.join(HOME, '.config', 'Cursor'))
|
|
35
|
-
? path.join(HOME, '.config', 'Cursor')
|
|
36
|
-
: path.join(HOME, '.cursor')),
|
|
37
|
-
label: 'Cursor',
|
|
38
|
-
color: '#3b82f6',
|
|
39
|
-
},
|
|
40
|
-
windsurf: {
|
|
41
|
-
installed: exists(path.join(HOME, '.codeium', 'windsurf')) ||
|
|
42
|
-
exists(path.join(appSupport, 'Windsurf')) ||
|
|
43
|
-
exists(path.join(appData, 'Windsurf')),
|
|
44
|
-
dataDir: process.env.WINDSURF_DIR ||
|
|
45
|
-
(exists(path.join(appSupport, 'Windsurf'))
|
|
46
|
-
? path.join(appSupport, 'Windsurf')
|
|
47
|
-
: path.join(HOME, '.codeium', 'windsurf')),
|
|
48
|
-
label: 'Windsurf',
|
|
49
|
-
color: '#06b6d4',
|
|
50
|
-
},
|
|
51
|
-
codex: {
|
|
52
|
-
installed: exists(path.join(HOME, '.codex')),
|
|
53
|
-
dataDir: process.env.CODEX_DIR || path.join(HOME, '.codex'),
|
|
54
|
-
label: 'Codex CLI',
|
|
55
|
-
color: '#10b981',
|
|
56
|
-
},
|
|
57
|
-
antigravity: {
|
|
58
|
-
installed: exists(path.join(appSupport, 'Antigravity')) ||
|
|
59
|
-
exists(path.join(appData, 'Antigravity')) ||
|
|
60
|
-
exists(path.join(HOME, '.antigravity')) ||
|
|
61
|
-
exists(path.join(process.cwd(), '.agent')),
|
|
62
|
-
dataDir: process.env.ANTIGRAVITY_DIR ||
|
|
63
|
-
(exists(path.join(appSupport, 'Antigravity'))
|
|
64
|
-
? path.join(appSupport, 'Antigravity')
|
|
65
|
-
: exists(path.join(appData, 'Antigravity'))
|
|
66
|
-
? path.join(appData, 'Antigravity')
|
|
67
|
-
: path.join(HOME, '.antigravity')),
|
|
68
|
-
label: 'Antigravity',
|
|
69
|
-
color: '#7c3aed',
|
|
70
|
-
},
|
|
71
|
-
opencode: {
|
|
72
|
-
installed: exists(path.join(HOME, '.opencode')) ||
|
|
73
|
-
exists(path.join(HOME, '.config', 'opencode')) ||
|
|
74
|
-
exists(path.join(appSupport, 'opencode')) ||
|
|
75
|
-
exists(path.join(appData, 'opencode')) ||
|
|
76
|
-
exists(path.join(process.cwd(), '.opencode')),
|
|
77
|
-
dataDir: process.env.OPENCODE_DIR ||
|
|
78
|
-
(exists(path.join(HOME, '.opencode'))
|
|
79
|
-
? path.join(HOME, '.opencode')
|
|
80
|
-
: exists(path.join(HOME, '.config', 'opencode'))
|
|
81
|
-
? path.join(HOME, '.config', 'opencode')
|
|
82
|
-
: exists(path.join(appSupport, 'opencode'))
|
|
83
|
-
? path.join(appSupport, 'opencode')
|
|
84
|
-
: path.join(process.cwd(), '.opencode')),
|
|
85
|
-
label: 'OpenCode',
|
|
86
|
-
color: '#f43f5e',
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Antigravity Reader
|
|
2
|
-
// Antigravity is Google's AI coding IDE (VSCode fork).
|
|
3
|
-
// It stores AI sessions server-side — no local conversation logs available.
|
|
4
|
-
// This reader detects installed projects and reads workspaceStorage metadata.
|
|
5
|
-
// Part of Kodelyth ECC — github.com/sifxprime/kodelyth-ecc
|
|
6
|
-
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
|
|
11
|
-
const HOME = os.homedir();
|
|
12
|
-
|
|
13
|
-
export async function readAntigravityData(dataDir) {
|
|
14
|
-
const candidates = [
|
|
15
|
-
dataDir,
|
|
16
|
-
path.join(HOME, 'Library', 'Application Support', 'Antigravity'),
|
|
17
|
-
path.join(HOME, 'AppData', 'Roaming', 'Antigravity'),
|
|
18
|
-
path.join(HOME, '.antigravity'),
|
|
19
|
-
].filter(Boolean);
|
|
20
|
-
const appSupportDir = candidates.find(d => fs.existsSync(d)) || candidates[0];
|
|
21
|
-
const wsDir = path.join(appSupportDir, 'User', 'workspaceStorage');
|
|
22
|
-
|
|
23
|
-
// Scan home dir for .agent/ project folders (ECC installs)
|
|
24
|
-
const agentProjects = scanAgentProjects();
|
|
25
|
-
|
|
26
|
-
// Read workspace metadata from workspaceStorage DBs
|
|
27
|
-
const workspaces = readWorkspaces(wsDir);
|
|
28
|
-
|
|
29
|
-
// Antigravity stores AI conversations server-side — no local session JSONL.
|
|
30
|
-
// We return synthetic session records for each known project so the
|
|
31
|
-
// platform shows as "active" in the dashboard with project context.
|
|
32
|
-
const sessions = buildProjectSessions(agentProjects, workspaces);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
sessions,
|
|
36
|
-
available: sessions.length > 0,
|
|
37
|
-
cloudAI: true, // Flag: AI sessions are server-side
|
|
38
|
-
projectCount: agentProjects.length,
|
|
39
|
-
workspaceCount: workspaces.length,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function scanAgentProjects() {
|
|
44
|
-
const projects = [];
|
|
45
|
-
const searchRoots = [HOME, path.join(HOME, 'Downloads'), path.join(HOME, 'Documents')];
|
|
46
|
-
|
|
47
|
-
for (const root of searchRoots) {
|
|
48
|
-
if (!fs.existsSync(root)) continue;
|
|
49
|
-
try {
|
|
50
|
-
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (!entry.isDirectory()) continue;
|
|
53
|
-
const agentPath = path.join(root, entry.name, '.agent');
|
|
54
|
-
if (fs.existsSync(agentPath)) {
|
|
55
|
-
// Check if it has ECC content (not just an empty folder)
|
|
56
|
-
const hasEcc = fs.existsSync(path.join(agentPath, 'kodelyth-ecc-install-state.json'))
|
|
57
|
-
|| fs.existsSync(path.join(agentPath, 'rules'))
|
|
58
|
-
|| fs.existsSync(path.join(agentPath, 'workflows'));
|
|
59
|
-
if (hasEcc) {
|
|
60
|
-
projects.push({
|
|
61
|
-
name: entry.name,
|
|
62
|
-
path: path.join(root, entry.name),
|
|
63
|
-
hasEcc: true,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch { /* skip inaccessible directories */ }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return projects;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function readWorkspaces(wsDir) {
|
|
75
|
-
if (!fs.existsSync(wsDir)) return [];
|
|
76
|
-
const workspaces = [];
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const entries = fs.readdirSync(wsDir, { withFileTypes: true });
|
|
80
|
-
for (const entry of entries) {
|
|
81
|
-
if (!entry.isDirectory() || entry.name === 'History') continue;
|
|
82
|
-
workspaces.push({ id: entry.name });
|
|
83
|
-
}
|
|
84
|
-
} catch { /* skip */ }
|
|
85
|
-
|
|
86
|
-
return workspaces;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function buildProjectSessions(agentProjects, workspaces) {
|
|
90
|
-
if (!agentProjects.length && !workspaces.length) return [];
|
|
91
|
-
|
|
92
|
-
const now = new Date().toISOString();
|
|
93
|
-
const today = now.slice(0, 10);
|
|
94
|
-
|
|
95
|
-
// One synthetic session per ECC-enabled project
|
|
96
|
-
return agentProjects.map(proj => ({
|
|
97
|
-
id: `ag-${proj.name}`,
|
|
98
|
-
platform: 'antigravity',
|
|
99
|
-
project: proj.name,
|
|
100
|
-
date: today,
|
|
101
|
-
lastDate: today,
|
|
102
|
-
timestamp: now,
|
|
103
|
-
lastActivity: now,
|
|
104
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
105
|
-
cost: 0,
|
|
106
|
-
cacheSaved: 0,
|
|
107
|
-
messageCount: 0,
|
|
108
|
-
agents: [],
|
|
109
|
-
agentCalls: 0,
|
|
110
|
-
cloudAI: true, // Conversations are server-side
|
|
111
|
-
note: 'Antigravity AI sessions are stored server-side',
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Claude Code Reader
|
|
2
|
-
// Reads session data from ~/.claude/projects/
|
|
3
|
-
// Part of Kodelyth ECC — github.com/sifxprime/kodelyth-ecc
|
|
4
|
-
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import { tokensToCost, cacheSavings } from '../cost-calculator.js';
|
|
9
|
-
import { detectAgentsInMessages } from '../agent-tracker.js';
|
|
10
|
-
|
|
11
|
-
const HOME = os.homedir();
|
|
12
|
-
|
|
13
|
-
export async function readClaudeData(dataDir) {
|
|
14
|
-
const dir = dataDir || process.env.CLAUDE_DIR || path.join(HOME, '.claude');
|
|
15
|
-
if (!fs.existsSync(dir)) return { sessions: [], available: false };
|
|
16
|
-
|
|
17
|
-
const projectsDir = path.join(dir, 'projects');
|
|
18
|
-
if (!fs.existsSync(projectsDir)) return { sessions: [], available: true };
|
|
19
|
-
|
|
20
|
-
const sessions = [];
|
|
21
|
-
const projectDirs = fs.readdirSync(projectsDir, { withFileTypes: true })
|
|
22
|
-
.filter(d => d.isDirectory())
|
|
23
|
-
.map(d => path.join(projectsDir, d.name));
|
|
24
|
-
|
|
25
|
-
for (const projectDir of projectDirs) {
|
|
26
|
-
let files;
|
|
27
|
-
try { files = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl')); }
|
|
28
|
-
catch { continue; }
|
|
29
|
-
|
|
30
|
-
for (const file of files) {
|
|
31
|
-
try {
|
|
32
|
-
const session = parseSessionFile(path.join(projectDir, file), projectDir);
|
|
33
|
-
if (session) sessions.push(session);
|
|
34
|
-
} catch { /* skip malformed */ }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Also merge our hook-based tracking file for richer agent data
|
|
39
|
-
const trackingFile = path.join(dir, 'kodelyth-agent-tracking.jsonl');
|
|
40
|
-
const hookTracking = readTrackingFile(trackingFile);
|
|
41
|
-
|
|
42
|
-
// Enrich sessions with hook tracking data
|
|
43
|
-
if (hookTracking.length > 0) {
|
|
44
|
-
mergeHookTracking(sessions, hookTracking);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
sessions.sort((a, b) => b.timestamp?.localeCompare(a.timestamp));
|
|
48
|
-
return { sessions, available: true };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function parseSessionFile(filePath, projectDir) {
|
|
52
|
-
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
53
|
-
if (!raw) return null;
|
|
54
|
-
|
|
55
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
56
|
-
const messages = [];
|
|
57
|
-
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
58
|
-
let firstTs = null, lastTs = null;
|
|
59
|
-
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
try {
|
|
62
|
-
const entry = JSON.parse(line);
|
|
63
|
-
if (entry.timestamp) {
|
|
64
|
-
if (!firstTs) firstTs = entry.timestamp;
|
|
65
|
-
lastTs = entry.timestamp;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Usage from assistant turns
|
|
69
|
-
const usage = entry.message?.usage || entry.usage;
|
|
70
|
-
if (usage) {
|
|
71
|
-
tokens.input += usage.input_tokens || 0;
|
|
72
|
-
tokens.output += usage.output_tokens || 0;
|
|
73
|
-
tokens.cacheRead += usage.cache_read_input_tokens || 0;
|
|
74
|
-
tokens.cacheWrite += usage.cache_creation_input_tokens || 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Collect messages for agent detection
|
|
78
|
-
const role = entry.message?.role || entry.role;
|
|
79
|
-
const content = entry.message?.content || entry.content;
|
|
80
|
-
if (role && content) {
|
|
81
|
-
messages.push({
|
|
82
|
-
role,
|
|
83
|
-
content: typeof content === 'string' ? content : JSON.stringify(content),
|
|
84
|
-
timestamp: entry.timestamp,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
} catch { /* skip */ }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!firstTs) return null;
|
|
91
|
-
|
|
92
|
-
const agents = detectAgentsInMessages(messages);
|
|
93
|
-
const cost = tokensToCost(tokens);
|
|
94
|
-
const saved = cacheSavings(tokens);
|
|
95
|
-
const project = path.basename(projectDir).replace(/^-+/, '').replace(/-/g, '/');
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
id: path.basename(filePath, '.jsonl'),
|
|
99
|
-
platform: 'claude',
|
|
100
|
-
project,
|
|
101
|
-
date: firstTs.slice(0, 10),
|
|
102
|
-
lastDate: lastTs ? lastTs.slice(0, 10) : firstTs.slice(0, 10),
|
|
103
|
-
timestamp: firstTs,
|
|
104
|
-
lastActivity: lastTs,
|
|
105
|
-
tokens: { ...tokens, total: tokens.input + tokens.output },
|
|
106
|
-
cost: round(cost),
|
|
107
|
-
cacheSaved: round(saved),
|
|
108
|
-
messageCount: messages.length,
|
|
109
|
-
agents,
|
|
110
|
-
agentCalls: agents.length,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function readTrackingFile(filePath) {
|
|
115
|
-
if (!fs.existsSync(filePath)) return [];
|
|
116
|
-
return fs.readFileSync(filePath, 'utf-8')
|
|
117
|
-
.split('\n').filter(Boolean)
|
|
118
|
-
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
119
|
-
.filter(Boolean);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function mergeHookTracking(sessions, tracking) {
|
|
123
|
-
for (const entry of tracking) {
|
|
124
|
-
const session = sessions.find(s => s.date === entry.date);
|
|
125
|
-
if (session && entry.agent) {
|
|
126
|
-
const already = session.agents.some(a => a.agent === entry.agent && a.timestamp === entry.timestamp);
|
|
127
|
-
if (!already) {
|
|
128
|
-
session.agents.push({ agent: entry.agent, timestamp: entry.timestamp, role: 'hook' });
|
|
129
|
-
session.agentCalls++;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function round(n) { return Math.round(n * 10000) / 10000; }
|