@devness/useai 0.4.3 → 0.4.4
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/dist/index.js +672 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -111,7 +111,7 @@ var VERSION;
|
|
|
111
111
|
var init_version = __esm({
|
|
112
112
|
"../shared/dist/constants/version.js"() {
|
|
113
113
|
"use strict";
|
|
114
|
-
VERSION = "0.4.
|
|
114
|
+
VERSION = "0.4.4";
|
|
115
115
|
}
|
|
116
116
|
});
|
|
117
117
|
|
|
@@ -397,6 +397,10 @@ var init_format = __esm({
|
|
|
397
397
|
});
|
|
398
398
|
|
|
399
399
|
// ../shared/dist/utils/detect-client.js
|
|
400
|
+
function normalizeMcpClientName(mcpName) {
|
|
401
|
+
const lower = mcpName.toLowerCase().trim();
|
|
402
|
+
return MCP_CLIENT_NAME_MAP[lower] ?? lower;
|
|
403
|
+
}
|
|
400
404
|
function detectClient() {
|
|
401
405
|
const env = process.env;
|
|
402
406
|
for (const [envVar, clientName] of Object.entries(AI_CLIENT_ENV_VARS)) {
|
|
@@ -407,10 +411,35 @@ function detectClient() {
|
|
|
407
411
|
return env.MCP_CLIENT_NAME;
|
|
408
412
|
return "unknown";
|
|
409
413
|
}
|
|
414
|
+
var MCP_CLIENT_NAME_MAP;
|
|
410
415
|
var init_detect_client = __esm({
|
|
411
416
|
"../shared/dist/utils/detect-client.js"() {
|
|
412
417
|
"use strict";
|
|
413
418
|
init_clients();
|
|
419
|
+
MCP_CLIENT_NAME_MAP = {
|
|
420
|
+
"claude-code": "claude-code",
|
|
421
|
+
"claude code": "claude-code",
|
|
422
|
+
"claude-desktop": "claude-desktop",
|
|
423
|
+
"claude desktop": "claude-desktop",
|
|
424
|
+
"cursor": "cursor",
|
|
425
|
+
"windsurf": "windsurf",
|
|
426
|
+
"codeium": "windsurf",
|
|
427
|
+
"vscode": "vscode",
|
|
428
|
+
"visual studio code": "vscode",
|
|
429
|
+
"vscode-insiders": "vscode-insiders",
|
|
430
|
+
"codex": "codex",
|
|
431
|
+
"codex-cli": "codex",
|
|
432
|
+
"gemini-cli": "gemini-cli",
|
|
433
|
+
"gemini cli": "gemini-cli",
|
|
434
|
+
"zed": "zed",
|
|
435
|
+
"cline": "cline",
|
|
436
|
+
"roo-code": "roo-code",
|
|
437
|
+
"roo-cline": "roo-code",
|
|
438
|
+
"amazon-q": "amazon-q",
|
|
439
|
+
"opencode": "opencode",
|
|
440
|
+
"goose": "goose",
|
|
441
|
+
"junie": "junie"
|
|
442
|
+
};
|
|
414
443
|
}
|
|
415
444
|
});
|
|
416
445
|
|
|
@@ -1954,6 +1983,610 @@ var init_setup = __esm({
|
|
|
1954
1983
|
}
|
|
1955
1984
|
});
|
|
1956
1985
|
|
|
1986
|
+
// src/dashboard/html.ts
|
|
1987
|
+
function getDashboardHtml() {
|
|
1988
|
+
return `<!DOCTYPE html>
|
|
1989
|
+
<html lang="en">
|
|
1990
|
+
<head>
|
|
1991
|
+
<meta charset="UTF-8">
|
|
1992
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1993
|
+
<title>UseAI \u2014 Local Dashboard</title>
|
|
1994
|
+
<style>
|
|
1995
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1996
|
+
|
|
1997
|
+
:root {
|
|
1998
|
+
--bg: #0f0e0c;
|
|
1999
|
+
--surface: #1a1816;
|
|
2000
|
+
--border: #2a2520;
|
|
2001
|
+
--amber: #d4a04a;
|
|
2002
|
+
--amber-dim: #b8892e;
|
|
2003
|
+
--text: #e8e0d4;
|
|
2004
|
+
--muted: #9a9082;
|
|
2005
|
+
--green: #6abf69;
|
|
2006
|
+
--red: #d45a5a;
|
|
2007
|
+
--blue: #5a9fd4;
|
|
2008
|
+
--purple: #a87fd4;
|
|
2009
|
+
--radius: 8px;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
body {
|
|
2013
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
2014
|
+
background: var(--bg);
|
|
2015
|
+
color: var(--text);
|
|
2016
|
+
line-height: 1.5;
|
|
2017
|
+
min-height: 100vh;
|
|
2018
|
+
padding: 24px 16px;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
.container { max-width: 960px; margin: 0 auto; }
|
|
2022
|
+
|
|
2023
|
+
/* Header */
|
|
2024
|
+
.header { margin-bottom: 32px; }
|
|
2025
|
+
.header h1 {
|
|
2026
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
2027
|
+
font-size: 1.5rem;
|
|
2028
|
+
color: var(--amber);
|
|
2029
|
+
font-weight: 700;
|
|
2030
|
+
letter-spacing: -0.02em;
|
|
2031
|
+
}
|
|
2032
|
+
.header .subtitle {
|
|
2033
|
+
color: var(--muted);
|
|
2034
|
+
font-size: 0.85rem;
|
|
2035
|
+
margin-top: 2px;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/* Stats row */
|
|
2039
|
+
.stats-row {
|
|
2040
|
+
display: grid;
|
|
2041
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
2042
|
+
gap: 12px;
|
|
2043
|
+
margin-bottom: 28px;
|
|
2044
|
+
}
|
|
2045
|
+
.stat-card {
|
|
2046
|
+
background: var(--surface);
|
|
2047
|
+
border: 1px solid var(--border);
|
|
2048
|
+
border-radius: var(--radius);
|
|
2049
|
+
padding: 16px;
|
|
2050
|
+
}
|
|
2051
|
+
.stat-card .label {
|
|
2052
|
+
font-size: 0.75rem;
|
|
2053
|
+
color: var(--muted);
|
|
2054
|
+
text-transform: uppercase;
|
|
2055
|
+
letter-spacing: 0.05em;
|
|
2056
|
+
margin-bottom: 4px;
|
|
2057
|
+
}
|
|
2058
|
+
.stat-card .value {
|
|
2059
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
2060
|
+
font-size: 1.6rem;
|
|
2061
|
+
font-weight: 700;
|
|
2062
|
+
color: var(--amber);
|
|
2063
|
+
}
|
|
2064
|
+
.stat-card .unit {
|
|
2065
|
+
font-size: 0.8rem;
|
|
2066
|
+
color: var(--muted);
|
|
2067
|
+
font-weight: 400;
|
|
2068
|
+
margin-left: 4px;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/* Section */
|
|
2072
|
+
.section {
|
|
2073
|
+
background: var(--surface);
|
|
2074
|
+
border: 1px solid var(--border);
|
|
2075
|
+
border-radius: var(--radius);
|
|
2076
|
+
padding: 20px;
|
|
2077
|
+
margin-bottom: 16px;
|
|
2078
|
+
}
|
|
2079
|
+
.section h2 {
|
|
2080
|
+
font-size: 0.85rem;
|
|
2081
|
+
color: var(--muted);
|
|
2082
|
+
text-transform: uppercase;
|
|
2083
|
+
letter-spacing: 0.05em;
|
|
2084
|
+
margin-bottom: 16px;
|
|
2085
|
+
font-weight: 600;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/* Bar chart */
|
|
2089
|
+
.bar-row { margin-bottom: 10px; }
|
|
2090
|
+
.bar-row:last-child { margin-bottom: 0; }
|
|
2091
|
+
.bar-label {
|
|
2092
|
+
display: flex;
|
|
2093
|
+
justify-content: space-between;
|
|
2094
|
+
font-size: 0.82rem;
|
|
2095
|
+
margin-bottom: 4px;
|
|
2096
|
+
}
|
|
2097
|
+
.bar-label .name {
|
|
2098
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
2099
|
+
color: var(--text);
|
|
2100
|
+
}
|
|
2101
|
+
.bar-label .hours {
|
|
2102
|
+
color: var(--muted);
|
|
2103
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
2104
|
+
}
|
|
2105
|
+
.bar-track {
|
|
2106
|
+
height: 8px;
|
|
2107
|
+
background: var(--bg);
|
|
2108
|
+
border-radius: 4px;
|
|
2109
|
+
overflow: hidden;
|
|
2110
|
+
}
|
|
2111
|
+
.bar-fill {
|
|
2112
|
+
height: 100%;
|
|
2113
|
+
background: var(--amber);
|
|
2114
|
+
border-radius: 4px;
|
|
2115
|
+
transition: width 0.6s ease;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/* Milestones */
|
|
2119
|
+
.milestone-item {
|
|
2120
|
+
display: flex;
|
|
2121
|
+
align-items: flex-start;
|
|
2122
|
+
gap: 10px;
|
|
2123
|
+
padding: 10px 0;
|
|
2124
|
+
border-bottom: 1px solid var(--border);
|
|
2125
|
+
}
|
|
2126
|
+
.milestone-item:last-child { border-bottom: none; }
|
|
2127
|
+
.milestone-title {
|
|
2128
|
+
flex: 1;
|
|
2129
|
+
font-size: 0.88rem;
|
|
2130
|
+
color: var(--text);
|
|
2131
|
+
}
|
|
2132
|
+
.milestone-meta {
|
|
2133
|
+
display: flex;
|
|
2134
|
+
gap: 8px;
|
|
2135
|
+
align-items: center;
|
|
2136
|
+
flex-shrink: 0;
|
|
2137
|
+
}
|
|
2138
|
+
.badge {
|
|
2139
|
+
font-size: 0.7rem;
|
|
2140
|
+
padding: 2px 8px;
|
|
2141
|
+
border-radius: 4px;
|
|
2142
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
2143
|
+
text-transform: uppercase;
|
|
2144
|
+
letter-spacing: 0.03em;
|
|
2145
|
+
font-weight: 600;
|
|
2146
|
+
}
|
|
2147
|
+
.badge-feature { background: #2a3520; color: var(--green); }
|
|
2148
|
+
.badge-bugfix { background: #3a2020; color: var(--red); }
|
|
2149
|
+
.badge-refactor { background: #202a3a; color: var(--blue); }
|
|
2150
|
+
.badge-test { background: #2a2035; color: var(--purple); }
|
|
2151
|
+
.badge-docs { background: #2a2820; color: #c4a854; }
|
|
2152
|
+
.badge-setup { background: #252520; color: #b0a070; }
|
|
2153
|
+
.badge-deployment { background: #202525; color: #70b0a0; }
|
|
2154
|
+
.badge-other { background: #252525; color: var(--muted); }
|
|
2155
|
+
.complexity {
|
|
2156
|
+
font-size: 0.7rem;
|
|
2157
|
+
color: var(--muted);
|
|
2158
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
2159
|
+
}
|
|
2160
|
+
.milestone-date {
|
|
2161
|
+
font-size: 0.72rem;
|
|
2162
|
+
color: var(--muted);
|
|
2163
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
2164
|
+
white-space: nowrap;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/* Sync section */
|
|
2168
|
+
.sync-section { text-align: center; padding: 24px; }
|
|
2169
|
+
.sync-btn {
|
|
2170
|
+
background: var(--amber);
|
|
2171
|
+
color: var(--bg);
|
|
2172
|
+
border: none;
|
|
2173
|
+
padding: 10px 28px;
|
|
2174
|
+
border-radius: var(--radius);
|
|
2175
|
+
font-weight: 600;
|
|
2176
|
+
font-size: 0.9rem;
|
|
2177
|
+
cursor: pointer;
|
|
2178
|
+
font-family: system-ui, sans-serif;
|
|
2179
|
+
transition: opacity 0.15s;
|
|
2180
|
+
}
|
|
2181
|
+
.sync-btn:hover { opacity: 0.85; }
|
|
2182
|
+
.sync-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2183
|
+
.sync-status {
|
|
2184
|
+
margin-top: 10px;
|
|
2185
|
+
font-size: 0.82rem;
|
|
2186
|
+
color: var(--muted);
|
|
2187
|
+
}
|
|
2188
|
+
.sync-result {
|
|
2189
|
+
margin-top: 8px;
|
|
2190
|
+
font-size: 0.82rem;
|
|
2191
|
+
}
|
|
2192
|
+
.sync-result.success { color: var(--green); }
|
|
2193
|
+
.sync-result.error { color: var(--red); }
|
|
2194
|
+
|
|
2195
|
+
.setup-msg {
|
|
2196
|
+
text-align: center;
|
|
2197
|
+
padding: 20px;
|
|
2198
|
+
color: var(--muted);
|
|
2199
|
+
font-size: 0.88rem;
|
|
2200
|
+
}
|
|
2201
|
+
.setup-msg a {
|
|
2202
|
+
color: var(--amber);
|
|
2203
|
+
text-decoration: underline;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.empty {
|
|
2207
|
+
text-align: center;
|
|
2208
|
+
color: var(--muted);
|
|
2209
|
+
padding: 20px;
|
|
2210
|
+
font-size: 0.85rem;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
@media (max-width: 500px) {
|
|
2214
|
+
body { padding: 16px 10px; }
|
|
2215
|
+
.stats-row { grid-template-columns: repeat(2, 1fr); }
|
|
2216
|
+
.milestone-meta { flex-direction: column; gap: 4px; align-items: flex-end; }
|
|
2217
|
+
}
|
|
2218
|
+
</style>
|
|
2219
|
+
</head>
|
|
2220
|
+
<body>
|
|
2221
|
+
<div class="container">
|
|
2222
|
+
<div class="header">
|
|
2223
|
+
<h1>UseAI</h1>
|
|
2224
|
+
<div class="subtitle">Local Dashboard</div>
|
|
2225
|
+
</div>
|
|
2226
|
+
|
|
2227
|
+
<div class="stats-row" id="stats-row">
|
|
2228
|
+
<div class="stat-card"><div class="label">Total Hours</div><div class="value" id="stat-hours">-</div></div>
|
|
2229
|
+
<div class="stat-card"><div class="label">Sessions</div><div class="value" id="stat-sessions">-</div></div>
|
|
2230
|
+
<div class="stat-card"><div class="label">Current Streak</div><div class="value" id="stat-streak">-<span class="unit">days</span></div></div>
|
|
2231
|
+
<div class="stat-card"><div class="label">Files Touched</div><div class="value" id="stat-files">-</div></div>
|
|
2232
|
+
</div>
|
|
2233
|
+
|
|
2234
|
+
<div class="section" id="clients-section" style="display:none">
|
|
2235
|
+
<h2>Tools / Clients</h2>
|
|
2236
|
+
<div id="clients-bars"></div>
|
|
2237
|
+
</div>
|
|
2238
|
+
|
|
2239
|
+
<div class="section" id="languages-section" style="display:none">
|
|
2240
|
+
<h2>Languages</h2>
|
|
2241
|
+
<div id="languages-bars"></div>
|
|
2242
|
+
</div>
|
|
2243
|
+
|
|
2244
|
+
<div class="section" id="tasks-section" style="display:none">
|
|
2245
|
+
<h2>Task Types</h2>
|
|
2246
|
+
<div id="tasks-bars"></div>
|
|
2247
|
+
</div>
|
|
2248
|
+
|
|
2249
|
+
<div class="section" id="milestones-section" style="display:none">
|
|
2250
|
+
<h2>Recent Milestones</h2>
|
|
2251
|
+
<div id="milestones-list"></div>
|
|
2252
|
+
</div>
|
|
2253
|
+
|
|
2254
|
+
<div class="section" id="sync-section" style="display:none"></div>
|
|
2255
|
+
</div>
|
|
2256
|
+
|
|
2257
|
+
<script>
|
|
2258
|
+
(function() {
|
|
2259
|
+
const API = '';
|
|
2260
|
+
|
|
2261
|
+
function animateCounter(el, target, decimals) {
|
|
2262
|
+
if (decimals === undefined) decimals = 0;
|
|
2263
|
+
var start = 0;
|
|
2264
|
+
var duration = 600;
|
|
2265
|
+
var startTime = null;
|
|
2266
|
+
function step(ts) {
|
|
2267
|
+
if (!startTime) startTime = ts;
|
|
2268
|
+
var progress = Math.min((ts - startTime) / duration, 1);
|
|
2269
|
+
var eased = 1 - Math.pow(1 - progress, 3);
|
|
2270
|
+
var current = start + (target - start) * eased;
|
|
2271
|
+
el.textContent = decimals > 0 ? current.toFixed(decimals) : Math.round(current);
|
|
2272
|
+
if (progress < 1) requestAnimationFrame(step);
|
|
2273
|
+
}
|
|
2274
|
+
requestAnimationFrame(step);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
function formatHours(seconds) {
|
|
2278
|
+
var h = seconds / 3600;
|
|
2279
|
+
return h < 0.1 ? h.toFixed(2) : h.toFixed(1);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function renderBars(containerId, data) {
|
|
2283
|
+
var container = document.getElementById(containerId);
|
|
2284
|
+
if (!container) return;
|
|
2285
|
+
var entries = Object.entries(data).sort(function(a, b) { return b[1] - a[1]; });
|
|
2286
|
+
if (entries.length === 0) { container.innerHTML = '<div class="empty">No data yet</div>'; return; }
|
|
2287
|
+
var max = entries[0][1];
|
|
2288
|
+
container.innerHTML = entries.map(function(e) {
|
|
2289
|
+
var pct = max > 0 ? (e[1] / max * 100) : 0;
|
|
2290
|
+
return '<div class="bar-row">' +
|
|
2291
|
+
'<div class="bar-label"><span class="name">' + escapeHtml(e[0]) + '</span><span class="hours">' + formatHours(e[1]) + 'h</span></div>' +
|
|
2292
|
+
'<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%"></div></div>' +
|
|
2293
|
+
'</div>';
|
|
2294
|
+
}).join('');
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
function badgeClass(cat) {
|
|
2298
|
+
var map = { feature: 'feature', bugfix: 'bugfix', refactor: 'refactor', test: 'test', docs: 'docs', setup: 'setup', deployment: 'deployment' };
|
|
2299
|
+
return 'badge badge-' + (map[cat] || 'other');
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function escapeHtml(s) {
|
|
2303
|
+
var d = document.createElement('div');
|
|
2304
|
+
d.textContent = s;
|
|
2305
|
+
return d.innerHTML;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function renderMilestones(milestones) {
|
|
2309
|
+
var section = document.getElementById('milestones-section');
|
|
2310
|
+
var list = document.getElementById('milestones-list');
|
|
2311
|
+
if (!milestones || milestones.length === 0) {
|
|
2312
|
+
section.style.display = 'block';
|
|
2313
|
+
list.innerHTML = '<div class="empty">No milestones recorded yet</div>';
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
section.style.display = 'block';
|
|
2317
|
+
var recent = milestones.slice(-20).reverse();
|
|
2318
|
+
list.innerHTML = recent.map(function(m) {
|
|
2319
|
+
var date = m.created_at ? m.created_at.slice(0, 10) : '';
|
|
2320
|
+
return '<div class="milestone-item">' +
|
|
2321
|
+
'<div class="milestone-title">' + escapeHtml(m.title) + '</div>' +
|
|
2322
|
+
'<div class="milestone-meta">' +
|
|
2323
|
+
'<span class="' + badgeClass(m.category) + '">' + escapeHtml(m.category) + '</span>' +
|
|
2324
|
+
(m.complexity ? '<span class="complexity">' + escapeHtml(m.complexity) + '</span>' : '') +
|
|
2325
|
+
'<span class="milestone-date">' + escapeHtml(date) + '</span>' +
|
|
2326
|
+
'</div>' +
|
|
2327
|
+
'</div>';
|
|
2328
|
+
}).join('');
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function renderSync(config) {
|
|
2332
|
+
var section = document.getElementById('sync-section');
|
|
2333
|
+
if (!config) { section.style.display = 'none'; return; }
|
|
2334
|
+
section.style.display = 'block';
|
|
2335
|
+
|
|
2336
|
+
if (config.authenticated) {
|
|
2337
|
+
var lastSync = config.last_sync_at ? 'Last sync: ' + config.last_sync_at : 'Never synced';
|
|
2338
|
+
section.innerHTML = '<div class="sync-section">' +
|
|
2339
|
+
'<h2 style="font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:12px;font-weight:600;">Sync</h2>' +
|
|
2340
|
+
'<button class="sync-btn" id="sync-btn">Sync to useai.dev</button>' +
|
|
2341
|
+
'<div class="sync-status" id="sync-status">' + escapeHtml(lastSync) + '</div>' +
|
|
2342
|
+
'<div class="sync-result" id="sync-result"></div>' +
|
|
2343
|
+
'</div>';
|
|
2344
|
+
document.getElementById('sync-btn').addEventListener('click', doSync);
|
|
2345
|
+
} else {
|
|
2346
|
+
section.innerHTML = '<div class="setup-msg">Login at <a href="https://useai.dev" target="_blank">useai.dev</a> to sync your data across devices</div>';
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function doSync() {
|
|
2351
|
+
var btn = document.getElementById('sync-btn');
|
|
2352
|
+
var result = document.getElementById('sync-result');
|
|
2353
|
+
var status = document.getElementById('sync-status');
|
|
2354
|
+
btn.disabled = true;
|
|
2355
|
+
btn.textContent = 'Syncing...';
|
|
2356
|
+
result.textContent = '';
|
|
2357
|
+
result.className = 'sync-result';
|
|
2358
|
+
|
|
2359
|
+
fetch(API + '/api/local/sync', { method: 'POST' })
|
|
2360
|
+
.then(function(r) { return r.json(); })
|
|
2361
|
+
.then(function(data) {
|
|
2362
|
+
btn.disabled = false;
|
|
2363
|
+
btn.textContent = 'Sync to useai.dev';
|
|
2364
|
+
if (data.success) {
|
|
2365
|
+
result.textContent = 'Synced successfully';
|
|
2366
|
+
result.className = 'sync-result success';
|
|
2367
|
+
if (data.last_sync_at) status.textContent = 'Last sync: ' + data.last_sync_at;
|
|
2368
|
+
} else {
|
|
2369
|
+
result.textContent = 'Sync failed: ' + (data.error || 'Unknown error');
|
|
2370
|
+
result.className = 'sync-result error';
|
|
2371
|
+
}
|
|
2372
|
+
})
|
|
2373
|
+
.catch(function(err) {
|
|
2374
|
+
btn.disabled = false;
|
|
2375
|
+
btn.textContent = 'Sync to useai.dev';
|
|
2376
|
+
result.textContent = 'Sync failed: ' + err.message;
|
|
2377
|
+
result.className = 'sync-result error';
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function loadAll() {
|
|
2382
|
+
fetch(API + '/api/local/stats')
|
|
2383
|
+
.then(function(r) { return r.json(); })
|
|
2384
|
+
.then(function(stats) {
|
|
2385
|
+
animateCounter(document.getElementById('stat-hours'), stats.totalHours, 1);
|
|
2386
|
+
animateCounter(document.getElementById('stat-sessions'), stats.totalSessions);
|
|
2387
|
+
var streakEl = document.getElementById('stat-streak');
|
|
2388
|
+
streakEl.innerHTML = '';
|
|
2389
|
+
var numSpan = document.createElement('span');
|
|
2390
|
+
streakEl.appendChild(numSpan);
|
|
2391
|
+
animateCounter(numSpan, stats.currentStreak);
|
|
2392
|
+
var unitSpan = document.createElement('span');
|
|
2393
|
+
unitSpan.className = 'unit';
|
|
2394
|
+
unitSpan.textContent = ' days';
|
|
2395
|
+
streakEl.appendChild(unitSpan);
|
|
2396
|
+
animateCounter(document.getElementById('stat-files'), stats.filesTouched || 0);
|
|
2397
|
+
|
|
2398
|
+
if (Object.keys(stats.byClient || {}).length > 0) {
|
|
2399
|
+
document.getElementById('clients-section').style.display = 'block';
|
|
2400
|
+
renderBars('clients-bars', stats.byClient);
|
|
2401
|
+
}
|
|
2402
|
+
if (Object.keys(stats.byLanguage || {}).length > 0) {
|
|
2403
|
+
document.getElementById('languages-section').style.display = 'block';
|
|
2404
|
+
renderBars('languages-bars', stats.byLanguage);
|
|
2405
|
+
}
|
|
2406
|
+
if (Object.keys(stats.byTaskType || {}).length > 0) {
|
|
2407
|
+
document.getElementById('tasks-section').style.display = 'block';
|
|
2408
|
+
renderBars('tasks-bars', stats.byTaskType);
|
|
2409
|
+
}
|
|
2410
|
+
})
|
|
2411
|
+
.catch(function() {});
|
|
2412
|
+
|
|
2413
|
+
fetch(API + '/api/local/milestones')
|
|
2414
|
+
.then(function(r) { return r.json(); })
|
|
2415
|
+
.then(function(data) { renderMilestones(data); })
|
|
2416
|
+
.catch(function() {});
|
|
2417
|
+
|
|
2418
|
+
fetch(API + '/api/local/config')
|
|
2419
|
+
.then(function(r) { return r.json(); })
|
|
2420
|
+
.then(function(data) { renderSync(data); })
|
|
2421
|
+
.catch(function() {});
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
loadAll();
|
|
2425
|
+
setInterval(loadAll, 30000);
|
|
2426
|
+
})();
|
|
2427
|
+
</script>
|
|
2428
|
+
</body>
|
|
2429
|
+
</html>`;
|
|
2430
|
+
}
|
|
2431
|
+
var init_html = __esm({
|
|
2432
|
+
"src/dashboard/html.ts"() {
|
|
2433
|
+
"use strict";
|
|
2434
|
+
}
|
|
2435
|
+
});
|
|
2436
|
+
|
|
2437
|
+
// src/dashboard/local-api.ts
|
|
2438
|
+
function json(res, status, data) {
|
|
2439
|
+
const body = JSON.stringify(data);
|
|
2440
|
+
res.writeHead(status, {
|
|
2441
|
+
"Content-Type": "application/json",
|
|
2442
|
+
"Access-Control-Allow-Origin": "*",
|
|
2443
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
2444
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
2445
|
+
});
|
|
2446
|
+
res.end(body);
|
|
2447
|
+
}
|
|
2448
|
+
function readBody(req) {
|
|
2449
|
+
return new Promise((resolve, reject) => {
|
|
2450
|
+
const chunks = [];
|
|
2451
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2452
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
2453
|
+
req.on("error", reject);
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
function calculateStreak(sessions2) {
|
|
2457
|
+
if (sessions2.length === 0) return 0;
|
|
2458
|
+
const days = /* @__PURE__ */ new Set();
|
|
2459
|
+
for (const s of sessions2) {
|
|
2460
|
+
days.add(s.started_at.slice(0, 10));
|
|
2461
|
+
}
|
|
2462
|
+
const sorted = [...days].sort().reverse();
|
|
2463
|
+
if (sorted.length === 0) return 0;
|
|
2464
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2465
|
+
const yesterday = new Date(Date.now() - 864e5).toISOString().slice(0, 10);
|
|
2466
|
+
if (sorted[0] !== today && sorted[0] !== yesterday) return 0;
|
|
2467
|
+
let streak = 1;
|
|
2468
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
2469
|
+
const prev = new Date(sorted[i - 1]);
|
|
2470
|
+
const curr = new Date(sorted[i]);
|
|
2471
|
+
const diffDays = (prev.getTime() - curr.getTime()) / 864e5;
|
|
2472
|
+
if (diffDays === 1) {
|
|
2473
|
+
streak++;
|
|
2474
|
+
} else {
|
|
2475
|
+
break;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return streak;
|
|
2479
|
+
}
|
|
2480
|
+
function handleLocalStats(_req, res) {
|
|
2481
|
+
try {
|
|
2482
|
+
const sessions2 = readJson(SESSIONS_FILE, []);
|
|
2483
|
+
let totalSeconds = 0;
|
|
2484
|
+
let filesTouched = 0;
|
|
2485
|
+
const byClient = {};
|
|
2486
|
+
const byLanguage = {};
|
|
2487
|
+
const byTaskType = {};
|
|
2488
|
+
for (const s of sessions2) {
|
|
2489
|
+
totalSeconds += s.duration_seconds;
|
|
2490
|
+
filesTouched += s.files_touched;
|
|
2491
|
+
byClient[s.client] = (byClient[s.client] ?? 0) + s.duration_seconds;
|
|
2492
|
+
for (const lang of s.languages) {
|
|
2493
|
+
byLanguage[lang] = (byLanguage[lang] ?? 0) + s.duration_seconds;
|
|
2494
|
+
}
|
|
2495
|
+
byTaskType[s.task_type] = (byTaskType[s.task_type] ?? 0) + s.duration_seconds;
|
|
2496
|
+
}
|
|
2497
|
+
json(res, 200, {
|
|
2498
|
+
totalHours: totalSeconds / 3600,
|
|
2499
|
+
totalSessions: sessions2.length,
|
|
2500
|
+
currentStreak: calculateStreak(sessions2),
|
|
2501
|
+
filesTouched,
|
|
2502
|
+
byClient,
|
|
2503
|
+
byLanguage,
|
|
2504
|
+
byTaskType
|
|
2505
|
+
});
|
|
2506
|
+
} catch (err2) {
|
|
2507
|
+
json(res, 500, { error: err2.message });
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
function handleLocalMilestones(_req, res) {
|
|
2511
|
+
try {
|
|
2512
|
+
const milestones = readJson(MILESTONES_FILE, []);
|
|
2513
|
+
json(res, 200, milestones);
|
|
2514
|
+
} catch (err2) {
|
|
2515
|
+
json(res, 500, { error: err2.message });
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
function handleLocalConfig(_req, res) {
|
|
2519
|
+
try {
|
|
2520
|
+
const config = readJson(CONFIG_FILE, {
|
|
2521
|
+
milestone_tracking: true,
|
|
2522
|
+
auto_sync: false,
|
|
2523
|
+
sync_interval_hours: 24
|
|
2524
|
+
});
|
|
2525
|
+
json(res, 200, {
|
|
2526
|
+
authenticated: !!config.auth?.token,
|
|
2527
|
+
email: config.auth?.user?.email ?? null,
|
|
2528
|
+
username: config.auth?.user?.username ?? null,
|
|
2529
|
+
last_sync_at: config.last_sync_at ?? null,
|
|
2530
|
+
auto_sync: config.auto_sync
|
|
2531
|
+
});
|
|
2532
|
+
} catch (err2) {
|
|
2533
|
+
json(res, 500, { error: err2.message });
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
async function handleLocalSync(req, res) {
|
|
2537
|
+
try {
|
|
2538
|
+
await readBody(req);
|
|
2539
|
+
const config = readJson(CONFIG_FILE, {
|
|
2540
|
+
milestone_tracking: true,
|
|
2541
|
+
auto_sync: false,
|
|
2542
|
+
sync_interval_hours: 24
|
|
2543
|
+
});
|
|
2544
|
+
if (!config.auth?.token) {
|
|
2545
|
+
json(res, 401, { success: false, error: "Not authenticated. Login at useai.dev first." });
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
const token = config.auth.token;
|
|
2549
|
+
const headers = {
|
|
2550
|
+
"Content-Type": "application/json",
|
|
2551
|
+
"Authorization": `Bearer ${token}`
|
|
2552
|
+
};
|
|
2553
|
+
const sessions2 = readJson(SESSIONS_FILE, []);
|
|
2554
|
+
const sessionsRes = await fetch("https://api.useai.dev/api/sync", {
|
|
2555
|
+
method: "POST",
|
|
2556
|
+
headers,
|
|
2557
|
+
body: JSON.stringify({ sessions: sessions2 })
|
|
2558
|
+
});
|
|
2559
|
+
if (!sessionsRes.ok) {
|
|
2560
|
+
const errBody = await sessionsRes.text();
|
|
2561
|
+
json(res, 502, { success: false, error: `Sessions sync failed: ${sessionsRes.status} ${errBody}` });
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
const milestones = readJson(MILESTONES_FILE, []);
|
|
2565
|
+
const milestonesRes = await fetch("https://api.useai.dev/api/publish", {
|
|
2566
|
+
method: "POST",
|
|
2567
|
+
headers,
|
|
2568
|
+
body: JSON.stringify({ milestones })
|
|
2569
|
+
});
|
|
2570
|
+
if (!milestonesRes.ok) {
|
|
2571
|
+
const errBody = await milestonesRes.text();
|
|
2572
|
+
json(res, 502, { success: false, error: `Milestones publish failed: ${milestonesRes.status} ${errBody}` });
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2576
|
+
config.last_sync_at = now;
|
|
2577
|
+
writeJson(CONFIG_FILE, config);
|
|
2578
|
+
json(res, 200, { success: true, last_sync_at: now });
|
|
2579
|
+
} catch (err2) {
|
|
2580
|
+
json(res, 500, { success: false, error: err2.message });
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
var init_local_api = __esm({
|
|
2584
|
+
"src/dashboard/local-api.ts"() {
|
|
2585
|
+
"use strict";
|
|
2586
|
+
init_dist();
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
|
|
1957
2590
|
// src/daemon.ts
|
|
1958
2591
|
var daemon_exports = {};
|
|
1959
2592
|
__export(daemon_exports, {
|
|
@@ -2087,6 +2720,38 @@ async function startDaemon(port) {
|
|
|
2087
2720
|
handleHealth(res);
|
|
2088
2721
|
return;
|
|
2089
2722
|
}
|
|
2723
|
+
if ((url.pathname === "/" || url.pathname === "/dashboard") && req.method === "GET") {
|
|
2724
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2725
|
+
res.end(getDashboardHtml());
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (url.pathname.startsWith("/api/local/") && req.method === "GET") {
|
|
2729
|
+
if (url.pathname === "/api/local/stats") {
|
|
2730
|
+
handleLocalStats(req, res);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (url.pathname === "/api/local/milestones") {
|
|
2734
|
+
handleLocalMilestones(req, res);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (url.pathname === "/api/local/config") {
|
|
2738
|
+
handleLocalConfig(req, res);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
if (url.pathname === "/api/local/sync" && req.method === "POST") {
|
|
2743
|
+
await handleLocalSync(req, res);
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
if (url.pathname.startsWith("/api/local/") && req.method === "OPTIONS") {
|
|
2747
|
+
res.writeHead(204, {
|
|
2748
|
+
"Access-Control-Allow-Origin": "*",
|
|
2749
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
2750
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
2751
|
+
});
|
|
2752
|
+
res.end();
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2090
2755
|
if (url.pathname !== "/mcp") {
|
|
2091
2756
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2092
2757
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
@@ -2120,6 +2785,10 @@ async function startDaemon(port) {
|
|
|
2120
2785
|
const transport = new StreamableHTTPServerTransport({
|
|
2121
2786
|
sessionIdGenerator: () => randomUUID4(),
|
|
2122
2787
|
onsessioninitialized: (newSid) => {
|
|
2788
|
+
const clientInfo = mcpServer.server.getClientVersion();
|
|
2789
|
+
if (clientInfo?.name) {
|
|
2790
|
+
sessionState.setClient(normalizeMcpClientName(clientInfo.name));
|
|
2791
|
+
}
|
|
2123
2792
|
const idleTimer = setTimeout(async () => {
|
|
2124
2793
|
await cleanupSession(newSid);
|
|
2125
2794
|
}, IDLE_TIMEOUT_MS);
|
|
@@ -2217,6 +2886,8 @@ var init_daemon2 = __esm({
|
|
|
2217
2886
|
init_dist();
|
|
2218
2887
|
init_session_state();
|
|
2219
2888
|
init_register_tools();
|
|
2889
|
+
init_html();
|
|
2890
|
+
init_local_api();
|
|
2220
2891
|
IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2221
2892
|
sessions = /* @__PURE__ */ new Map();
|
|
2222
2893
|
startedAt = Date.now();
|