@devness/useai 0.4.5 → 0.4.6

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 +272 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -112,7 +112,7 @@ var VERSION;
112
112
  var init_version = __esm({
113
113
  "../shared/dist/constants/version.js"() {
114
114
  "use strict";
115
- VERSION = "0.4.5";
115
+ VERSION = "0.4.6";
116
116
  }
117
117
  });
118
118
 
@@ -2302,6 +2302,12 @@ function getDashboardHtml() {
2302
2302
  color: var(--muted);
2303
2303
  font-family: 'SF Mono', 'Fira Code', monospace;
2304
2304
  }
2305
+ .milestone-duration {
2306
+ font-size: 0.7rem;
2307
+ color: var(--muted);
2308
+ font-family: 'SF Mono', 'Fira Code', monospace;
2309
+ white-space: nowrap;
2310
+ }
2305
2311
  .milestone-date {
2306
2312
  font-size: 0.72rem;
2307
2313
  color: var(--muted);
@@ -2348,6 +2354,77 @@ function getDashboardHtml() {
2348
2354
  text-decoration: underline;
2349
2355
  }
2350
2356
 
2357
+ /* Login form */
2358
+ .login-form {
2359
+ max-width: 320px;
2360
+ margin: 0 auto;
2361
+ text-align: center;
2362
+ }
2363
+ .login-form h3 {
2364
+ font-size: 0.95rem;
2365
+ color: var(--text);
2366
+ margin-bottom: 4px;
2367
+ font-weight: 600;
2368
+ }
2369
+ .login-form .login-sub {
2370
+ font-size: 0.8rem;
2371
+ color: var(--muted);
2372
+ margin-bottom: 16px;
2373
+ }
2374
+ .login-input {
2375
+ width: 100%;
2376
+ padding: 10px 12px;
2377
+ background: var(--bg);
2378
+ border: 1px solid var(--border);
2379
+ border-radius: var(--radius);
2380
+ color: var(--text);
2381
+ font-size: 0.88rem;
2382
+ font-family: system-ui, sans-serif;
2383
+ outline: none;
2384
+ margin-bottom: 10px;
2385
+ }
2386
+ .login-input:focus { border-color: var(--amber); }
2387
+ .login-input::placeholder { color: #5a5248; }
2388
+ .login-input.otp-input {
2389
+ text-align: center;
2390
+ font-family: 'SF Mono', 'Fira Code', monospace;
2391
+ font-size: 1.2rem;
2392
+ letter-spacing: 6px;
2393
+ }
2394
+ .login-btn {
2395
+ width: 100%;
2396
+ padding: 10px;
2397
+ background: var(--amber);
2398
+ color: var(--bg);
2399
+ border: none;
2400
+ border-radius: var(--radius);
2401
+ font-weight: 600;
2402
+ font-size: 0.88rem;
2403
+ cursor: pointer;
2404
+ font-family: system-ui, sans-serif;
2405
+ transition: opacity 0.15s;
2406
+ }
2407
+ .login-btn:hover { opacity: 0.85; }
2408
+ .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
2409
+ .login-msg {
2410
+ margin-top: 10px;
2411
+ font-size: 0.8rem;
2412
+ }
2413
+ .login-msg.error { color: var(--red); }
2414
+ .login-msg.success { color: var(--green); }
2415
+ .login-msg.dim { color: var(--muted); }
2416
+ .login-link {
2417
+ background: none;
2418
+ border: none;
2419
+ color: var(--amber);
2420
+ cursor: pointer;
2421
+ font-size: 0.8rem;
2422
+ font-family: system-ui, sans-serif;
2423
+ text-decoration: underline;
2424
+ padding: 0;
2425
+ margin-top: 6px;
2426
+ }
2427
+
2351
2428
  .empty {
2352
2429
  text-align: center;
2353
2430
  color: var(--muted);
@@ -2450,6 +2527,14 @@ function getDashboardHtml() {
2450
2527
  return d.innerHTML;
2451
2528
  }
2452
2529
 
2530
+ function fmtDuration(mins) {
2531
+ if (!mins || mins <= 0) return '';
2532
+ if (mins < 60) return mins + 'm';
2533
+ var h = Math.floor(mins / 60);
2534
+ var m = mins % 60;
2535
+ return m > 0 ? h + 'h ' + m + 'm' : h + 'h';
2536
+ }
2537
+
2453
2538
  function renderMilestones(milestones) {
2454
2539
  var section = document.getElementById('milestones-section');
2455
2540
  var list = document.getElementById('milestones-list');
@@ -2462,17 +2547,21 @@ function getDashboardHtml() {
2462
2547
  var recent = milestones.slice(-20).reverse();
2463
2548
  list.innerHTML = recent.map(function(m) {
2464
2549
  var date = m.created_at ? m.created_at.slice(0, 10) : '';
2550
+ var dur = fmtDuration(m.duration_minutes);
2465
2551
  return '<div class="milestone-item">' +
2466
2552
  '<div class="milestone-title">' + escapeHtml(m.title) + '</div>' +
2467
2553
  '<div class="milestone-meta">' +
2468
2554
  '<span class="' + badgeClass(m.category) + '">' + escapeHtml(m.category) + '</span>' +
2469
2555
  (m.complexity ? '<span class="complexity">' + escapeHtml(m.complexity) + '</span>' : '') +
2556
+ (dur ? '<span class="milestone-duration">' + dur + '</span>' : '') +
2470
2557
  '<span class="milestone-date">' + escapeHtml(date) + '</span>' +
2471
2558
  '</div>' +
2472
2559
  '</div>';
2473
2560
  }).join('');
2474
2561
  }
2475
2562
 
2563
+ var loginEmail = '';
2564
+
2476
2565
  function renderSync(config) {
2477
2566
  var section = document.getElementById('sync-section');
2478
2567
  if (!config) { section.style.display = 'none'; return; }
@@ -2482,20 +2571,134 @@ function getDashboardHtml() {
2482
2571
  var lastSync = config.last_sync_at ? 'Last sync: ' + config.last_sync_at : 'Never synced';
2483
2572
  section.innerHTML = '<div class="sync-section">' +
2484
2573
  '<h2 style="font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:12px;font-weight:600;">Sync</h2>' +
2574
+ '<div class="sync-status" id="sync-status" style="margin-bottom:12px;">Logged in as <strong>' + escapeHtml(config.email || '') + '</strong></div>' +
2485
2575
  '<button class="sync-btn" id="sync-btn">Sync to useai.dev</button>' +
2486
- '<div class="sync-status" id="sync-status">' + escapeHtml(lastSync) + '</div>' +
2576
+ '<div class="sync-status" id="sync-time">' + escapeHtml(lastSync) + '</div>' +
2487
2577
  '<div class="sync-result" id="sync-result"></div>' +
2488
2578
  '</div>';
2489
2579
  document.getElementById('sync-btn').addEventListener('click', doSync);
2490
2580
  } else {
2491
- 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>';
2581
+ renderLoginForm();
2492
2582
  }
2493
2583
  }
2494
2584
 
2585
+ function renderLoginForm() {
2586
+ var section = document.getElementById('sync-section');
2587
+ section.innerHTML = '<div class="login-form" id="login-form">' +
2588
+ '<h3>Sign in to sync</h3>' +
2589
+ '<div class="login-sub">Sync sessions & milestones to useai.dev</div>' +
2590
+ '<input class="login-input" id="login-email" type="email" placeholder="you@email.com" autocomplete="email">' +
2591
+ '<button class="login-btn" id="login-send-btn">Send verification code</button>' +
2592
+ '<div class="login-msg" id="login-msg"></div>' +
2593
+ '</div>';
2594
+ document.getElementById('login-send-btn').addEventListener('click', sendOtp);
2595
+ document.getElementById('login-email').addEventListener('keydown', function(e) {
2596
+ if (e.key === 'Enter') sendOtp();
2597
+ });
2598
+ }
2599
+
2600
+ function renderOtpForm() {
2601
+ var section = document.getElementById('sync-section');
2602
+ section.innerHTML = '<div class="login-form" id="otp-form">' +
2603
+ '<h3>Check your email</h3>' +
2604
+ '<div class="login-sub">Enter the 6-digit code sent to ' + escapeHtml(loginEmail) + '</div>' +
2605
+ '<input class="login-input otp-input" id="login-otp" type="text" maxlength="6" placeholder="000000" inputmode="numeric" autocomplete="one-time-code">' +
2606
+ '<button class="login-btn" id="login-verify-btn">Verify</button>' +
2607
+ '<div class="login-msg" id="login-msg"></div>' +
2608
+ '<button class="login-link" id="login-resend">Resend code</button>' +
2609
+ '</div>';
2610
+ document.getElementById('login-verify-btn').addEventListener('click', verifyOtp);
2611
+ document.getElementById('login-resend').addEventListener('click', function() { sendOtp(); });
2612
+ document.getElementById('login-otp').addEventListener('keydown', function(e) {
2613
+ if (e.key === 'Enter') verifyOtp();
2614
+ });
2615
+ document.getElementById('login-otp').focus();
2616
+ }
2617
+
2618
+ function sendOtp() {
2619
+ var emailEl = document.getElementById('login-email');
2620
+ var msg = document.getElementById('login-msg');
2621
+ var btn = document.getElementById('login-send-btn') || document.getElementById('login-resend');
2622
+ var email = emailEl ? emailEl.value.trim() : loginEmail;
2623
+
2624
+ if (!email || email.indexOf('@') === -1) {
2625
+ msg.textContent = 'Please enter a valid email';
2626
+ msg.className = 'login-msg error';
2627
+ return;
2628
+ }
2629
+
2630
+ loginEmail = email;
2631
+ if (btn) { btn.disabled = true; btn.textContent = btn.id === 'login-resend' ? 'Sending...' : 'Sending code...'; }
2632
+
2633
+ fetch(API + '/api/local/auth/send-otp', {
2634
+ method: 'POST',
2635
+ headers: { 'Content-Type': 'application/json' },
2636
+ body: JSON.stringify({ email: email }),
2637
+ })
2638
+ .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
2639
+ .then(function(res) {
2640
+ if (!res.ok) {
2641
+ msg.textContent = res.data.message || 'Failed to send code';
2642
+ msg.className = 'login-msg error';
2643
+ if (btn) { btn.disabled = false; btn.textContent = 'Send verification code'; }
2644
+ return;
2645
+ }
2646
+ renderOtpForm();
2647
+ })
2648
+ .catch(function(err) {
2649
+ msg.textContent = 'Error: ' + err.message;
2650
+ msg.className = 'login-msg error';
2651
+ if (btn) { btn.disabled = false; btn.textContent = 'Send verification code'; }
2652
+ });
2653
+ }
2654
+
2655
+ function verifyOtp() {
2656
+ var otpEl = document.getElementById('login-otp');
2657
+ var msg = document.getElementById('login-msg');
2658
+ var btn = document.getElementById('login-verify-btn');
2659
+ var code = otpEl ? otpEl.value.trim() : '';
2660
+
2661
+ if (!/^d{6}$/.test(code)) {
2662
+ msg.textContent = 'Please enter the 6-digit code';
2663
+ msg.className = 'login-msg error';
2664
+ return;
2665
+ }
2666
+
2667
+ btn.disabled = true;
2668
+ btn.textContent = 'Verifying...';
2669
+ msg.textContent = '';
2670
+
2671
+ fetch(API + '/api/local/auth/verify-otp', {
2672
+ method: 'POST',
2673
+ headers: { 'Content-Type': 'application/json' },
2674
+ body: JSON.stringify({ email: loginEmail, code: code }),
2675
+ })
2676
+ .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
2677
+ .then(function(res) {
2678
+ if (!res.ok) {
2679
+ msg.textContent = res.data.message || 'Invalid code';
2680
+ msg.className = 'login-msg error';
2681
+ btn.disabled = false;
2682
+ btn.textContent = 'Verify';
2683
+ return;
2684
+ }
2685
+ // Success \u2014 reload config and re-render sync section
2686
+ msg.textContent = 'Logged in!';
2687
+ msg.className = 'login-msg success';
2688
+ setTimeout(function() { loadAll(); }, 500);
2689
+ })
2690
+ .catch(function(err) {
2691
+ msg.textContent = 'Error: ' + err.message;
2692
+ msg.className = 'login-msg error';
2693
+ btn.disabled = false;
2694
+ btn.textContent = 'Verify';
2695
+ });
2696
+ }
2697
+
2495
2698
  function doSync() {
2496
2699
  var btn = document.getElementById('sync-btn');
2497
2700
  var result = document.getElementById('sync-result');
2498
- var status = document.getElementById('sync-status');
2701
+ var timeEl = document.getElementById('sync-time');
2499
2702
  btn.disabled = true;
2500
2703
  btn.textContent = 'Syncing...';
2501
2704
  result.textContent = '';
@@ -2509,7 +2712,7 @@ function getDashboardHtml() {
2509
2712
  if (data.success) {
2510
2713
  result.textContent = 'Synced successfully';
2511
2714
  result.className = 'sync-result success';
2512
- if (data.last_sync_at) status.textContent = 'Last sync: ' + data.last_sync_at;
2715
+ if (data.last_sync_at && timeEl) timeEl.textContent = 'Last sync: ' + data.last_sync_at;
2513
2716
  } else {
2514
2717
  result.textContent = 'Sync failed: ' + (data.error || 'Unknown error');
2515
2718
  result.className = 'sync-result error';
@@ -2725,10 +2928,66 @@ async function handleLocalSync(req, res) {
2725
2928
  json(res, 500, { success: false, error: err2.message });
2726
2929
  }
2727
2930
  }
2931
+ async function handleLocalSendOtp(req, res) {
2932
+ try {
2933
+ const raw = await readBody(req);
2934
+ const body = raw ? JSON.parse(raw) : {};
2935
+ const apiRes = await fetch(`${USEAI_API}/api/auth/send-otp`, {
2936
+ method: "POST",
2937
+ headers: { "Content-Type": "application/json" },
2938
+ body: JSON.stringify({ email: body.email })
2939
+ });
2940
+ const data = await apiRes.json();
2941
+ if (!apiRes.ok) {
2942
+ json(res, apiRes.status, data);
2943
+ return;
2944
+ }
2945
+ json(res, 200, data);
2946
+ } catch (err2) {
2947
+ json(res, 500, { error: err2.message });
2948
+ }
2949
+ }
2950
+ async function handleLocalVerifyOtp(req, res) {
2951
+ try {
2952
+ const raw = await readBody(req);
2953
+ const body = raw ? JSON.parse(raw) : {};
2954
+ const apiRes = await fetch(`${USEAI_API}/api/auth/verify-otp`, {
2955
+ method: "POST",
2956
+ headers: { "Content-Type": "application/json" },
2957
+ body: JSON.stringify({ email: body.email, code: body.code })
2958
+ });
2959
+ const data = await apiRes.json();
2960
+ if (!apiRes.ok) {
2961
+ json(res, apiRes.status, data);
2962
+ return;
2963
+ }
2964
+ if (data.token && data.user) {
2965
+ const config = readJson(CONFIG_FILE, {
2966
+ milestone_tracking: true,
2967
+ auto_sync: true,
2968
+ sync_interval_hours: 24
2969
+ });
2970
+ config.auth = {
2971
+ token: data.token,
2972
+ user: {
2973
+ id: data.user.id,
2974
+ email: data.user.email,
2975
+ username: data.user.username
2976
+ }
2977
+ };
2978
+ writeJson(CONFIG_FILE, config);
2979
+ }
2980
+ json(res, 200, { success: true, email: data.user?.email, username: data.user?.username });
2981
+ } catch (err2) {
2982
+ json(res, 500, { error: err2.message });
2983
+ }
2984
+ }
2985
+ var USEAI_API;
2728
2986
  var init_local_api = __esm({
2729
2987
  "src/dashboard/local-api.ts"() {
2730
2988
  "use strict";
2731
2989
  init_dist();
2990
+ USEAI_API = "https://api.useai.dev";
2732
2991
  }
2733
2992
  });
2734
2993
 
@@ -3017,6 +3276,14 @@ async function startDaemon(port) {
3017
3276
  await handleLocalSync(req, res);
3018
3277
  return;
3019
3278
  }
3279
+ if (url.pathname === "/api/local/auth/send-otp" && req.method === "POST") {
3280
+ await handleLocalSendOtp(req, res);
3281
+ return;
3282
+ }
3283
+ if (url.pathname === "/api/local/auth/verify-otp" && req.method === "POST") {
3284
+ await handleLocalVerifyOtp(req, res);
3285
+ return;
3286
+ }
3020
3287
  if (url.pathname === "/api/seal-active" && req.method === "POST") {
3021
3288
  const sids = [...sessions.keys()];
3022
3289
  for (const sid of sids) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devness/useai",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
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",