kodelyth-ecc 1.2.1 → 1.2.2

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/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.1
1
+ 1.2.2
@@ -68,6 +68,7 @@ export async function aggregateAll() {
68
68
  sessions: allSessions.slice(0, 100),
69
69
  costs: dailyCosts,
70
70
  platforms: platformStats,
71
+ windsurf: { quotaData: windsurf.quotaData || null },
71
72
  updatedAt: new Date().toISOString(),
72
73
  };
73
74
  }
@@ -1,126 +1,146 @@
1
1
  // Kodelyth Lens — Windsurf Reader
2
- // Reads Windsurf (Codeium) session logs
2
+ // Reads Windsurf (Codeium) real quota data from state.vscdb SQLite databases
3
3
  // Part of Kodelyth ECC — github.com/sifxprime/kodelyth-ecc
4
4
 
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import os from 'node:os';
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';
8
9
  import { detectAgentsInMessages } from '../agent-tracker.js';
9
10
 
10
11
  const HOME = os.homedir();
11
12
 
12
- export async function readWindsurfData(dataDir) {
13
- const candidates = [
14
- dataDir,
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,
15
41
  process.env.WINDSURF_DIR,
16
42
  path.join(HOME, 'Library', 'Application Support', 'Windsurf'),
17
43
  path.join(HOME, 'AppData', 'Roaming', 'Windsurf'),
18
44
  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));
45
+ ].filter(Boolean).find(d => fs.existsSync(d)) || null;
46
+ }
33
47
 
34
- for (const ws of wsDirs) {
35
- const session = parseWindsurfWorkspace(ws);
36
- if (session) sessions.push(session);
37
- }
38
- }
48
+ // ── Main reader ────────────────────────────────────────────────────────────────
39
49
 
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 */ }
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
+ };
49
77
  }
50
78
  }
51
79
 
52
- sessions.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
53
- return { sessions, available: sessions.length > 0 };
54
- }
80
+ // ── 2. Read workspace sessions from workspaceStorage SQLite DBs ─────────────
81
+ const sessions = [];
82
+ const wsStorageDir = path.join(windsurfDir, 'User', 'workspaceStorage');
55
83
 
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 });
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 */ }
74
113
  }
75
- } catch { /* skip */ }
76
- }
77
114
 
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
- }
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
+ }
98
143
 
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
- };
144
+ sessions.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
145
+ return { sessions, available: true, quotaData };
126
146
  }
@@ -727,6 +727,17 @@
727
727
  </div>
728
728
  </div>
729
729
 
730
+ <!-- Windsurf Quota Panel (hidden until data loads) -->
731
+ <div class="card" id="windsurfQuota" style="display:none;margin-bottom:0">
732
+ <div class="card-header" style="padding-bottom:8px">
733
+ <div class="card-title">
734
+ <div class="card-title-icon" style="background:#cffafe">🏄</div>
735
+ Windsurf Quota Usage
736
+ </div>
737
+ </div>
738
+ <div class="card-body"></div>
739
+ </div>
740
+
730
741
  <!-- Cost chart + Recent sessions -->
731
742
  <div class="two-col">
732
743
  <div class="card">
@@ -824,11 +835,12 @@ async function refreshData(force = false) {
824
835
  function renderAll(d) {
825
836
  renderStats(d.stats);
826
837
  renderAgents(d.agents);
827
- renderPlatforms(d.platforms);
838
+ renderPlatforms(d.platforms, d.windsurf);
828
839
  renderCostChart(d.costs);
829
840
  renderToday(d.stats);
830
841
  renderSessions(d.sessions);
831
842
  renderSavings(d.stats);
843
+ renderWindsurfQuota(d.windsurf?.quotaData);
832
844
 
833
845
  // Dynamic subtitle — list installed platform names
834
846
  if (d.platforms) {
@@ -915,12 +927,11 @@ function renderAgents(agents) {
915
927
  }).join('');
916
928
  }
917
929
 
918
- // Cloud AI platforms — store conversations server-side
919
- // Antigravity stores AI sessions server-side — only platform with no local conversation logs
920
- const CLOUD_AI_PLATFORMS = new Set(['antigravity']);
930
+ // Cloud AI platforms — store conversations server-side (no local token data)
931
+ const CLOUD_AI_PLATFORMS = new Set(['antigravity', 'windsurf']);
921
932
 
922
933
  // ── Platforms ─────────────────────────────────────────────────────────────────
923
- function renderPlatforms(platforms) {
934
+ function renderPlatforms(platforms, windsurfData) {
924
935
  const grid = document.getElementById('platformGrid');
925
936
  if (!platforms) { grid.innerHTML = '<div class="empty"><div class="empty-icon">❌</div></div>'; return; }
926
937
 
@@ -946,10 +957,39 @@ function renderPlatforms(platforms) {
946
957
  statusClass = 'status-inactive'; statusText = '○ Installed';
947
958
  }
948
959
 
949
- const cloudBadge = cloudAI && present
950
- ? `<div class="cloud-badge">☁ Cloud AI sessions</div>` : '';
951
- const sessDisplay = cloudAI ? (p.sessions > 0 ? fmtNum(p.sessions) + ' proj' : '—') : fmtNum(p.sessions);
952
- const sessLabel = cloudAI ? 'ECC projects' : 'sessions';
960
+ let cloudBadge = '';
961
+ let sessDisplay, sessLabel;
962
+ if (key === 'windsurf' && present) {
963
+ const q = windsurfData?.quotaData;
964
+ if (q) {
965
+ const msgPct = q.totalMessages > 0 ? Math.round((q.usedMessages / q.totalMessages) * 100) : q.dailyUsedPercent;
966
+ sessDisplay = `${q.usedMessages.toLocaleString()} / ${q.totalMessages.toLocaleString()}`;
967
+ sessLabel = 'messages used';
968
+ cloudBadge = `
969
+ <div style="margin-top:8px;font-size:11px;color:var(--muted)">
970
+ <div style="display:flex;justify-content:space-between;margin-bottom:3px">
971
+ <span>${q.planName} plan</span><span>${q.dailyUsedPercent}% daily used</span>
972
+ </div>
973
+ <div style="background:#e2e8f0;border-radius:4px;height:4px;overflow:hidden">
974
+ <div style="background:${meta.color};height:4px;width:${Math.min(q.dailyUsedPercent,100)}%;transition:width .3s"></div>
975
+ </div>
976
+ <div style="display:flex;justify-content:space-between;margin-top:4px">
977
+ <span>Flow: ${q.usedFlowActions} / ${q.totalFlowActions}</span><span>${q.weeklyUsedPercent}% weekly</span>
978
+ </div>
979
+ </div>`;
980
+ } else {
981
+ sessDisplay = p.sessions > 0 ? fmtNum(p.sessions) + ' proj' : '—';
982
+ sessLabel = 'ECC projects';
983
+ cloudBadge = `<div class="cloud-badge">☁ Quota data loading…</div>`;
984
+ }
985
+ } else if (cloudAI && present) {
986
+ sessDisplay = p.sessions > 0 ? fmtNum(p.sessions) + ' proj' : '—';
987
+ sessLabel = 'ECC projects';
988
+ cloudBadge = `<div class="cloud-badge">☁ Cloud AI sessions</div>`;
989
+ } else {
990
+ sessDisplay = fmtNum(p.sessions);
991
+ sessLabel = 'sessions';
992
+ }
953
993
 
954
994
  return `
955
995
  <div class="platform-card ${active || (cloudAI && present) ? 'active' : ''}" style="color:${meta.color}">
@@ -957,7 +997,7 @@ function renderPlatforms(platforms) {
957
997
  <span class="platform-dot-lg" style="background:${meta.color}"></span>
958
998
  ${meta.label}
959
999
  </div>
960
- <div class="platform-sessions" style="color:${(active || (cloudAI && present)) ? meta.color : 'var(--muted)'}">${sessDisplay}</div>
1000
+ <div class="platform-sessions" style="color:${(active || (cloudAI && present)) ? meta.color : 'var(--muted)'};font-size:${key==='windsurf'&&windsurfData?.quotaData?'14px':'22px'}">${sessDisplay}</div>
961
1001
  <div class="platform-label">${sessLabel}</div>
962
1002
  <div class="platform-status ${statusClass}">${statusText}</div>
963
1003
  ${cloudBadge}
@@ -965,6 +1005,40 @@ function renderPlatforms(platforms) {
965
1005
  }).join('');
966
1006
  }
967
1007
 
1008
+ // ── Windsurf Quota Panel ─────────────────────────────────────────────────────
1009
+ function renderWindsurfQuota(q) {
1010
+ const el = document.getElementById('windsurfQuota');
1011
+ if (!el) return;
1012
+ if (!q) { el.style.display = 'none'; return; }
1013
+ el.style.display = 'block';
1014
+ el.querySelector('.card-body').innerHTML = `
1015
+ <div style="display:flex;gap:24px;flex-wrap:wrap;align-items:center">
1016
+ <div>
1017
+ <div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">Windsurf ${q.planName}</div>
1018
+ <div style="font-size:22px;font-weight:800;color:#06b6d4;font-variant-numeric:tabular-nums">${q.usedMessages.toLocaleString()} <span style="font-size:14px;font-weight:400;color:var(--muted)">/ ${q.totalMessages.toLocaleString()} msgs</span></div>
1019
+ </div>
1020
+ <div style="flex:1;min-width:160px">
1021
+ <div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:4px">
1022
+ <span>Daily quota</span><span>${q.dailyUsedPercent}% used</span>
1023
+ </div>
1024
+ <div style="background:#e2e8f0;border-radius:6px;height:8px;overflow:hidden">
1025
+ <div style="background:#06b6d4;height:8px;width:${Math.min(q.dailyUsedPercent,100)}%;border-radius:6px;transition:width .4s"></div>
1026
+ </div>
1027
+ <div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-top:6px">
1028
+ <span>Weekly quota</span><span>${q.weeklyUsedPercent}% used</span>
1029
+ </div>
1030
+ <div style="background:#e2e8f0;border-radius:6px;height:8px;overflow:hidden;margin-top:4px">
1031
+ <div style="background:#0e7490;height:8px;width:${Math.min(q.weeklyUsedPercent,100)}%;border-radius:6px;transition:width .4s"></div>
1032
+ </div>
1033
+ </div>
1034
+ <div style="text-align:right">
1035
+ <div style="font-size:11px;color:var(--muted)">Flow Actions</div>
1036
+ <div style="font-size:16px;font-weight:700;color:#06b6d4">${q.usedFlowActions.toLocaleString()} <span style="font-size:12px;color:var(--muted)">/ ${q.totalFlowActions.toLocaleString()}</span></div>
1037
+ ${q.endDate ? `<div style="font-size:10px;color:var(--muted);margin-top:2px">Resets ${q.endDate}</div>` : ''}
1038
+ </div>
1039
+ </div>`;
1040
+ }
1041
+
968
1042
  // ── Cost chart ────────────────────────────────────────────────────────────────
969
1043
  function renderCostChart(dailyCosts) {
970
1044
  const svg = document.getElementById('costChart');
@@ -1060,8 +1134,8 @@ function renderSessions(sessions) {
1060
1134
  <td style="max-width:220px">
1061
1135
  <div style="display:flex;flex-wrap:wrap;gap:4px">${agents}${moreAgents}</div>
1062
1136
  </td>
1063
- <td class="td-mono">${fmtTokens(s.tokens?.total || 0)}</td>
1064
- <td class="td-mono">${s.cost > 0 ? '$' + s.cost.toFixed(4) : '—'}</td>
1137
+ <td class="td-mono">${s.cost === null ? '<span style="color:#94a3b8">quota</span>' : fmtTokens(s.tokens?.total || 0)}</td>
1138
+ <td class="td-mono">${s.cost === null ? '<span style="color:#94a3b8">—</span>' : s.cost > 0 ? '$' + s.cost.toFixed(4) : '—'}</td>
1065
1139
  <td style="white-space:nowrap;color:var(--muted)">${fmtDate(s.date)}</td>
1066
1140
  </tr>`;
1067
1141
  }).join('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodelyth-ecc",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Production-grade AI coding toolkit — 53 agents, 185 skills, 79 commands. Works with Claude Code, Windsurf, Cursor, Codex, Antigravity, and OpenCode.",
5
5
  "author": "Kodelyth <github.com/sifxprime>",
6
6
  "license": "MIT",