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,192 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Codex CLI Reader
|
|
2
|
-
// Reads OpenAI Codex CLI sessions from ~/.codex/state_5.sqlite (primary)
|
|
3
|
-
// and falls back to JSONL/JSON files in sessions/, history/, logs/
|
|
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 { execSync } from 'node:child_process';
|
|
10
|
-
import { detectAgentsInMessages } from '../agent-tracker.js';
|
|
11
|
-
|
|
12
|
-
const HOME = os.homedir();
|
|
13
|
-
|
|
14
|
-
export async function readCodexData(dataDir) {
|
|
15
|
-
const dir = dataDir || process.env.CODEX_DIR || path.join(HOME, '.codex');
|
|
16
|
-
if (!fs.existsSync(dir)) return { sessions: [], available: false };
|
|
17
|
-
|
|
18
|
-
const sessions = [];
|
|
19
|
-
|
|
20
|
-
// ── Primary: state_5.sqlite (Codex stores threads here) ─────────────────────
|
|
21
|
-
const sqliteFile = findSqliteFile(dir);
|
|
22
|
-
if (sqliteFile) {
|
|
23
|
-
const sqliteSessions = readSqliteSessions(sqliteFile);
|
|
24
|
-
sessions.push(...sqliteSessions);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ── Fallback: JSONL / JSON files ─────────────────────────────────────────────
|
|
28
|
-
if (sessions.length === 0) {
|
|
29
|
-
for (const subdir of ['sessions', 'history', 'logs', '']) {
|
|
30
|
-
const target = subdir ? path.join(dir, subdir) : dir;
|
|
31
|
-
if (!fs.existsSync(target)) continue;
|
|
32
|
-
|
|
33
|
-
const files = fs.readdirSync(target).filter(f =>
|
|
34
|
-
(f.endsWith('.jsonl') || f.endsWith('.json')) && !f.endsWith('.sqlite')
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
for (const file of files) {
|
|
38
|
-
try {
|
|
39
|
-
const session = parseCodexJsonFile(path.join(target, file));
|
|
40
|
-
if (session) sessions.push(session);
|
|
41
|
-
} catch { /* skip */ }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
sessions.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
47
|
-
return { sessions, available: sessions.length > 0 };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ── SQLite helpers ─────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
function findSqliteFile(dir) {
|
|
53
|
-
// Codex v2+ uses state_5.sqlite, older versions used state.sqlite
|
|
54
|
-
for (const name of ['state_5.sqlite', 'state.sqlite', 'sessions.sqlite']) {
|
|
55
|
-
const p = path.join(dir, name);
|
|
56
|
-
if (fs.existsSync(p)) return p;
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function sqlite3Available() {
|
|
62
|
-
try {
|
|
63
|
-
execSync('sqlite3 --version', { stdio: 'pipe', timeout: 3000 });
|
|
64
|
-
return true;
|
|
65
|
-
} catch { return false; }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function querySqliteJson(dbPath, sql) {
|
|
69
|
-
try {
|
|
70
|
-
// Use -json mode: handles | and newlines in content safely
|
|
71
|
-
const raw = execSync(
|
|
72
|
-
`sqlite3 -json "${dbPath}" "${sql.replace(/"/g, '\\"')}"`,
|
|
73
|
-
{ stdio: 'pipe', timeout: 5000, encoding: 'utf-8' }
|
|
74
|
-
).trim();
|
|
75
|
-
return raw ? JSON.parse(raw) : [];
|
|
76
|
-
} catch { return []; }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function readSqliteSessions(dbPath) {
|
|
80
|
-
if (!sqlite3Available()) return [];
|
|
81
|
-
|
|
82
|
-
// Query the threads table — primary source of Codex sessions
|
|
83
|
-
const rows = querySqliteJson(
|
|
84
|
-
dbPath,
|
|
85
|
-
'SELECT id, created_at, title, model, cwd, COALESCE(tokens_used, 0) AS tokens_used FROM threads ORDER BY created_at DESC'
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
return rows.map(row => {
|
|
89
|
-
// created_at is Unix seconds (10 digits)
|
|
90
|
-
const tsMs = row.created_at
|
|
91
|
-
? parseInt(row.created_at, 10) * 1000
|
|
92
|
-
: Date.now();
|
|
93
|
-
const ts = new Date(tsMs).toISOString();
|
|
94
|
-
const tokens = parseInt(row.tokens_used, 10) || 0;
|
|
95
|
-
|
|
96
|
-
// Use title for agent detection (doesn't expose raw user messages in UI)
|
|
97
|
-
const messages = [
|
|
98
|
-
row.title ? { role: 'user', content: row.title, timestamp: ts } : null,
|
|
99
|
-
row.first_user_message ? { role: 'user', content: row.first_user_message, timestamp: ts } : null,
|
|
100
|
-
].filter(Boolean);
|
|
101
|
-
|
|
102
|
-
const agents = detectAgentsInMessages(messages);
|
|
103
|
-
|
|
104
|
-
// Safe project label (strip path, no secrets)
|
|
105
|
-
const project = row.cwd ? path.basename(row.cwd) : 'codex';
|
|
106
|
-
const titleSafe = (row.title || '').slice(0, 80).replace(/\n/g, ' ');
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
id: row.id || path.basename(dbPath),
|
|
110
|
-
platform: 'codex',
|
|
111
|
-
project,
|
|
112
|
-
date: ts.slice(0, 10),
|
|
113
|
-
lastDate: ts.slice(0, 10),
|
|
114
|
-
timestamp: ts,
|
|
115
|
-
lastActivity: ts,
|
|
116
|
-
title: titleSafe || 'Codex session',
|
|
117
|
-
model: row.model || 'gpt',
|
|
118
|
-
tokens: {
|
|
119
|
-
input: Math.round(tokens * 0.7),
|
|
120
|
-
output: Math.round(tokens * 0.3),
|
|
121
|
-
cacheRead: 0,
|
|
122
|
-
cacheWrite: 0,
|
|
123
|
-
total: tokens,
|
|
124
|
-
},
|
|
125
|
-
cost: estimateCost(tokens),
|
|
126
|
-
cacheSaved: 0,
|
|
127
|
-
messageCount: 1,
|
|
128
|
-
agents,
|
|
129
|
-
agentCalls: agents.length,
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── JSONL / JSON fallback ──────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
function parseCodexJsonFile(filePath) {
|
|
137
|
-
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
138
|
-
if (!raw) return null;
|
|
139
|
-
|
|
140
|
-
const messages = [];
|
|
141
|
-
let firstTs = null;
|
|
142
|
-
|
|
143
|
-
for (const line of raw.split('\n').filter(Boolean)) {
|
|
144
|
-
try {
|
|
145
|
-
const entry = JSON.parse(line);
|
|
146
|
-
const ts = entry.timestamp || entry.created_at || null;
|
|
147
|
-
if (ts && !firstTs) firstTs = ts;
|
|
148
|
-
const role = entry.role || (entry.type === 'input' ? 'user' : 'assistant');
|
|
149
|
-
const content = entry.content || entry.message || entry.text || '';
|
|
150
|
-
if (content) messages.push({ role, content, timestamp: ts });
|
|
151
|
-
} catch { /* skip */ }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (!messages.length) {
|
|
155
|
-
try {
|
|
156
|
-
const data = JSON.parse(raw);
|
|
157
|
-
const items = data.messages || data.history || [];
|
|
158
|
-
for (const item of items) {
|
|
159
|
-
const ts = item.timestamp || null;
|
|
160
|
-
if (ts && !firstTs) firstTs = ts;
|
|
161
|
-
messages.push({ role: item.role || 'user', content: item.content || item.text || '', timestamp: ts });
|
|
162
|
-
}
|
|
163
|
-
if (!firstTs) firstTs = data.timestamp || data.created_at || null;
|
|
164
|
-
} catch { /* skip */ }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!messages.length) return null;
|
|
168
|
-
|
|
169
|
-
const ts = firstTs || new Date().toISOString();
|
|
170
|
-
const agents = detectAgentsInMessages(messages);
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
id: path.basename(filePath),
|
|
174
|
-
platform: 'codex',
|
|
175
|
-
project: path.basename(filePath),
|
|
176
|
-
date: ts.slice(0, 10),
|
|
177
|
-
timestamp: ts,
|
|
178
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
179
|
-
cost: 0,
|
|
180
|
-
cacheSaved: 0,
|
|
181
|
-
messageCount: messages.length,
|
|
182
|
-
agents,
|
|
183
|
-
agentCalls: agents.length,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function estimateCost(tokens) {
|
|
188
|
-
// Codex/GPT-5.3 pricing approximation: $2/M input, $8/M output
|
|
189
|
-
const inputCost = (tokens * 0.7 / 1_000_000) * 2;
|
|
190
|
-
const outputCost = (tokens * 0.3 / 1_000_000) * 8;
|
|
191
|
-
return Math.round((inputCost + outputCost) * 10000) / 10000;
|
|
192
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Cursor Reader
|
|
2
|
-
// Reads Cursor session/conversation 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
|
-
import { tokensToCost } from '../cost-calculator.js';
|
|
10
|
-
|
|
11
|
-
const HOME = os.homedir();
|
|
12
|
-
|
|
13
|
-
export async function readCursorData(dataDir) {
|
|
14
|
-
const candidates = [
|
|
15
|
-
dataDir,
|
|
16
|
-
process.env.CURSOR_DIR,
|
|
17
|
-
path.join(HOME, 'Library', 'Application Support', 'Cursor'),
|
|
18
|
-
path.join(HOME, 'AppData', 'Roaming', 'Cursor'),
|
|
19
|
-
path.join(HOME, '.cursor'),
|
|
20
|
-
].filter(Boolean);
|
|
21
|
-
|
|
22
|
-
const cursorDir = candidates.find(d => d && fs.existsSync(d));
|
|
23
|
-
if (!cursorDir) return { sessions: [], available: false };
|
|
24
|
-
|
|
25
|
-
const sessions = [];
|
|
26
|
-
|
|
27
|
-
// Cursor stores workspace state in workspaceStorage
|
|
28
|
-
const wsDir = path.join(cursorDir, 'User', 'workspaceStorage');
|
|
29
|
-
if (fs.existsSync(wsDir)) {
|
|
30
|
-
const wsDirs = fs.readdirSync(wsDir, { withFileTypes: true })
|
|
31
|
-
.filter(d => d.isDirectory())
|
|
32
|
-
.map(d => path.join(wsDir, d.name));
|
|
33
|
-
|
|
34
|
-
for (const ws of wsDirs) {
|
|
35
|
-
const session = parseCursorWorkspace(ws);
|
|
36
|
-
if (session) sessions.push(session);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Also look for .cursor/chat/ directories in project roots
|
|
41
|
-
const chatDir = path.join(cursorDir, 'chat');
|
|
42
|
-
if (fs.existsSync(chatDir)) {
|
|
43
|
-
const chatFiles = fs.readdirSync(chatDir).filter(f => f.endsWith('.json'));
|
|
44
|
-
for (const file of chatFiles) {
|
|
45
|
-
try {
|
|
46
|
-
const session = parseCursorChatFile(path.join(chatDir, 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: true };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parseCursorWorkspace(wsDir) {
|
|
57
|
-
// Look for conversation or chat files
|
|
58
|
-
const files = fs.readdirSync(wsDir).filter(f =>
|
|
59
|
-
f.includes('conversation') || f.includes('chat') || f.includes('composer')
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
if (!files.length) return null;
|
|
63
|
-
|
|
64
|
-
const messages = [];
|
|
65
|
-
let firstTs = null;
|
|
66
|
-
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
try {
|
|
69
|
-
const data = JSON.parse(fs.readFileSync(path.join(wsDir, file), 'utf-8'));
|
|
70
|
-
const items = data.conversations || data.messages || data.items || [];
|
|
71
|
-
|
|
72
|
-
for (const item of items) {
|
|
73
|
-
if (item.type === 'human' || item.role === 'user') {
|
|
74
|
-
const ts = item.timestamp || item.createdAt || null;
|
|
75
|
-
if (ts && !firstTs) firstTs = ts;
|
|
76
|
-
messages.push({ role: 'user', content: item.text || item.content || '', timestamp: ts });
|
|
77
|
-
}
|
|
78
|
-
if (item.type === 'ai' || item.role === 'assistant') {
|
|
79
|
-
messages.push({ role: 'assistant', content: item.text || item.content || '', timestamp: item.timestamp });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch { /* skip */ }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!messages.length) return null;
|
|
86
|
-
|
|
87
|
-
const agents = detectAgentsInMessages(messages);
|
|
88
|
-
const ts = firstTs || new Date().toISOString();
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
id: path.basename(wsDir),
|
|
92
|
-
platform: 'cursor',
|
|
93
|
-
project: path.basename(wsDir),
|
|
94
|
-
date: ts.slice(0, 10),
|
|
95
|
-
lastDate: ts.slice(0, 10),
|
|
96
|
-
timestamp: ts,
|
|
97
|
-
lastActivity: ts,
|
|
98
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
99
|
-
cost: 0,
|
|
100
|
-
cacheSaved: 0,
|
|
101
|
-
messageCount: messages.length,
|
|
102
|
-
agents,
|
|
103
|
-
agentCalls: agents.length,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function parseCursorChatFile(filePath) {
|
|
108
|
-
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
109
|
-
const messages = (data.messages || []).map(m => ({
|
|
110
|
-
role: m.role || (m.type === 'human' ? 'user' : 'assistant'),
|
|
111
|
-
content: m.content || m.text || '',
|
|
112
|
-
timestamp: m.timestamp || null,
|
|
113
|
-
}));
|
|
114
|
-
|
|
115
|
-
if (!messages.length) return null;
|
|
116
|
-
|
|
117
|
-
const ts = messages[0].timestamp || new Date().toISOString();
|
|
118
|
-
const agents = detectAgentsInMessages(messages);
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
id: path.basename(filePath, '.json'),
|
|
122
|
-
platform: 'cursor',
|
|
123
|
-
project: path.basename(filePath, '.json'),
|
|
124
|
-
date: ts.slice(0, 10),
|
|
125
|
-
lastDate: ts.slice(0, 10),
|
|
126
|
-
timestamp: ts,
|
|
127
|
-
lastActivity: ts,
|
|
128
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
129
|
-
cost: 0,
|
|
130
|
-
cacheSaved: 0,
|
|
131
|
-
messageCount: messages.length,
|
|
132
|
-
agents,
|
|
133
|
-
agentCalls: agents.length,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
@@ -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,146 +0,0 @@
|
|
|
1
|
-
// Kodelyth Lens — Windsurf Reader
|
|
2
|
-
// Reads Windsurf (Codeium) real quota data from state.vscdb SQLite databases
|
|
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 { execSync } from 'node:child_process';
|
|
9
|
-
import { detectAgentsInMessages } from '../agent-tracker.js';
|
|
10
|
-
|
|
11
|
-
const HOME = os.homedir();
|
|
12
|
-
|
|
13
|
-
// ── SQLite helpers (same pattern as codex-reader) ──────────────────────────────
|
|
14
|
-
|
|
15
|
-
function sqlite3Available() {
|
|
16
|
-
try { execSync('sqlite3 --version', { stdio: 'pipe', timeout: 3000 }); return true; }
|
|
17
|
-
catch { return false; }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function querySqliteJson(dbPath, sql) {
|
|
21
|
-
try {
|
|
22
|
-
const raw = execSync(
|
|
23
|
-
`sqlite3 -json "${dbPath}" "${sql.replace(/"/g, '\\"')}"`,
|
|
24
|
-
{ stdio: 'pipe', timeout: 5000, encoding: 'utf-8' }
|
|
25
|
-
).trim();
|
|
26
|
-
return raw ? JSON.parse(raw) : [];
|
|
27
|
-
} catch { return []; }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function readDbKey(dbPath, key) {
|
|
31
|
-
const rows = querySqliteJson(dbPath, `SELECT value FROM ItemTable WHERE key='${key}'`);
|
|
32
|
-
if (!rows.length || !rows[0].value) return null;
|
|
33
|
-
try { return JSON.parse(rows[0].value); } catch { return rows[0].value; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Find Windsurf data directory ───────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
function findWindsurfDir(override) {
|
|
39
|
-
return [
|
|
40
|
-
override,
|
|
41
|
-
process.env.WINDSURF_DIR,
|
|
42
|
-
path.join(HOME, 'Library', 'Application Support', 'Windsurf'),
|
|
43
|
-
path.join(HOME, 'AppData', 'Roaming', 'Windsurf'),
|
|
44
|
-
path.join(HOME, '.codeium', 'windsurf'),
|
|
45
|
-
].filter(Boolean).find(d => fs.existsSync(d)) || null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ── Main reader ────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
export async function readWindsurfData(dataDir) {
|
|
51
|
-
const windsurfDir = findWindsurfDir(dataDir);
|
|
52
|
-
if (!windsurfDir) return { sessions: [], available: false, quotaData: null };
|
|
53
|
-
|
|
54
|
-
const hasSqlite = sqlite3Available();
|
|
55
|
-
let quotaData = null;
|
|
56
|
-
|
|
57
|
-
// ── 1. Read real quota / plan info from global state.vscdb ─────────────────
|
|
58
|
-
const globalDb = path.join(windsurfDir, 'User', 'globalStorage', 'state.vscdb');
|
|
59
|
-
if (hasSqlite && fs.existsSync(globalDb)) {
|
|
60
|
-
const plan = readDbKey(globalDb, 'windsurf.settings.cachedPlanInfo');
|
|
61
|
-
if (plan?.usage) {
|
|
62
|
-
const daily = plan.quotaUsage?.dailyRemainingPercent ?? 100;
|
|
63
|
-
const weekly = plan.quotaUsage?.weeklyRemainingPercent ?? 100;
|
|
64
|
-
quotaData = {
|
|
65
|
-
planName: plan.planName || 'Unknown',
|
|
66
|
-
usedMessages: plan.usage.usedMessages || 0,
|
|
67
|
-
totalMessages: plan.usage.messages || 0,
|
|
68
|
-
usedFlowActions: plan.usage.usedFlowActions || 0,
|
|
69
|
-
totalFlowActions: plan.usage.flowActions || 0,
|
|
70
|
-
dailyUsedPercent: Math.round(100 - daily),
|
|
71
|
-
weeklyUsedPercent: Math.round(100 - weekly),
|
|
72
|
-
dailyRemainingPercent: Math.round(daily),
|
|
73
|
-
weeklyRemainingPercent:Math.round(weekly),
|
|
74
|
-
startDate: plan.startTimestamp ? new Date(plan.startTimestamp).toISOString().slice(0, 10) : null,
|
|
75
|
-
endDate: plan.endTimestamp ? new Date(plan.endTimestamp).toISOString().slice(0, 10) : null,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── 2. Read workspace sessions from workspaceStorage SQLite DBs ─────────────
|
|
81
|
-
const sessions = [];
|
|
82
|
-
const wsStorageDir = path.join(windsurfDir, 'User', 'workspaceStorage');
|
|
83
|
-
|
|
84
|
-
if (hasSqlite && fs.existsSync(wsStorageDir)) {
|
|
85
|
-
const wsDirs = fs.readdirSync(wsStorageDir, { withFileTypes: true })
|
|
86
|
-
.filter(d => d.isDirectory())
|
|
87
|
-
.map(d => path.join(wsStorageDir, d.name));
|
|
88
|
-
|
|
89
|
-
for (const wsDir of wsDirs) {
|
|
90
|
-
const db = path.join(wsDir, 'state.vscdb');
|
|
91
|
-
if (!fs.existsSync(db)) continue;
|
|
92
|
-
|
|
93
|
-
// Determine timestamp: readDateBaseline2 → epoch dirname → skip
|
|
94
|
-
const baseline = readDbKey(db, 'agentSessions.readDateBaseline2');
|
|
95
|
-
const dirName = path.basename(wsDir);
|
|
96
|
-
let ts = null;
|
|
97
|
-
if (baseline && !isNaN(parseInt(baseline, 10))) {
|
|
98
|
-
ts = new Date(parseInt(baseline, 10)).toISOString();
|
|
99
|
-
} else if (/^\d{13}$/.test(dirName)) {
|
|
100
|
-
ts = new Date(parseInt(dirName, 10)).toISOString();
|
|
101
|
-
}
|
|
102
|
-
if (!ts) continue;
|
|
103
|
-
|
|
104
|
-
// Resolve project name from workspace.json
|
|
105
|
-
let projectName = dirName;
|
|
106
|
-
const wsJson = path.join(wsDir, 'workspace.json');
|
|
107
|
-
if (fs.existsSync(wsJson)) {
|
|
108
|
-
try {
|
|
109
|
-
const wd = JSON.parse(fs.readFileSync(wsJson, 'utf-8'));
|
|
110
|
-
const uri = wd.folder || wd.workspace?.folders?.[0]?.uri || '';
|
|
111
|
-
if (uri) projectName = path.basename(decodeURIComponent(uri).replace(/\/$/, ''));
|
|
112
|
-
} catch { /* skip */ }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Check for any agent usage in this workspace
|
|
116
|
-
const agentCache = readDbKey(db, 'agentSessions.state.cache') || [];
|
|
117
|
-
const messages = Array.isArray(agentCache)
|
|
118
|
-
? agentCache.flatMap(s => (s.messages || []).map(m => ({
|
|
119
|
-
role: m.role || 'user',
|
|
120
|
-
content: m.content || m.text || '',
|
|
121
|
-
})))
|
|
122
|
-
: [];
|
|
123
|
-
const agents = detectAgentsInMessages(messages);
|
|
124
|
-
|
|
125
|
-
sessions.push({
|
|
126
|
-
id: dirName,
|
|
127
|
-
platform: 'windsurf',
|
|
128
|
-
project: projectName,
|
|
129
|
-
date: ts.slice(0, 10),
|
|
130
|
-
lastDate: ts.slice(0, 10),
|
|
131
|
-
timestamp: ts,
|
|
132
|
-
lastActivity: ts,
|
|
133
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
134
|
-
cost: null, // Windsurf is quota-based, not per-token
|
|
135
|
-
cacheSaved: 0,
|
|
136
|
-
messageCount: messages.length,
|
|
137
|
-
agents,
|
|
138
|
-
agentCalls: agents.length,
|
|
139
|
-
cloudAI: true,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
sessions.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
145
|
-
return { sessions, available: true, quotaData };
|
|
146
|
-
}
|
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
|
-
}
|