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.
Files changed (46) hide show
  1. package/AGENTS.md +101 -181
  2. package/CHANGELOG.md +67 -0
  3. package/CLAUDE.md +72 -63
  4. package/KODELYTH.md +79 -44
  5. package/README.md +244 -192
  6. package/VERSION +1 -1
  7. package/agents/dependency-doctor.md +120 -0
  8. package/agents/env-debugger.md +154 -0
  9. package/agents/flake-hunter.md +142 -0
  10. package/agents/git-rescue.md +133 -0
  11. package/agents/kodelyth-memory.md +87 -0
  12. package/agents/release-captain.md +190 -0
  13. package/bin/kodelyth-ecc.js +18 -12
  14. package/commands/memory.md +62 -0
  15. package/hooks/hooks.json +26 -0
  16. package/hooks/memory/capture-stop.js +88 -0
  17. package/hooks/memory/inject-start.js +60 -0
  18. package/install.ps1 +28 -9
  19. package/install.sh +11 -97
  20. package/package.json +4 -2
  21. package/rules/common/agent-intent-routing.md +337 -0
  22. package/rules/common/memory-protocol.md +56 -0
  23. package/scripts/memory/cli.js +200 -0
  24. package/scripts/memory/extract.js +176 -0
  25. package/scripts/memory/inject.js +145 -0
  26. package/scripts/memory/store.js +300 -0
  27. package/skills/agent-handoff/SKILL.md +184 -0
  28. package/skills/intent-routing/SKILL.md +134 -0
  29. package/skills/kodelyth-memory/SKILL.md +136 -0
  30. package/tests/memory/store.test.js +121 -0
  31. package/dashboard/lib/agent-tracker.js +0 -366
  32. package/dashboard/lib/aggregator.js +0 -119
  33. package/dashboard/lib/cost-calculator.js +0 -50
  34. package/dashboard/lib/platform-detector.js +0 -89
  35. package/dashboard/lib/readers/antigravity-reader.js +0 -113
  36. package/dashboard/lib/readers/claude-reader.js +0 -135
  37. package/dashboard/lib/readers/codex-reader.js +0 -192
  38. package/dashboard/lib/readers/cursor-reader.js +0 -135
  39. package/dashboard/lib/readers/opencode-reader.js +0 -201
  40. package/dashboard/lib/readers/windsurf-reader.js +0 -146
  41. package/dashboard/package.json +0 -24
  42. package/dashboard/public/index.html +0 -1221
  43. package/dashboard/server.js +0 -119
  44. package/scripts/agent-tracker-hook.js +0 -81
  45. package/social/readme-lens.svg +0 -140
  46. 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; }