kodelyth-ecc 1.2.1 → 1.3.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 +32 -0
- package/CLAUDE.md +72 -63
- package/KODELYTH.md +79 -44
- package/README.md +189 -204
- 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/release-captain.md +190 -0
- package/install.ps1 +9 -9
- package/install.sh +11 -97
- package/package.json +2 -2
- package/rules/common/agent-intent-routing.md +337 -0
- package/skills/agent-handoff/SKILL.md +184 -0
- package/skills/intent-routing/SKILL.md +134 -0
- package/dashboard/lib/agent-tracker.js +0 -366
- package/dashboard/lib/aggregator.js +0 -118
- 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 -126
- package/dashboard/package.json +0 -24
- package/dashboard/public/index.html +0 -1147
- 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,201 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — OpenCode Reader
|
|
2
|
-
// Reads OpenCode session logs from ~/.opencode/ and project .opencode/
|
|
3
|
-
// OpenCode stores conversations in JSONL and JSON formats
|
|
4
|
-
// Part of Kodelyth ECC — github.com/sifxprime/kodelyth-ecc
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import os from 'node:os';
|
|
9
|
-
import { detectAgentsInMessages } from '../agent-tracker.js';
|
|
10
|
-
|
|
11
|
-
const HOME = os.homedir();
|
|
12
|
-
|
|
13
|
-
export async function readOpenCodeData(dataDir) {
|
|
14
|
-
// OpenCode may store data in multiple locations
|
|
15
|
-
const searchDirs = [
|
|
16
|
-
dataDir,
|
|
17
|
-
path.join(HOME, '.opencode'),
|
|
18
|
-
path.join(HOME, '.config', 'opencode'),
|
|
19
|
-
path.join(HOME, 'Library', 'Application Support', 'opencode'), // macOS
|
|
20
|
-
path.join(HOME, 'AppData', 'Roaming', 'opencode'), // Windows
|
|
21
|
-
path.join(process.cwd(), '.opencode'), // project-local
|
|
22
|
-
].filter(Boolean);
|
|
23
|
-
|
|
24
|
-
const sessions = [];
|
|
25
|
-
|
|
26
|
-
for (const dir of searchDirs) {
|
|
27
|
-
if (!dir || !fs.existsSync(dir)) continue;
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
await collectSessionsFromDir(dir, sessions);
|
|
31
|
-
} catch { /* skip inaccessible directories */ }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Deduplicate by id
|
|
35
|
-
const seen = new Set();
|
|
36
|
-
const unique = sessions.filter(s => {
|
|
37
|
-
if (seen.has(s.id)) return false;
|
|
38
|
-
seen.add(s.id);
|
|
39
|
-
return true;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
unique.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
43
|
-
|
|
44
|
-
return { sessions: unique, available: unique.length > 0 };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function collectSessionsFromDir(dir, sessions) {
|
|
48
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
49
|
-
|
|
50
|
-
for (const entry of entries) {
|
|
51
|
-
const fullPath = path.join(dir, entry.name);
|
|
52
|
-
|
|
53
|
-
if (entry.isDirectory()) {
|
|
54
|
-
// Recurse one level into sessions/, conversations/, history/ subdirs
|
|
55
|
-
const name = entry.name.toLowerCase();
|
|
56
|
-
if (['sessions', 'conversations', 'history', 'chats', 'logs'].includes(name)) {
|
|
57
|
-
try {
|
|
58
|
-
await collectSessionsFromDir(fullPath, sessions);
|
|
59
|
-
} catch { /* skip */ }
|
|
60
|
-
}
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!entry.isFile()) continue;
|
|
65
|
-
|
|
66
|
-
const isJsonl = entry.name.endsWith('.jsonl');
|
|
67
|
-
const isJson = entry.name.endsWith('.json');
|
|
68
|
-
if (!isJsonl && !isJson) continue;
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const session = parseOpenCodeFile(fullPath);
|
|
72
|
-
if (session) sessions.push(session);
|
|
73
|
-
} catch { /* skip malformed files */ }
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function parseOpenCodeFile(filePath) {
|
|
78
|
-
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
79
|
-
if (!raw) return null;
|
|
80
|
-
|
|
81
|
-
const messages = [];
|
|
82
|
-
let firstTs = null;
|
|
83
|
-
let tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
84
|
-
|
|
85
|
-
// ── Try JSONL (line-by-line events) ─────────────────────────────────────────
|
|
86
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
87
|
-
let jsonlOk = false;
|
|
88
|
-
|
|
89
|
-
for (const line of lines) {
|
|
90
|
-
try {
|
|
91
|
-
const entry = JSON.parse(line);
|
|
92
|
-
if (typeof entry !== 'object' || !entry) continue;
|
|
93
|
-
|
|
94
|
-
// Extract timestamp
|
|
95
|
-
const ts = entry.timestamp || entry.created_at || entry.time || null;
|
|
96
|
-
if (ts && !firstTs) firstTs = ts;
|
|
97
|
-
|
|
98
|
-
// Extract role + content — OpenCode may use different field names
|
|
99
|
-
const role = entry.role
|
|
100
|
-
|| (entry.type === 'user' ? 'user' : null)
|
|
101
|
-
|| (entry.type === 'assistant' ? 'assistant' : null)
|
|
102
|
-
|| (entry.sender === 'human' ? 'user' : null)
|
|
103
|
-
|| (entry.sender === 'ai' ? 'assistant' : null)
|
|
104
|
-
|| 'user';
|
|
105
|
-
|
|
106
|
-
const content = entry.content
|
|
107
|
-
|| entry.message
|
|
108
|
-
|| entry.text
|
|
109
|
-
|| entry.body
|
|
110
|
-
|| (typeof entry.parts === 'string' ? entry.parts : null)
|
|
111
|
-
|| (Array.isArray(entry.parts)
|
|
112
|
-
? entry.parts.map(p => (typeof p === 'string' ? p : p?.text || '')).join(' ')
|
|
113
|
-
: null)
|
|
114
|
-
|| '';
|
|
115
|
-
|
|
116
|
-
if (content) {
|
|
117
|
-
messages.push({ role, content, timestamp: ts });
|
|
118
|
-
jsonlOk = true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Accumulate token usage if present
|
|
122
|
-
if (entry.usage) {
|
|
123
|
-
tokens.input += entry.usage.input_tokens || entry.usage.prompt_tokens || 0;
|
|
124
|
-
tokens.output += entry.usage.output_tokens || entry.usage.completion_tokens || 0;
|
|
125
|
-
tokens.cacheRead += entry.usage.cache_read_input_tokens || 0;
|
|
126
|
-
tokens.cacheWrite+= entry.usage.cache_creation_input_tokens || 0;
|
|
127
|
-
}
|
|
128
|
-
} catch { /* not valid JSON line */ }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── Try single JSON object ───────────────────────────────────────────────────
|
|
132
|
-
if (!jsonlOk || messages.length === 0) {
|
|
133
|
-
try {
|
|
134
|
-
const data = JSON.parse(raw);
|
|
135
|
-
const items = data.messages
|
|
136
|
-
|| data.history
|
|
137
|
-
|| data.conversation
|
|
138
|
-
|| data.turns
|
|
139
|
-
|| [];
|
|
140
|
-
|
|
141
|
-
for (const item of items) {
|
|
142
|
-
if (!item) continue;
|
|
143
|
-
const ts = item.timestamp || item.created_at || null;
|
|
144
|
-
if (ts && !firstTs) firstTs = ts;
|
|
145
|
-
|
|
146
|
-
const role = item.role || item.sender || 'user';
|
|
147
|
-
const content = item.content || item.text || item.message || '';
|
|
148
|
-
|
|
149
|
-
if (content) messages.push({ role, content, timestamp: ts });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Top-level usage
|
|
153
|
-
if (data.usage) {
|
|
154
|
-
tokens.input = data.usage.input_tokens || data.usage.prompt_tokens || 0;
|
|
155
|
-
tokens.output = data.usage.output_tokens || data.usage.completion_tokens || 0;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Top-level timestamp
|
|
159
|
-
if (!firstTs) firstTs = data.timestamp || data.created_at || data.startedAt || null;
|
|
160
|
-
} catch { /* skip */ }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (messages.length === 0) return null;
|
|
164
|
-
|
|
165
|
-
tokens.total = tokens.input + tokens.output;
|
|
166
|
-
|
|
167
|
-
const ts = firstTs || new Date().toISOString();
|
|
168
|
-
const agents = detectAgentsInMessages(messages);
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
id: path.basename(filePath),
|
|
172
|
-
platform: 'opencode',
|
|
173
|
-
project: path.dirname(filePath).split(path.sep).pop() || 'opencode',
|
|
174
|
-
date: ts.slice(0, 10),
|
|
175
|
-
lastDate: ts.slice(0, 10),
|
|
176
|
-
timestamp: ts,
|
|
177
|
-
lastActivity: ts,
|
|
178
|
-
tokens,
|
|
179
|
-
cost: estimateCost(tokens),
|
|
180
|
-
cacheSaved: estimateCacheSavings(tokens),
|
|
181
|
-
messageCount: messages.length,
|
|
182
|
-
agents,
|
|
183
|
-
agentCalls: agents.length,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// OpenCode uses Claude/OpenAI models — approximate pricing
|
|
188
|
-
function estimateCost(tokens) {
|
|
189
|
-
// Sonnet 4 pricing: $3/M input, $15/M output
|
|
190
|
-
const inputCost = (tokens.input / 1_000_000) * 3;
|
|
191
|
-
const outputCost = (tokens.output / 1_000_000) * 15;
|
|
192
|
-
const cacheRead = (tokens.cacheRead / 1_000_000) * 0.3;
|
|
193
|
-
return Math.round((inputCost + outputCost + cacheRead) * 10000) / 10000;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function estimateCacheSavings(tokens) {
|
|
197
|
-
// Cache reads are ~10x cheaper than fresh input
|
|
198
|
-
const fullPrice = (tokens.cacheRead / 1_000_000) * 3;
|
|
199
|
-
const cachePrice = (tokens.cacheRead / 1_000_000) * 0.3;
|
|
200
|
-
return Math.round((fullPrice - cachePrice) * 10000) / 10000;
|
|
201
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Windsurf Reader
|
|
2
|
-
// Reads Windsurf (Codeium) session logs
|
|
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 { detectAgentsInMessages } from '../agent-tracker.js';
|
|
9
|
-
|
|
10
|
-
const HOME = os.homedir();
|
|
11
|
-
|
|
12
|
-
export async function readWindsurfData(dataDir) {
|
|
13
|
-
const candidates = [
|
|
14
|
-
dataDir,
|
|
15
|
-
process.env.WINDSURF_DIR,
|
|
16
|
-
path.join(HOME, 'Library', 'Application Support', 'Windsurf'),
|
|
17
|
-
path.join(HOME, 'AppData', 'Roaming', 'Windsurf'),
|
|
18
|
-
path.join(HOME, '.codeium', 'windsurf'),
|
|
19
|
-
path.join(HOME, '.windsurf'),
|
|
20
|
-
].filter(Boolean);
|
|
21
|
-
|
|
22
|
-
const windsurfDir = candidates.find(d => d && fs.existsSync(d));
|
|
23
|
-
if (!windsurfDir) return { sessions: [], available: false };
|
|
24
|
-
|
|
25
|
-
const sessions = [];
|
|
26
|
-
|
|
27
|
-
// Windsurf stores session data similar to VS Code extension
|
|
28
|
-
const storageDir = path.join(windsurfDir, 'User', 'workspaceStorage');
|
|
29
|
-
if (fs.existsSync(storageDir)) {
|
|
30
|
-
const wsDirs = fs.readdirSync(storageDir, { withFileTypes: true })
|
|
31
|
-
.filter(d => d.isDirectory())
|
|
32
|
-
.map(d => path.join(storageDir, d.name));
|
|
33
|
-
|
|
34
|
-
for (const ws of wsDirs) {
|
|
35
|
-
const session = parseWindsurfWorkspace(ws);
|
|
36
|
-
if (session) sessions.push(session);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check for Cascade conversation files
|
|
41
|
-
const cascadeDir = path.join(windsurfDir, 'cascade');
|
|
42
|
-
if (fs.existsSync(cascadeDir)) {
|
|
43
|
-
const jsonFiles = fs.readdirSync(cascadeDir).filter(f => f.endsWith('.json'));
|
|
44
|
-
for (const file of jsonFiles) {
|
|
45
|
-
try {
|
|
46
|
-
const session = parseCascadeFile(path.join(cascadeDir, file));
|
|
47
|
-
if (session) sessions.push(session);
|
|
48
|
-
} catch { /* skip */ }
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
sessions.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
53
|
-
return { sessions, available: sessions.length > 0 };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parseWindsurfWorkspace(wsDir) {
|
|
57
|
-
const files = fs.readdirSync(wsDir).filter(f =>
|
|
58
|
-
f.includes('cascade') || f.includes('chat') || f.includes('conversation')
|
|
59
|
-
);
|
|
60
|
-
if (!files.length) return null;
|
|
61
|
-
|
|
62
|
-
const messages = [];
|
|
63
|
-
let firstTs = null;
|
|
64
|
-
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
try {
|
|
67
|
-
const data = JSON.parse(fs.readFileSync(path.join(wsDir, file), 'utf-8'));
|
|
68
|
-
const turns = data.turns || data.messages || data.conversations || [];
|
|
69
|
-
for (const turn of turns) {
|
|
70
|
-
const ts = turn.timestamp || turn.createdAt || null;
|
|
71
|
-
if (ts && !firstTs) firstTs = ts;
|
|
72
|
-
const role = turn.type === 'human' ? 'user' : (turn.type === 'ai' ? 'assistant' : (turn.role || 'user'));
|
|
73
|
-
messages.push({ role, content: turn.text || turn.content || '', timestamp: ts });
|
|
74
|
-
}
|
|
75
|
-
} catch { /* skip */ }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!messages.length) return null;
|
|
79
|
-
const ts = firstTs || new Date().toISOString();
|
|
80
|
-
const agents = detectAgentsInMessages(messages);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
id: path.basename(wsDir),
|
|
84
|
-
platform: 'windsurf',
|
|
85
|
-
project: path.basename(wsDir),
|
|
86
|
-
date: ts.slice(0, 10),
|
|
87
|
-
lastDate: ts.slice(0, 10),
|
|
88
|
-
timestamp: ts,
|
|
89
|
-
lastActivity: ts,
|
|
90
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
91
|
-
cost: 0,
|
|
92
|
-
cacheSaved: 0,
|
|
93
|
-
messageCount: messages.length,
|
|
94
|
-
agents,
|
|
95
|
-
agentCalls: agents.length,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function parseCascadeFile(filePath) {
|
|
100
|
-
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
101
|
-
const messages = (data.messages || data.turns || []).map(m => ({
|
|
102
|
-
role: m.role || (m.type === 'human' ? 'user' : 'assistant'),
|
|
103
|
-
content: m.content || m.text || '',
|
|
104
|
-
timestamp: m.timestamp || null,
|
|
105
|
-
}));
|
|
106
|
-
|
|
107
|
-
if (!messages.length) return null;
|
|
108
|
-
const ts = messages[0].timestamp || new Date().toISOString();
|
|
109
|
-
const agents = detectAgentsInMessages(messages);
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
id: path.basename(filePath, '.json'),
|
|
113
|
-
platform: 'windsurf',
|
|
114
|
-
project: path.basename(filePath, '.json'),
|
|
115
|
-
date: ts.slice(0, 10),
|
|
116
|
-
lastDate: ts.slice(0, 10),
|
|
117
|
-
timestamp: ts,
|
|
118
|
-
lastActivity: ts,
|
|
119
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
120
|
-
cost: 0,
|
|
121
|
-
cacheSaved: 0,
|
|
122
|
-
messageCount: messages.length,
|
|
123
|
-
agents,
|
|
124
|
-
agentCalls: agents.length,
|
|
125
|
-
};
|
|
126
|
-
}
|
package/dashboard/package.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kodelyth-lens",
|
|
3
|
-
"version": "1.2.0",
|
|
4
|
-
"description": "Kodelyth Lens — AI coding agent usage dashboard for Claude Code, Cursor, Windsurf, Codex, OpenCode, and Antigravity",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "server.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"kodelyth-lens": "./server.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"start": "node server.js",
|
|
12
|
-
"dev": "node --watch server.js"
|
|
13
|
-
},
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18.0.0"
|
|
16
|
-
},
|
|
17
|
-
"keywords": ["claude-code", "ai-agents", "developer-tools", "kodelyth", "dashboard"],
|
|
18
|
-
"author": "Kodelyth <github.com/sifxprime>",
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "https://github.com/sifxprime/kodelyth-ecc.git"
|
|
23
|
-
}
|
|
24
|
-
}
|