memhouse 0.0.1 → 0.3.1
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/LICENSE +22 -0
- package/README.md +72 -3
- package/bin/memhouse.js +559 -0
- package/cache.js +850 -0
- package/ch-ingest.js +329 -0
- package/ch-schema.js +152 -0
- package/clickhouse.js +87 -0
- package/editors/adapter-errors.js +52 -0
- package/editors/antigravity.js +1048 -0
- package/editors/base.js +321 -0
- package/editors/claude.js +386 -0
- package/editors/codebuff.js +338 -0
- package/editors/codex.js +537 -0
- package/editors/commandcode.js +166 -0
- package/editors/copilot-jetbrains.js +207 -0
- package/editors/copilot.js +260 -0
- package/editors/cursor-agent.js +202 -0
- package/editors/cursor.js +498 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +386 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +274 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +432 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +229 -0
- package/index.js +424 -2
- package/mcp-server.js +294 -0
- package/mem-house/DESIGN.md +119 -0
- package/mem-house/delivery/AGENT-INSTALL.md +87 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +83 -0
- package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
- package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
- package/mem-house/rls.sql +17 -0
- package/mem-house/schema.sql +145 -0
- package/mem-house/server/queries.js +878 -0
- package/mem-house/server/server.js +228 -0
- package/mem-house/shipper/ship.js +461 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +59 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-CCPh7a2Z.js +75 -0
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/index.html +16 -0
- package/relay-client.js +306 -0
- package/relay-server.js +577 -0
- package/server.js +860 -0
- package/share-image.js +367 -0
- package/ui/eslint.config.js +29 -0
- package/ui/index.html +15 -0
- package/ui/package-lock.json +4229 -0
- package/ui/package.json +38 -0
- package/ui/src/App.jsx +364 -0
- package/ui/src/components/ActivityHeatmap.jsx +206 -0
- package/ui/src/components/AiAuditCard.jsx +254 -0
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/components/AnimatedLogo.jsx +96 -0
- package/ui/src/components/ChatSidebar.jsx +226 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/components/EditorDot.jsx +11 -0
- package/ui/src/components/EditorIcon.jsx +117 -0
- package/ui/src/components/KpiCard.jsx +9 -0
- package/ui/src/components/LiveFeed.jsx +128 -0
- package/ui/src/components/LoginScreen.jsx +81 -0
- package/ui/src/components/MessageRenderer.jsx +148 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/components/SectionTitle.jsx +3 -0
- package/ui/src/components/ShareModal.jsx +305 -0
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/hooks/useLive.jsx +15 -0
- package/ui/src/index.css +83 -0
- package/ui/src/lib/api.js +343 -0
- package/ui/src/lib/constants.js +88 -0
- package/ui/src/lib/theme.jsx +23 -0
- package/ui/src/main.jsx +16 -0
- package/ui/src/pages/Artifacts.jsx +575 -0
- package/ui/src/pages/Compare.jsx +232 -0
- package/ui/src/pages/CostAnalysis.jsx +354 -0
- package/ui/src/pages/Dashboard.jsx +529 -0
- package/ui/src/pages/DeepAnalysis.jsx +495 -0
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +325 -0
- package/ui/src/pages/Projects.jsx +249 -0
- package/ui/src/pages/RelayDashboard.jsx +590 -0
- package/ui/src/pages/RelayUserDetail.jsx +466 -0
- package/ui/src/pages/Sessions.jsx +535 -0
- package/ui/src/pages/Settings.jsx +241 -0
- package/ui/src/pages/SqlViewer.jsx +370 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- package/ui/vite.config.js +18 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
export const BASE = '';
|
|
2
|
+
|
|
3
|
+
// ── Auth helpers ──
|
|
4
|
+
const AUTH_KEY = 'agentlytics_relay_token';
|
|
5
|
+
|
|
6
|
+
export function getAuthToken() {
|
|
7
|
+
return localStorage.getItem(AUTH_KEY);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setAuthToken(token) {
|
|
11
|
+
if (token) localStorage.setItem(AUTH_KEY, token);
|
|
12
|
+
else localStorage.removeItem(AUTH_KEY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let onAuthFailure = null;
|
|
16
|
+
export function setOnAuthFailure(fn) { onAuthFailure = fn; }
|
|
17
|
+
|
|
18
|
+
async function authFetch(url, opts = {}) {
|
|
19
|
+
const token = getAuthToken();
|
|
20
|
+
if (token) {
|
|
21
|
+
opts.headers = { ...opts.headers, Authorization: `Bearer ${token}` };
|
|
22
|
+
}
|
|
23
|
+
const res = await fetch(url, opts);
|
|
24
|
+
if (res.status === 401) {
|
|
25
|
+
setAuthToken(null);
|
|
26
|
+
if (onAuthFailure) onAuthFailure();
|
|
27
|
+
throw new Error('Unauthorized');
|
|
28
|
+
}
|
|
29
|
+
return res;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function login(password) {
|
|
33
|
+
const res = await fetch(`${BASE}/api/login`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ password }),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const data = await res.json().catch(() => ({}));
|
|
40
|
+
throw new Error(data.error || 'Login failed');
|
|
41
|
+
}
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
if (data.token) setAuthToken(data.token);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Append optional dateFrom/dateTo (ms timestamps) to URLSearchParams
|
|
48
|
+
function appendDateParams(q, params) {
|
|
49
|
+
if (params.dateFrom) q.set('dateFrom', params.dateFrom);
|
|
50
|
+
if (params.dateTo) q.set('dateTo', params.dateTo);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function fetchOverview(params = {}) {
|
|
54
|
+
const q = new URLSearchParams();
|
|
55
|
+
if (params.editor) q.set('editor', params.editor);
|
|
56
|
+
appendDateParams(q, params);
|
|
57
|
+
const qs = q.toString();
|
|
58
|
+
const res = await fetch(`${BASE}/api/overview${qs ? '?' + qs : ''}`);
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchChats(params = {}) {
|
|
63
|
+
const q = new URLSearchParams();
|
|
64
|
+
if (params.editor) q.set('editor', params.editor);
|
|
65
|
+
if (params.folder) q.set('folder', params.folder);
|
|
66
|
+
if (params.limit) q.set('limit', params.limit);
|
|
67
|
+
if (params.offset) q.set('offset', params.offset);
|
|
68
|
+
if (params.named === false) q.set('named', 'false');
|
|
69
|
+
appendDateParams(q, params);
|
|
70
|
+
const res = await fetch(`${BASE}/api/chats?${q}`);
|
|
71
|
+
return res.json();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function fetchChat(id) {
|
|
75
|
+
// Some adapters emit ids containing '/' (e.g. Kiro's dir/file fallback) —
|
|
76
|
+
// encode so the id stays a single path segment for the /api/chats/:id route.
|
|
77
|
+
const res = await fetch(`${BASE}/api/chats/${encodeURIComponent(id)}`);
|
|
78
|
+
return res.json();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function fetchProjects(params = {}) {
|
|
82
|
+
const q = new URLSearchParams();
|
|
83
|
+
appendDateParams(q, params);
|
|
84
|
+
const qs = q.toString();
|
|
85
|
+
const res = await fetch(`${BASE}/api/projects${qs ? '?' + qs : ''}`);
|
|
86
|
+
return res.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function fetchDailyActivity(params = {}) {
|
|
90
|
+
const q = new URLSearchParams();
|
|
91
|
+
if (params.editor) q.set('editor', params.editor);
|
|
92
|
+
appendDateParams(q, params);
|
|
93
|
+
const qs = q.toString();
|
|
94
|
+
const res = await fetch(`${BASE}/api/daily-activity${qs ? '?' + qs : ''}`);
|
|
95
|
+
return res.json();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function fetchDeepAnalytics(params = {}) {
|
|
99
|
+
const q = new URLSearchParams();
|
|
100
|
+
if (params.editor) q.set('editor', params.editor);
|
|
101
|
+
if (params.folder) q.set('folder', params.folder);
|
|
102
|
+
if (params.limit) q.set('limit', params.limit);
|
|
103
|
+
appendDateParams(q, params);
|
|
104
|
+
const res = await fetch(`${BASE}/api/deep-analytics?${q}`);
|
|
105
|
+
return res.json();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function refetchAgents(onProgress) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const es = new EventSource(`${BASE}/api/refetch`);
|
|
111
|
+
es.onmessage = (evt) => {
|
|
112
|
+
try {
|
|
113
|
+
const data = JSON.parse(evt.data);
|
|
114
|
+
if (data.done) { es.close(); resolve(data); }
|
|
115
|
+
else if (data.error) { es.close(); reject(new Error(data.error)); }
|
|
116
|
+
else if (onProgress) onProgress(data);
|
|
117
|
+
} catch { /* ignore parse errors */ }
|
|
118
|
+
};
|
|
119
|
+
es.onerror = () => { es.close(); reject(new Error('SSE error')); };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function fetchDashboardStats(params = {}) {
|
|
124
|
+
const q = new URLSearchParams();
|
|
125
|
+
if (params.editor) q.set('editor', params.editor);
|
|
126
|
+
appendDateParams(q, params);
|
|
127
|
+
const qs = q.toString();
|
|
128
|
+
const res = await fetch(`${BASE}/api/dashboard-stats${qs ? '?' + qs : ''}`);
|
|
129
|
+
return res.json();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function fetchCostAnalytics(params = {}) {
|
|
133
|
+
const q = new URLSearchParams();
|
|
134
|
+
if (params.editor) q.set('editor', params.editor);
|
|
135
|
+
appendDateParams(q, params);
|
|
136
|
+
const qs = q.toString();
|
|
137
|
+
const res = await fetch(`${BASE}/api/cost-analytics${qs ? '?' + qs : ''}`);
|
|
138
|
+
return res.json();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function fetchCosts(params = {}) {
|
|
142
|
+
const q = new URLSearchParams();
|
|
143
|
+
if (params.editor) q.set('editor', params.editor);
|
|
144
|
+
if (params.folder) q.set('folder', params.folder);
|
|
145
|
+
if (params.chatId) q.set('chatId', params.chatId);
|
|
146
|
+
appendDateParams(q, params);
|
|
147
|
+
const qs = q.toString();
|
|
148
|
+
const res = await fetch(`${BASE}/api/costs${qs ? '?' + qs : ''}`);
|
|
149
|
+
return res.json();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function fetchConfig() {
|
|
153
|
+
const res = await fetch(`${BASE}/api/config`);
|
|
154
|
+
return res.json();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function updateConfig(data) {
|
|
158
|
+
const res = await fetch(`${BASE}/api/config`, {
|
|
159
|
+
method: 'PUT',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify(data),
|
|
162
|
+
});
|
|
163
|
+
return res.json();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function fetchAllProjects() {
|
|
167
|
+
const res = await fetch(`${BASE}/api/all-projects`);
|
|
168
|
+
return res.json();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function executeQuery(sql) {
|
|
172
|
+
const res = await fetch(`${BASE}/api/query`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'Content-Type': 'application/json' },
|
|
175
|
+
body: JSON.stringify({ sql }),
|
|
176
|
+
});
|
|
177
|
+
return res.json();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function fetchSchema() {
|
|
181
|
+
const res = await fetch(`${BASE}/api/schema`);
|
|
182
|
+
return res.json();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function fetchShareImage(opts = {}) {
|
|
186
|
+
const q = new URLSearchParams();
|
|
187
|
+
if (opts.showEditors !== undefined) q.set('showEditors', opts.showEditors);
|
|
188
|
+
if (opts.showModels !== undefined) q.set('showModels', opts.showModels);
|
|
189
|
+
if (opts.showCosts !== undefined) q.set('showCosts', opts.showCosts);
|
|
190
|
+
if (opts.showTokens !== undefined) q.set('showTokens', opts.showTokens);
|
|
191
|
+
if (opts.showHours !== undefined) q.set('showHours', opts.showHours);
|
|
192
|
+
if (opts.username) q.set('username', opts.username);
|
|
193
|
+
if (opts.theme) q.set('theme', opts.theme);
|
|
194
|
+
if (opts.folder) q.set('folder', opts.folder);
|
|
195
|
+
const qs = q.toString();
|
|
196
|
+
const res = await fetch(`${BASE}/api/share-image${qs ? '?' + qs : ''}`);
|
|
197
|
+
return res.text();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function fetchToolCalls(name, opts = {}) {
|
|
201
|
+
const q = new URLSearchParams({ name });
|
|
202
|
+
if (opts.limit) q.set('limit', opts.limit);
|
|
203
|
+
if (opts.folder) q.set('folder', opts.folder);
|
|
204
|
+
if (opts.editor) q.set('editor', opts.editor);
|
|
205
|
+
appendDateParams(q, opts);
|
|
206
|
+
const res = await fetch(`${BASE}/api/tool-calls?${q}`);
|
|
207
|
+
return res.json();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function fetchCheckAi(folder) {
|
|
211
|
+
const q = new URLSearchParams({ folder });
|
|
212
|
+
const res = await fetch(`${BASE}/api/check-ai?${q}`);
|
|
213
|
+
return res.json();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function fetchUsage() {
|
|
217
|
+
const res = await fetch(`${BASE}/api/usage`);
|
|
218
|
+
return res.json();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Artifacts API ──
|
|
222
|
+
|
|
223
|
+
export async function fetchArtifacts() {
|
|
224
|
+
const res = await fetch(`${BASE}/api/artifacts`);
|
|
225
|
+
return res.json();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function fetchArtifactContent(filePath) {
|
|
229
|
+
const q = new URLSearchParams({ path: filePath });
|
|
230
|
+
const res = await fetch(`${BASE}/api/artifact-content?${q}`);
|
|
231
|
+
return res.json();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── MCPs API ──
|
|
235
|
+
|
|
236
|
+
export async function fetchMCPs() {
|
|
237
|
+
const res = await fetch(`${BASE}/api/mcps`);
|
|
238
|
+
return res.json();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── GSD API ──
|
|
242
|
+
|
|
243
|
+
export async function fetchGSDProjects() {
|
|
244
|
+
const res = await fetch(`${BASE}/api/gsd/projects`);
|
|
245
|
+
return res.json();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function fetchGSDPhases(folder) {
|
|
249
|
+
const q = new URLSearchParams({ folder });
|
|
250
|
+
const res = await fetch(`${BASE}/api/gsd/phases?${q}`);
|
|
251
|
+
return res.json();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function fetchGSDPlan(folder, phase) {
|
|
255
|
+
const q = new URLSearchParams({ folder, phase });
|
|
256
|
+
const res = await fetch(`${BASE}/api/gsd/plan?${q}`);
|
|
257
|
+
return res.json();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function fetchGSDOverview() {
|
|
261
|
+
const res = await fetch(`${BASE}/api/gsd/overview`);
|
|
262
|
+
return res.json();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function fetchGSDConfig(folder) {
|
|
266
|
+
const q = new URLSearchParams({ folder });
|
|
267
|
+
const res = await fetch(`${BASE}/api/gsd/config?${q}`);
|
|
268
|
+
return res.json();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function fetchGSDPhaseTokens(folder) {
|
|
272
|
+
const q = new URLSearchParams({ folder });
|
|
273
|
+
const res = await fetch(`${BASE}/api/gsd/phase-tokens?${q}`);
|
|
274
|
+
return res.json();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function fetchGSDFile(folder, type, phaseDir) {
|
|
278
|
+
const q = new URLSearchParams({ folder, type });
|
|
279
|
+
if (phaseDir) q.set('phase', phaseDir);
|
|
280
|
+
const res = await fetch(`${BASE}/api/gsd/file?${q}`);
|
|
281
|
+
return res.json();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Relay API ──
|
|
285
|
+
|
|
286
|
+
export async function fetchMode() {
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(`${BASE}/api/mode`);
|
|
289
|
+
if (!res.ok) return { mode: 'local' };
|
|
290
|
+
return res.json();
|
|
291
|
+
} catch {
|
|
292
|
+
return { mode: 'local' };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function fetchRelayTeamStats() {
|
|
297
|
+
const res = await authFetch(`${BASE}/relay/team-stats`);
|
|
298
|
+
return res.json();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function fetchRelayUserActivity(username, opts = {}) {
|
|
302
|
+
const q = new URLSearchParams();
|
|
303
|
+
if (opts.folder) q.set('folder', opts.folder);
|
|
304
|
+
if (opts.limit) q.set('limit', opts.limit);
|
|
305
|
+
const qs = q.toString();
|
|
306
|
+
const res = await authFetch(`${BASE}/relay/activity/${encodeURIComponent(username)}${qs ? '?' + qs : ''}`);
|
|
307
|
+
return res.json();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function fetchRelaySearch(query, opts = {}) {
|
|
311
|
+
const q = new URLSearchParams({ q: query });
|
|
312
|
+
if (opts.username) q.set('username', opts.username);
|
|
313
|
+
if (opts.folder) q.set('folder', opts.folder);
|
|
314
|
+
if (opts.limit) q.set('limit', opts.limit);
|
|
315
|
+
const res = await authFetch(`${BASE}/relay/search?${q}`);
|
|
316
|
+
return res.json();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function fetchRelayFeed(opts = {}) {
|
|
320
|
+
const q = new URLSearchParams();
|
|
321
|
+
if (opts.limit) q.set('limit', opts.limit);
|
|
322
|
+
if (opts.since) q.set('since', opts.since);
|
|
323
|
+
const qs = q.toString();
|
|
324
|
+
const res = await authFetch(`${BASE}/relay/feed${qs ? '?' + qs : ''}`);
|
|
325
|
+
return res.json();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function mergeRelayUsers(from, to) {
|
|
329
|
+
const res = await authFetch(`${BASE}/relay/merge-users`, {
|
|
330
|
+
method: 'POST',
|
|
331
|
+
headers: { 'Content-Type': 'application/json' },
|
|
332
|
+
body: JSON.stringify({ from, to }),
|
|
333
|
+
});
|
|
334
|
+
return res.json();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function fetchRelaySession(chatId, username) {
|
|
338
|
+
const q = new URLSearchParams();
|
|
339
|
+
if (username) q.set('username', username);
|
|
340
|
+
const qs = q.toString();
|
|
341
|
+
const res = await authFetch(`${BASE}/relay/session/${encodeURIComponent(chatId)}${qs ? '?' + qs : ''}`);
|
|
342
|
+
return res.json();
|
|
343
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const EDITOR_COLORS = {
|
|
2
|
+
'cursor': '#f59e0b',
|
|
3
|
+
'devin': '#2563eb',
|
|
4
|
+
'devin-next': '#7c3aed',
|
|
5
|
+
'antigravity': '#a78bfa',
|
|
6
|
+
'claude-code': '#f97316',
|
|
7
|
+
'claude': '#f97316',
|
|
8
|
+
'vscode': '#3b82f6',
|
|
9
|
+
'vscode-insiders': '#60a5fa',
|
|
10
|
+
'zed': '#10b981',
|
|
11
|
+
'opencode': '#656363',
|
|
12
|
+
'codex': '#3941FF',
|
|
13
|
+
'gemini-cli': '#4285f4',
|
|
14
|
+
'copilot-cli': '#8957e5',
|
|
15
|
+
'copilot-jetbrains': '#8957e5',
|
|
16
|
+
'cursor-agent': '#f59e0b',
|
|
17
|
+
'commandcode': '#e11d48',
|
|
18
|
+
'goose': '#333333',
|
|
19
|
+
'kiro': '#9046FF',
|
|
20
|
+
'codebuff': '#44ff00',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const EDITOR_LABELS = {
|
|
24
|
+
'cursor': 'Cursor',
|
|
25
|
+
'devin': 'Devin',
|
|
26
|
+
'devin-next': 'Devin Next',
|
|
27
|
+
'antigravity': 'Antigravity',
|
|
28
|
+
'claude-code': 'Claude Code',
|
|
29
|
+
'claude': 'Claude Code',
|
|
30
|
+
'vscode': 'VS Code',
|
|
31
|
+
'vscode-insiders': 'VS Code Insiders',
|
|
32
|
+
'zed': 'Zed',
|
|
33
|
+
'opencode': 'OpenCode',
|
|
34
|
+
'codex': 'Codex',
|
|
35
|
+
'gemini-cli': 'Gemini CLI',
|
|
36
|
+
'copilot-cli': 'GitHub Copilot',
|
|
37
|
+
'copilot-jetbrains': 'GitHub Copilot - JetBrains',
|
|
38
|
+
'cursor-agent': 'Cursor Agent',
|
|
39
|
+
'commandcode': 'Command Code',
|
|
40
|
+
'goose': 'Goose',
|
|
41
|
+
'kiro': 'Kiro',
|
|
42
|
+
'codebuff': 'Codebuff',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function editorColor(src) {
|
|
46
|
+
return EDITOR_COLORS[src] || '#6b7280';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function editorLabel(src) {
|
|
50
|
+
return EDITOR_LABELS[src] || src;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatNumber(n) {
|
|
54
|
+
if (n == null) return '0';
|
|
55
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
56
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
|
|
57
|
+
return n.toLocaleString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatCost(n) {
|
|
61
|
+
if (n == null || n === 0) return '$0';
|
|
62
|
+
if (n < 0.01) return '<$0.01';
|
|
63
|
+
if (n >= 1000) return '$' + (n / 1000).toFixed(1) + 'K';
|
|
64
|
+
if (n >= 100) return '$' + Math.round(n);
|
|
65
|
+
return '$' + n.toFixed(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatDate(ts) {
|
|
69
|
+
if (!ts) return '';
|
|
70
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD' } date range to API ms timestamps.
|
|
75
|
+
* Returns {} if range is null/incomplete.
|
|
76
|
+
*/
|
|
77
|
+
export function dateRangeToApiParams(range) {
|
|
78
|
+
if (!range?.from || !range?.to) return {};
|
|
79
|
+
return {
|
|
80
|
+
dateFrom: new Date(range.from).getTime(),
|
|
81
|
+
dateTo: new Date(range.to + 'T23:59:59').getTime(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatDateTime(ts) {
|
|
86
|
+
if (!ts) return '';
|
|
87
|
+
return new Date(ts).toLocaleString();
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
const ThemeContext = createContext()
|
|
4
|
+
|
|
5
|
+
export function ThemeProvider({ children }) {
|
|
6
|
+
const [dark, setDark] = useState(() => {
|
|
7
|
+
const saved = localStorage.getItem('agentlytics-theme')
|
|
8
|
+
return saved ? saved === 'dark' : true
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
document.documentElement.classList.toggle('light', !dark)
|
|
13
|
+
localStorage.setItem('agentlytics-theme', dark ? 'dark' : 'light')
|
|
14
|
+
}, [dark])
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<ThemeContext.Provider value={{ dark, toggle: () => setDark(d => !d) }}>
|
|
18
|
+
{children}
|
|
19
|
+
</ThemeContext.Provider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useTheme() { return useContext(ThemeContext) }
|
package/ui/src/main.jsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
4
|
+
import { ThemeProvider } from './lib/theme'
|
|
5
|
+
import App from './App'
|
|
6
|
+
import './index.css'
|
|
7
|
+
|
|
8
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
9
|
+
<React.StrictMode>
|
|
10
|
+
<ThemeProvider>
|
|
11
|
+
<BrowserRouter>
|
|
12
|
+
<App />
|
|
13
|
+
</BrowserRouter>
|
|
14
|
+
</ThemeProvider>
|
|
15
|
+
</React.StrictMode>,
|
|
16
|
+
)
|