@devness/useai 0.4.5 → 0.4.7

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