pawmode 1.3.0 → 1.5.0

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.
@@ -5,49 +5,49 @@ import * as http from "node:http";
5
5
  import * as crypto from "node:crypto";
6
6
 
7
7
  //#region src/core/dashboard-html.ts
8
+ const THEME_COLORS = {
9
+ paw: {
10
+ bg: "#1a1008",
11
+ surface: "#241a10",
12
+ surfaceHover: "#2e2218",
13
+ border: "#3a2a18",
14
+ text: "#e8d8c8",
15
+ textDim: "#8a7a6a",
16
+ accent: "#b4783c",
17
+ accentDim: "#8a5a2a",
18
+ done: "#6a9a5a",
19
+ high: "#d44",
20
+ low: "#666"
21
+ },
22
+ midnight: {
23
+ bg: "#0a0a0f",
24
+ surface: "#12121a",
25
+ surfaceHover: "#1a1a25",
26
+ border: "#222233",
27
+ text: "#d0d0e0",
28
+ textDim: "#6a6a8a",
29
+ accent: "#6688cc",
30
+ accentDim: "#445588",
31
+ done: "#5a8a5a",
32
+ high: "#cc5555",
33
+ low: "#555566"
34
+ },
35
+ neon: {
36
+ bg: "#050505",
37
+ surface: "#0a0a0a",
38
+ surfaceHover: "#111111",
39
+ border: "#1a1a1a",
40
+ text: "#e0e0e0",
41
+ textDim: "#555",
42
+ accent: "#00ff88",
43
+ accentDim: "#008844",
44
+ done: "#00cc66",
45
+ high: "#ff3355",
46
+ low: "#444"
47
+ }
48
+ };
8
49
  function generateDashboardHTML(theme, botName) {
9
- const themes = {
10
- paw: {
11
- bg: "#1a1008",
12
- surface: "#241a10",
13
- surfaceHover: "#2e2218",
14
- border: "#3a2a18",
15
- text: "#e8d8c8",
16
- textDim: "#8a7a6a",
17
- accent: "#b4783c",
18
- accentDim: "#8a5a2a",
19
- done: "#6a9a5a",
20
- high: "#d44",
21
- low: "#666"
22
- },
23
- midnight: {
24
- bg: "#0a0a0f",
25
- surface: "#12121a",
26
- surfaceHover: "#1a1a25",
27
- border: "#222233",
28
- text: "#d0d0e0",
29
- textDim: "#6a6a8a",
30
- accent: "#6688cc",
31
- accentDim: "#445588",
32
- done: "#5a8a5a",
33
- high: "#cc5555",
34
- low: "#555566"
35
- },
36
- neon: {
37
- bg: "#050505",
38
- surface: "#0a0a0a",
39
- surfaceHover: "#111111",
40
- border: "#1a1a1a",
41
- text: "#e0e0e0",
42
- textDim: "#555",
43
- accent: "#00ff88",
44
- accentDim: "#008844",
45
- done: "#00cc66",
46
- high: "#ff3355",
47
- low: "#444"
48
- }
49
- };
50
- const t = themes[theme];
50
+ const t = THEME_COLORS[theme];
51
51
  const safeBotName = botName.replace(/[&<>"']/g, "");
52
52
  return `<!DOCTYPE html>
53
53
  <html lang="en">
@@ -388,6 +388,162 @@ load();
388
388
  </body>
389
389
  </html>`;
390
390
  }
391
+ function generateFocusTimerHTML(theme, botName, endsAt, duration) {
392
+ const t = THEME_COLORS[theme];
393
+ const safeBotName = botName.replace(/[&<>"']/g, "");
394
+ const safeEndsAt = endsAt.replace(/[&<>"']/g, "");
395
+ const safeDuration = duration.replace(/[^0-9]/g, "");
396
+ return `<!DOCTYPE html>
397
+ <html lang="en">
398
+ <head>
399
+ <meta charset="UTF-8">
400
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
401
+ <title>${safeBotName} — Locked In</title>
402
+ <link rel="preconnect" href="https://fonts.googleapis.com">
403
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
404
+ <style>
405
+ *{margin:0;padding:0;box-sizing:border-box}
406
+ body{
407
+ font-family:'JetBrains Mono',monospace;
408
+ background:${t.bg};color:${t.text};
409
+ min-height:100vh;display:flex;flex-direction:column;
410
+ align-items:center;justify-content:center;
411
+ overflow:hidden;
412
+ }
413
+ .container{text-align:center;position:relative;z-index:1}
414
+ .ring-wrap{position:relative;width:200px;height:200px;margin:0 auto 20px}
415
+ .ring-svg{width:200px;height:200px;transform:rotate(-90deg)}
416
+ .ring-bg{fill:none;stroke:${t.border};stroke-width:4}
417
+ .ring-fg{fill:none;stroke:${t.accent};stroke-width:4;stroke-linecap:round;transition:stroke-dashoffset 1s linear}
418
+ .paw-ascii{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:13px;line-height:1.15;color:${t.accent};white-space:pre;animation:breathe 4s ease-in-out infinite}
419
+ .trail{font-size:14px;color:${t.textDim};letter-spacing:8px;margin-bottom:16px;height:20px}
420
+ .trail span{display:inline-block;animation:walk 2s ease-in-out infinite}
421
+ .trail span:nth-child(1){animation-delay:0s}
422
+ .trail span:nth-child(2){animation-delay:.25s}
423
+ .trail span:nth-child(3){animation-delay:.5s}
424
+ .trail span:nth-child(4){animation-delay:.75s}
425
+ .trail span:nth-child(5){animation-delay:1s}
426
+ .label{font-size:12px;text-transform:uppercase;letter-spacing:3px;color:${t.accent};font-weight:600;margin-bottom:24px}
427
+ .timer{font-size:64px;font-weight:700;letter-spacing:4px;color:${t.text};line-height:1;margin-bottom:12px;font-variant-numeric:tabular-nums}
428
+ .sub{font-size:12px;color:${t.textDim};margin-bottom:20px}
429
+ .quote{font-size:11px;color:${t.textDim};font-style:italic;height:16px;transition:opacity .8s}
430
+ .session-info{font-size:11px;color:${t.textDim};display:flex;gap:20px;justify-content:center;margin-top:20px}
431
+ .session-info span{display:flex;align-items:center;gap:5px}
432
+ .dot{width:5px;height:5px;border-radius:50%;background:${t.accent};animation:pulse 2s ease-in-out infinite}
433
+ .complete .label{color:${t.done}}
434
+ .complete .timer{color:${t.done}}
435
+ .complete .dot{background:${t.done};animation:none}
436
+ .complete .ring-fg{stroke:${t.done}}
437
+ .complete .paw-ascii{color:${t.done};animation:none}
438
+ .complete .trail span{animation:none;color:${t.done}}
439
+ @keyframes breathe{0%,100%{opacity:.6;transform:translate(-50%,-50%) scale(.95)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.05)}}
440
+ @keyframes walk{0%,100%{opacity:.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-3px)}}
441
+ @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
442
+ @keyframes flash{0%,100%{opacity:1}50%{opacity:.3}}
443
+ .flash .timer{animation:flash .5s ease-in-out 3}
444
+ .glow{position:fixed;width:250px;height:250px;border-radius:50%;background:${t.accent};opacity:.03;filter:blur(80px);pointer-events:none}
445
+ .glow-1{top:-80px;left:-80px}
446
+ .glow-2{bottom:-80px;right:-80px}
447
+ .glow-3{top:50%;left:50%;transform:translate(-50%,-50%);opacity:.015}
448
+ </style>
449
+ </head>
450
+ <body>
451
+ <div class="glow glow-1"></div>
452
+ <div class="glow glow-2"></div>
453
+ <div class="glow glow-3"></div>
454
+ <div class="container" id="container">
455
+ <div class="ring-wrap">
456
+ <svg class="ring-svg" viewBox="0 0 200 200">
457
+ <circle class="ring-bg" cx="100" cy="100" r="90"/>
458
+ <circle class="ring-fg" id="ring" cx="100" cy="100" r="90" stroke-dasharray="565.49" stroke-dashoffset="565.49"/>
459
+ </svg>
460
+ <pre class="paw-ascii" id="paw"> __
461
+ / \\
462
+ | .. |
463
+ \\ -- /
464
+ \\__/
465
+ ||</pre>
466
+ </div>
467
+ <div class="trail" id="trail"><span>.</span><span>o</span><span>O</span><span>o</span><span>.</span></div>
468
+ <div class="label" id="label">Locked In</div>
469
+ <div class="timer" id="timer">--:--</div>
470
+ <div class="sub" id="sub">${safeDuration} min session</div>
471
+ <div class="quote" id="quote"></div>
472
+ <div class="session-info">
473
+ <span><span class="dot"></span> Focus active</span>
474
+ </div>
475
+ </div>
476
+ <script>
477
+ var endsAt = "${safeEndsAt}";
478
+ var durationMs = ${safeDuration} * 60000;
479
+ var endTime = endsAt ? new Date(endsAt).getTime() : 0;
480
+ var startTime = endTime - durationMs;
481
+ var circumference = 2 * Math.PI * 90;
482
+ var done = false;
483
+ var ring = document.getElementById("ring");
484
+ var quotes = [
485
+ "Deep work is the superpower of the 21st century.",
486
+ "Focus is not about saying yes. It's about saying no.",
487
+ "The successful warrior is the average person with laser focus.",
488
+ "What you stay focused on will grow.",
489
+ "Starve your distractions. Feed your focus.",
490
+ "Small daily improvements lead to stunning results.",
491
+ "You don't need more time. You need more focus.",
492
+ "Discipline is choosing what you want most over what you want now."
493
+ ];
494
+ var qIdx = 0;
495
+
496
+ function pad(n) { return n < 10 ? "0" + n : "" + n; }
497
+
498
+ function updateRing(progress) {
499
+ var offset = circumference * (1 - progress);
500
+ ring.style.strokeDashoffset = Math.max(0, offset);
501
+ }
502
+
503
+ function tick() {
504
+ if (!endTime || done) return;
505
+ var now = Date.now();
506
+ var diff = endTime - now;
507
+ var elapsed = now - startTime;
508
+ var progress = Math.min(1, Math.max(0, elapsed / durationMs));
509
+ updateRing(progress);
510
+
511
+ if (diff <= 0) {
512
+ done = true;
513
+ document.getElementById("timer").textContent = "00:00";
514
+ document.getElementById("label").textContent = "Session Complete";
515
+ document.getElementById("sub").textContent = "Great work! Take a break.";
516
+ document.getElementById("paw").textContent = " __\\n / \\\\\\n| ^^ |\\n\\\\ \\u2323 /\\n \\\\__/\\n ||";
517
+ document.getElementById("container").classList.add("complete", "flash");
518
+ document.title = "${safeBotName} \\u2014 Done!";
519
+ updateRing(1);
520
+ return;
521
+ }
522
+
523
+ var h = Math.floor(diff / 3600000);
524
+ var m = Math.floor((diff % 3600000) / 60000);
525
+ var s = Math.floor((diff % 60000) / 1000);
526
+ document.getElementById("timer").textContent = h > 0 ? pad(h) + ":" + pad(m) + ":" + pad(s) : pad(m) + ":" + pad(s);
527
+ }
528
+
529
+ function rotateQuote() {
530
+ var el = document.getElementById("quote");
531
+ el.style.opacity = "0";
532
+ setTimeout(function() {
533
+ el.textContent = quotes[qIdx % quotes.length];
534
+ el.style.opacity = "1";
535
+ qIdx++;
536
+ }, 800);
537
+ }
538
+
539
+ tick();
540
+ setInterval(tick, 1000);
541
+ rotateQuote();
542
+ setInterval(rotateQuote, 12000);
543
+ </script>
544
+ </body>
545
+ </html>`;
546
+ }
391
547
 
392
548
  //#endregion
393
549
  //#region src/core/dashboard-server.ts
@@ -455,6 +611,23 @@ function startDashboard(opts) {
455
611
  html(res, generateDashboardHTML(current.theme, current.botName));
456
612
  return;
457
613
  }
614
+ if (method === "GET" && pathname === "/focus") {
615
+ const current = readConfig();
616
+ const ends = url.searchParams.get("ends") || "";
617
+ const dur = url.searchParams.get("duration") || "0";
618
+ html(res, generateFocusTimerHTML(current.theme, current.botName, ends, dur));
619
+ return;
620
+ }
621
+ if (method === "GET" && pathname === "/api/focus") {
622
+ try {
623
+ const sessionPath = path$1.join(CONFIG_DIR, "lockin-session.json");
624
+ const raw = fs$1.readFileSync(sessionPath, "utf-8");
625
+ json(res, JSON.parse(raw));
626
+ } catch {
627
+ json(res, { active: false });
628
+ }
629
+ return;
630
+ }
458
631
  if (method === "GET" && pathname === "/api/tasks") {
459
632
  json(res, readConfig().tasks);
460
633
  return;
@@ -521,9 +694,11 @@ function startDashboard(opts) {
521
694
  server.listen(port, () => {
522
695
  const url = `http://localhost:${port}`;
523
696
  console.log(`\n Dashboard running at ${url}\n`);
524
- const platform = os$1.platform();
525
- if (platform === "darwin") import("node:child_process").then((cp) => cp.exec(`open ${url}`));
526
- else if (platform === "linux") import("node:child_process").then((cp) => cp.exec(`xdg-open ${url}`));
697
+ if (!opts.noOpen) {
698
+ const platform = os$1.platform();
699
+ if (platform === "darwin") import("node:child_process").then((cp) => cp.exec(`open ${url}`));
700
+ else if (platform === "linux") import("node:child_process").then((cp) => cp.exec(`xdg-open ${url}`));
701
+ }
527
702
  });
528
703
  return server;
529
704
  }