kodelyth-ecc 1.2.0 → 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/README.md CHANGED
@@ -19,30 +19,42 @@ Works with **Claude Code**, **Google Antigravity**, **Cursor**, **Codex CLI**, a
19
19
 
20
20
  ## Install
21
21
 
22
- ### Option 1 — npx (Node.js, any platform)
22
+ ### Option 1 — npx from npm (Node.js 18+, any platform)
23
23
 
24
- Requires Node.js 18+. No `git` needed — npx downloads directly from GitHub.
24
+ The simplest install. Node.js is the only requirement.
25
25
 
26
26
  ```bash
27
- npx github:sifxprime/kodelyth-ecc
27
+ npx kodelyth-ecc
28
28
  ```
29
29
 
30
30
  Install for a specific platform:
31
31
 
32
32
  ```bash
33
- npx github:sifxprime/kodelyth-ecc --target windsurf-project # Windsurf (project)
34
- npx github:sifxprime/kodelyth-ecc --target windsurf-home # Windsurf (global)
35
- npx github:sifxprime/kodelyth-ecc --target antigravity # Google Antigravity
36
- npx github:sifxprime/kodelyth-ecc --target cursor-project # Cursor IDE
37
- npx github:sifxprime/kodelyth-ecc --target codex-home # Codex CLI
38
- npx github:sifxprime/kodelyth-ecc --target opencode # OpenCode
33
+ npx kodelyth-ecc --target windsurf-project # Windsurf (project)
34
+ npx kodelyth-ecc --target windsurf-home # Windsurf (global)
35
+ npx kodelyth-ecc --target antigravity # Google Antigravity
36
+ npx kodelyth-ecc --target cursor-project # Cursor IDE
37
+ npx kodelyth-ecc --target codex-home # Codex CLI
38
+ npx kodelyth-ecc --target opencode # OpenCode
39
39
  ```
40
40
 
41
41
  > Node.js not installed? Download it from [nodejs.org](https://nodejs.org) — LTS version recommended.
42
42
 
43
43
  ---
44
44
 
45
- ### Option 2 — curl (macOS / Linux)
45
+ ### Option 2 — npx from GitHub (always latest commit)
46
+
47
+ Use this if you want the very latest unreleased version directly from the main branch.
48
+
49
+ ```bash
50
+ npx github:sifxprime/kodelyth-ecc
51
+ ```
52
+
53
+ Same `--target` flags apply.
54
+
55
+ ---
56
+
57
+ ### Option 3 — curl (macOS / Linux)
46
58
 
47
59
  ```bash
48
60
  curl -fsSL https://raw.githubusercontent.com/sifxprime/kodelyth-ecc/main/install.sh | bash
@@ -56,7 +68,7 @@ curl -fsSL https://raw.githubusercontent.com/sifxprime/kodelyth-ecc/main/install
56
68
 
57
69
  ---
58
70
 
59
- ### Option 3 — clone and run (all platforms)
71
+ ### Option 4 — clone and run (all platforms)
60
72
 
61
73
  ```bash
62
74
  git clone https://github.com/sifxprime/kodelyth-ecc.git
@@ -67,8 +79,10 @@ cd kodelyth-ecc
67
79
  ./install.sh --target windsurf-project # Windsurf
68
80
  ./install.sh --target antigravity # Google Antigravity
69
81
 
70
- # Windows
82
+ # Windows (PowerShell)
71
83
  .\install.ps1
84
+ .\install.ps1 -Target windsurf-project
85
+ .\install.ps1 -Target codex-home
72
86
  ```
73
87
 
74
88
  ---
@@ -138,7 +152,7 @@ PORT=3456 # Dashboard port (default: 3456)
138
152
  | Skills | **185** | Domain knowledge — patterns, testing, security, DevOps |
139
153
  | Commands | **79** | Slash command workflows (`/tdd`, `/plan`, `/code-review`, etc.) |
140
154
  | Hooks | **15+** | Automated lifecycle triggers — quality gates, session memory, cost tracking |
141
- | Rules | **12** | Language-specific coding standards |
155
+ | Rules | **15** | Language-specific coding standards |
142
156
 
143
157
  ---
144
158
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
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/install.ps1 CHANGED
@@ -1,11 +1,14 @@
1
1
  # =============================================================================
2
2
  # Kodelyth ECC — Windows PowerShell Installer
3
- # Supports: Claude Code, Antigravity, Cursor, Codex, OpenCode
3
+ # Supports: Claude Code, Antigravity, Cursor, Codex, Windsurf, OpenCode
4
4
  # Usage:
5
- # .\install.ps1 # Claude Code (default)
6
- # .\install.ps1 -Target antigravity # Google Antigravity
7
- # .\install.ps1 -Target cursor-project # Cursor IDE
8
- # .\install.ps1 -Target codex-home # OpenAI Codex CLI
5
+ # .\install.ps1 # Claude Code (default)
6
+ # .\install.ps1 -Target windsurf-project # Windsurf (project)
7
+ # .\install.ps1 -Target windsurf-home # Windsurf (global)
8
+ # .\install.ps1 -Target antigravity # Google Antigravity
9
+ # .\install.ps1 -Target cursor-project # Cursor IDE
10
+ # .\install.ps1 -Target codex-home # OpenAI Codex CLI
11
+ # .\install.ps1 -Target opencode # OpenCode
9
12
  # =============================================================================
10
13
 
11
14
  param(
@@ -18,7 +21,7 @@ $ErrorActionPreference = "Stop"
18
21
  # ── Banner ────────────────────────────────────────────────────────────────────
19
22
  Write-Host ""
20
23
  Write-Host " Kodelyth ECC — Production-grade AI coding agent toolkit" -ForegroundColor Cyan
21
- Write-Host " 50 agents · 183 skills · 79 commands · 15+ hooks" -ForegroundColor Gray
24
+ Write-Host " 53 agents · 185 skills · 79 commands · 18+ hooks" -ForegroundColor Gray
22
25
  Write-Host ""
23
26
 
24
27
  $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
@@ -35,6 +38,22 @@ switch ($Target) {
35
38
  $HooksDest = "$Dest\hooks"
36
39
  $RulesDest = "$Dest\rules"
37
40
  }
41
+ "windsurf-project" {
42
+ $Dest = "$(Get-Location)\.windsurf"
43
+ $AgentsDest = "$Dest\agents"
44
+ $SkillsDest = "$Dest\skills"
45
+ $CommandsDest = ""
46
+ $HooksDest = ""
47
+ $RulesDest = "$Dest\rules"
48
+ }
49
+ "windsurf-home" {
50
+ $Dest = "$HomeDir\.codeium\windsurf"
51
+ $AgentsDest = "$Dest\agents"
52
+ $SkillsDest = "$Dest\skills"
53
+ $CommandsDest = ""
54
+ $HooksDest = ""
55
+ $RulesDest = "$Dest\rules"
56
+ }
38
57
  "antigravity" {
39
58
  $Dest = "$(Get-Location)\.agent"
40
59
  $AgentsDest = "$Dest\skills"
@@ -59,9 +78,17 @@ switch ($Target) {
59
78
  $HooksDest = ""
60
79
  $RulesDest = "$Dest\rules"
61
80
  }
81
+ "opencode" {
82
+ $Dest = "$(Get-Location)\.opencode"
83
+ $AgentsDest = ""
84
+ $SkillsDest = ""
85
+ $CommandsDest = ""
86
+ $HooksDest = ""
87
+ $RulesDest = "$Dest\rules"
88
+ }
62
89
  default {
63
90
  Write-Host "Unknown target: $Target" -ForegroundColor Red
64
- Write-Host "Valid targets: claude-home, antigravity, cursor-project, codex-home"
91
+ Write-Host "Valid targets: claude-home, windsurf-project, windsurf-home, antigravity, cursor-project, codex-home, opencode"
65
92
  exit 1
66
93
  }
67
94
  }
@@ -104,8 +131,8 @@ Write-Host "Installing components..." -ForegroundColor Bold
104
131
 
105
132
  switch ($Target) {
106
133
  "claude-home" {
107
- Install-Dir "$ScriptDir\agents" $AgentsDest "Agents (50)"
108
- Install-Dir "$ScriptDir\skills" $SkillsDest "Skills (183)"
134
+ Install-Dir "$ScriptDir\agents" $AgentsDest "Agents (53)"
135
+ Install-Dir "$ScriptDir\skills" $SkillsDest "Skills (185)"
109
136
  Install-Dir "$ScriptDir\commands" $CommandsDest "Commands (79)"
110
137
 
111
138
  # Hooks
@@ -134,21 +161,38 @@ switch ($Target) {
134
161
  Copy-Item "$ScriptDir\SOUL.md" "$Dest\SOUL.md" -Force -ErrorAction SilentlyContinue
135
162
  Write-Host " [OK] CLAUDE.md + SOUL.md" -ForegroundColor Green
136
163
  }
164
+ { $_ -in "windsurf-project","windsurf-home" } {
165
+ Install-Dir "$ScriptDir\agents" $AgentsDest "Agents (53)"
166
+ Install-Dir "$ScriptDir\skills" $SkillsDest "Skills (185)"
167
+ Install-Flat "$ScriptDir\rules" $RulesDest "Rules"
168
+
169
+ # Generate .windsurfrules from all common rules
170
+ $WindsurfRulesFile = if ($Target -eq "windsurf-project") { "$(Get-Location)\.windsurfrules" } else { "$HomeDir\.windsurfrules" }
171
+ $CommonRulesDir = "$ScriptDir\rules\common"
172
+ if (Test-Path $CommonRulesDir) {
173
+ $Combined = Get-ChildItem -Path $CommonRulesDir -Filter "*.md" | Sort-Object Name | ForEach-Object { Get-Content $_.FullName }
174
+ $Combined | Out-File -FilePath $WindsurfRulesFile -Encoding UTF8
175
+ Write-Host " [OK] .windsurfrules generated -> $WindsurfRulesFile" -ForegroundColor Green
176
+ }
177
+ }
137
178
  "antigravity" {
138
- Install-Dir "$ScriptDir\agents" $AgentsDest "Agents -> skills (50)"
179
+ Install-Dir "$ScriptDir\agents" $AgentsDest "Agents -> skills (53)"
139
180
  Install-Dir "$ScriptDir\commands" $CommandsDest "Commands -> workflows (79)"
140
181
  Install-Flat "$ScriptDir\rules" $RulesDest "Rules"
141
182
  }
142
183
  "cursor-project" {
143
- Install-Flat "$ScriptDir\rules" $RulesDest "Rules"
144
- Install-Dir "$ScriptDir\skills" $SkillsDest "Skills (183)"
184
+ Install-Flat "$ScriptDir\rules" $RulesDest "Rules"
185
+ Install-Dir "$ScriptDir\skills" $SkillsDest "Skills (185)"
145
186
  }
146
187
  "codex-home" {
147
- Install-Dir "$ScriptDir\agents" $AgentsDest "Agents (50)"
148
- Install-Dir "$ScriptDir\skills" $SkillsDest "Skills (183)"
188
+ Install-Dir "$ScriptDir\agents" $AgentsDest "Agents (53)"
189
+ Install-Dir "$ScriptDir\skills" $SkillsDest "Skills (185)"
149
190
  Install-Dir "$ScriptDir\commands" $CommandsDest "Commands (79)"
150
191
  Install-Flat "$ScriptDir\rules" $RulesDest "Rules"
151
192
  }
193
+ "opencode" {
194
+ Install-Flat "$ScriptDir\rules" $RulesDest "Rules"
195
+ }
152
196
  }
153
197
 
154
198
  # ── Write install state ───────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodelyth-ecc",
3
- "version": "1.2.0",
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",