kodelyth-ecc 1.2.2 → 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.
@@ -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; }
@@ -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
- }