@devness/useai 0.4.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +683 -5
  2. 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.2";
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" }));
@@ -2099,6 +2764,13 @@ async function startDaemon(port) {
2099
2764
  if (sid && sessions.has(sid)) {
2100
2765
  resetIdleTimer(sid);
2101
2766
  await sessions.get(sid).transport.handleRequest(req, res, body);
2767
+ } else if (sid && !sessions.has(sid)) {
2768
+ res.writeHead(404, { "Content-Type": "application/json" });
2769
+ res.end(JSON.stringify({
2770
+ jsonrpc: "2.0",
2771
+ error: { code: -32e3, message: "Session not found" },
2772
+ id: null
2773
+ }));
2102
2774
  } else if (!sid && isInitializeRequest(body)) {
2103
2775
  const sessionState = new SessionState();
2104
2776
  try {
@@ -2113,6 +2785,10 @@ async function startDaemon(port) {
2113
2785
  const transport = new StreamableHTTPServerTransport({
2114
2786
  sessionIdGenerator: () => randomUUID4(),
2115
2787
  onsessioninitialized: (newSid) => {
2788
+ const clientInfo = mcpServer.server.getClientVersion();
2789
+ if (clientInfo?.name) {
2790
+ sessionState.setClient(normalizeMcpClientName(clientInfo.name));
2791
+ }
2116
2792
  const idleTimer = setTimeout(async () => {
2117
2793
  await cleanupSession(newSid);
2118
2794
  }, IDLE_TIMEOUT_MS);
@@ -2146,8 +2822,8 @@ async function startDaemon(port) {
2146
2822
  } else if (req.method === "GET") {
2147
2823
  const sid = req.headers["mcp-session-id"];
2148
2824
  if (!sid || !sessions.has(sid)) {
2149
- res.writeHead(400, { "Content-Type": "application/json" });
2150
- res.end(JSON.stringify({ error: "Invalid or missing session ID" }));
2825
+ res.writeHead(sid ? 404 : 400, { "Content-Type": "application/json" });
2826
+ res.end(JSON.stringify({ error: sid ? "Session not found" : "Missing session ID" }));
2151
2827
  return;
2152
2828
  }
2153
2829
  resetIdleTimer(sid);
@@ -2155,8 +2831,8 @@ async function startDaemon(port) {
2155
2831
  } else if (req.method === "DELETE") {
2156
2832
  const sid = req.headers["mcp-session-id"];
2157
2833
  if (!sid || !sessions.has(sid)) {
2158
- res.writeHead(400, { "Content-Type": "application/json" });
2159
- res.end(JSON.stringify({ error: "Invalid or missing session ID" }));
2834
+ res.writeHead(sid ? 404 : 400, { "Content-Type": "application/json" });
2835
+ res.end(JSON.stringify({ error: sid ? "Session not found" : "Missing session ID" }));
2160
2836
  return;
2161
2837
  }
2162
2838
  await sessions.get(sid).transport.handleRequest(req, res);
@@ -2210,6 +2886,8 @@ var init_daemon2 = __esm({
2210
2886
  init_dist();
2211
2887
  init_session_state();
2212
2888
  init_register_tools();
2889
+ init_html();
2890
+ init_local_api();
2213
2891
  IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
2214
2892
  sessions = /* @__PURE__ */ new Map();
2215
2893
  startedAt = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devness/useai",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Track your AI-assisted development workflow. MCP server that records usage metrics across all your AI tools.",
5
5
  "keywords": [
6
6
  "mcp",