pawmode 1.4.0 → 1.5.1
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/README.md
CHANGED
|
@@ -1,50 +1,5 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<pre align="center">
|
|
3
|
-
▃▅
|
|
4
|
-
▁██▁ ▄█▁
|
|
5
|
-
▁▁▂▆▇██▃ ▅█▆
|
|
6
|
-
▅▆█▇▆▄███▆▂ ▁▂▆▇██▇▅▁
|
|
7
|
-
▁▃█▆▁ ▄███▄▁ ▆▇▇▅▄▄███▇▃▁
|
|
8
|
-
▁▃█▄ ▁████▂ ▁▅█▃ ▁▃████▂
|
|
9
|
-
▄█▇▁ ▂████▄▁ ▁▃█▇ ▁████▅▂
|
|
10
|
-
▅█▆ ▂█████▁ ▄█▇▁ ▂████▃
|
|
11
|
-
▂▁ ▆█▆ ▂█████▁ ▄█▇ ▁▂████▂
|
|
12
|
-
▁▅█▂ ▆█▆▁ ▁▃▇████▆▁ ▄█▇ ▂▄█████▂
|
|
13
|
-
▁██▄▂ ▂██▇▆▃▄▇██████ ▄██▄▇▇▃▃▆█████▁
|
|
14
|
-
▂█████▇▅▂▁ ▁▄▇█████████▆▂ ▃▇████████████▁ ▁▂▁
|
|
15
|
-
▄█▇▃ ▁███▆▁ ▁▃▆▇▇▇▇▃▄▂ ▂▆█████████▅ ▁▇█▂
|
|
16
|
-
▃▆▄▁ ▅███▆▃ ▄▄▅▅▅▅▂▁ ▂▄▃▄██▂
|
|
17
|
-
▆█▄ ▃████▄ ▁▄▄██████▄▃▃▂ ▁▂▆▇▇▆████▇▁
|
|
18
|
-
▆█▄ ▂████▄ ▂▅▇▇▅▅▁▃▅█▇████▆▂ ▅▇▇▅▄▁ ▂▆████▂
|
|
19
|
-
▇█▄ ▅████▄ ▇█▄ ▁▃▇▆████▇▂ ▂▃█▇▃ ▁ ▃███▂
|
|
20
|
-
▇██▅ ▂▆████▄▁ ▆█▄▁ ▁ ▅▄▅█████▅ ▆█▆▂ ▃▆███▁
|
|
21
|
-
▁████████████▃ ▂▇▆▂ ▄█████▆ ▃██▂ ▂▆███▆
|
|
22
|
-
▃█████████▅▂ ██▂ ▄█████▆ ▅██▁ ▁████▇
|
|
23
|
-
▂▄▄▄▄▄▄ ▇█▃▁ ▁▅█████▅ ▃██▄▇▆▁▁▄▇████▆
|
|
24
|
-
▁▁▁▇█▂▁ ▁ ▄██████▆▂ ▇███████████▅▁
|
|
25
|
-
▁▁▄▅▆██▅▂ ▁▁ ▄███████▄ ▄▅██████▆▅▂
|
|
26
|
-
▄██▆▅▄▂▁ ▁ ▃▆██████▇▄▂ ▁▁▁▁▁▁▁
|
|
27
|
-
▂██▄▁▁ ▁▄▅██████▇▆▂▁
|
|
28
|
-
▃▇▇▂ ▁▁ ▁ ▃▇██████▇▃▁
|
|
29
|
-
▁▄█▆ ▁ ▁▆█▇█████▆▂
|
|
30
|
-
▄██▇ ▃▅███████▅▁
|
|
31
|
-
▄██▇▁ ▂▂▂▁▁▂▆▇▇▇▇▇▃▁ ▄▆▄▂▇█████▃
|
|
32
|
-
▃▇██▇▃ ▁▂▆▆██▆▇█████████▆▄▁▁ ▁▁▁▄▇██████▃
|
|
33
|
-
▄█████▆▆▇███████████████████▆▃▁ ▁▄▇███████▃
|
|
34
|
-
▁▅█████████████████████████████▅█████████▄▁
|
|
35
|
-
▁▂▆█████████████████████████████████████▂
|
|
36
|
-
▁▂▆▆▆▆▆▆▃▂▂▂▂▂▂▂▂▂▂▂▂▂▅▆███████████▇▆▁
|
|
37
|
-
▃▃▇▇▇▇▇▇▇▃▃
|
|
38
|
-
|
|
39
|
-
█▀▀█░█▀▀▄░█▀▀▀░█▄ █░█▀▀▄░▄▀▀▄░█ █░
|
|
40
|
-
█ █░█▀▀ ░█▀▀ ░█ ▀█░█▀▀ ░█▀▀█░█ █ █░
|
|
41
|
-
▀▀▀▀░▀ ░▀▀▀▀░▀ ▀░▀ ░▀ ▀░ ▀ ▀ ░
|
|
42
|
-
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
43
|
-
</pre>
|
|
44
|
-
</p>
|
|
45
|
-
|
|
46
1
|
<h1 align="center">OpenPaw</h1>
|
|
47
|
-
<p align="center"><b>
|
|
2
|
+
<p align="center"><b>Turn Claude Code into a personal assistant.</b></p>
|
|
48
3
|
|
|
49
4
|
<p align="center">
|
|
50
5
|
<a href="https://www.npmjs.com/package/pawmode"><img src="https://img.shields.io/npm/v/pawmode?color=b4783c&label=npm&style=flat-square" alt="npm"></a>
|
|
@@ -53,18 +8,51 @@
|
|
|
53
8
|
<img src="https://img.shields.io/node/v/pawmode?color=8a5a2a&style=flat-square" alt="node">
|
|
54
9
|
</p>
|
|
55
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="docs/demo.gif" alt="OpenPaw Setup Demo" width="700">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
56
15
|
---
|
|
57
16
|
|
|
17
|
+
## What is OpenPaw?
|
|
18
|
+
|
|
19
|
+
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) is powerful, but out of the box it only knows how to code. OpenPaw turns it into a full personal assistant — one that reads your email, plays your music, controls your lights, manages your calendar, tracks your packages, and remembers what you told it last week.
|
|
20
|
+
|
|
21
|
+
Run one command and OpenPaw:
|
|
22
|
+
|
|
23
|
+
- **Installs 38 skills** across email, calendar, Spotify, smart home, Slack, GitHub, Obsidian, and more
|
|
24
|
+
- **Sets up the CLI tools** each skill needs (via Homebrew, npm, or pip) and walks you through auth
|
|
25
|
+
- **Gives Claude an identity** — a name, personality, and persistent memory across sessions
|
|
26
|
+
- **Configures permissions and safety hooks** so Claude can act autonomously without doing anything dangerous
|
|
27
|
+
- **Adds a Telegram bridge** so you can talk to Claude from your phone
|
|
28
|
+
- **Includes a task dashboard** — a local kanban board with drag-and-drop and 3 themes
|
|
29
|
+
- **Enables smart scheduling** — recurring tasks with per-run and daily cost caps so nothing runs away
|
|
30
|
+
|
|
31
|
+
After setup, you just talk to Claude:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
"Check my email and summarize anything urgent"
|
|
35
|
+
"Play lo-fi beats on Spotify"
|
|
36
|
+
"Turn the bedroom lights to 20%"
|
|
37
|
+
"What's on my calendar tomorrow?"
|
|
38
|
+
"Add milk to my reminders"
|
|
39
|
+
"Go to Hacker News and summarize the top 5 posts"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Claude figures out which skill to use. No special syntax needed.
|
|
43
|
+
|
|
44
|
+
**No daemon, no cloud, no subscriptions.** OpenPaw runs once, writes config, and gets out of the way. Everything runs locally through your existing Claude Code subscription.
|
|
45
|
+
|
|
58
46
|
## Quick Start
|
|
59
47
|
|
|
60
48
|
```bash
|
|
61
49
|
npx pawmode
|
|
62
50
|
```
|
|
63
51
|
|
|
64
|
-
|
|
52
|
+
The wizard walks you through picking skills, choosing your interface (terminal, Telegram, or both), setting up a task dashboard, and authenticating with services. Or skip the wizard entirely:
|
|
65
53
|
|
|
66
54
|
```bash
|
|
67
|
-
npx pawmode --preset essentials #
|
|
55
|
+
npx pawmode --preset essentials # common skills, no prompts
|
|
68
56
|
npx pawmode --preset developer --yes # fully non-interactive
|
|
69
57
|
```
|
|
70
58
|
|
|
@@ -173,6 +161,10 @@ npx pawmode --preset developer --yes # fully non-interactive
|
|
|
173
161
|
|
|
174
162
|
**Task Dashboard** — Local kanban board with 3 themes (Paw, Midnight, Neon). Run `openpaw dashboard`.
|
|
175
163
|
|
|
164
|
+
<p align="center">
|
|
165
|
+
<img src="docs/dashboard.png" alt="OpenPaw Task Dashboard" width="700">
|
|
166
|
+
</p>
|
|
167
|
+
|
|
176
168
|
**Scheduling** — Recurring tasks with per-run and daily cost caps. `openpaw schedule add "weekdays 8am" --run "check email"`.
|
|
177
169
|
|
|
178
170
|
**CLAUDE.md Integration** — OpenPaw writes a section into `~/.claude/CLAUDE.md` so Claude knows its name, installed skills, and personality on every session start. Your existing CLAUDE.md content is preserved.
|
|
@@ -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
|
|
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,135 @@ 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
|
+
const catArt = "⠀⠀⠀⠀⠀⠀⣀⡠⠄⠒⠠⢄⠀⣀⠤⠒⠂⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⠐⠒⠤⡀⢀⡠⠔⠂⠠⢄⣀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⣠⠞⠂⢀⣀⣀⠀⠀⠉⠁⠀⣀⣀⣀⠀⠑⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠋⠀⣀⣀⣀⠀⠈⠉⠀⢀⣀⣀⠀⠈⠲⡀⠀⠀⠀⠀\n⠀⠀⠀⣸⡏⠀⣴⢋⠤⢌⠙⠆⠀⢠⠎⢁⠤⠍⢧⠀⠀⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡿⠁⢠⡞⡡⠤⡉⠳⠀⠀⡴⠉⡠⠬⡹⡄⠀⢸⡄⠀⠀⠀\n⠀⠀⢀⣽⡁⠀⣿⡀⢀⡘⡄⡇⠀⢸⢢⠇⢀⢢⣺⠀⠀⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣨⣇⠀⢸⣏⠀⡐⢣⢸⠀⠀⡗⡜⢀⠐⣄⣧⠀⢸⣇⠀⠀⠀\n⠀⣰⠉⠙⠷⠀⢛⣟⣦⣄⣣⡇⠀⠘⣎⣰⣤⣾⡿⠁⠴⠋⠈⠱⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡎⠉⠻⠆⠈⣿⣳⣤⣙⣼⠂⠀⢻⣁⣦⣼⣿⠇⠠⠞⠁⠙⣆⠀\n⣺⠁⠀⣠⠒⠶⣄⠉⠓⢚⣣⡠⠤⢤⣈⡓⠛⠋⢠⠖⠲⢤⡀⠀⢷⠀⠀⠀⠀⠀⠀⠀⢐⡟⠀⢀⠔⠲⢦⡈⠑⠛⣋⣡⠤⠤⣄⣙⠚⠛⢁⡴⠒⠦⣄⠀⠨⡆\n⣽⠀⢸⠄⠣⡐⢹⠀⡤⠏⢁⢀⢀⠠⢀⠉⢦⡀⢹⡁⠔⡁⣗⠀⢸⡅⠀⠀⠀⠀⠀⠀⢸⡇⠀⡗⠘⢄⢈⡇⢠⠖⠉⡀⣀⠠⠄⡈⠳⣄⠈⣟⠠⢈⢸⠀⠀⣿\n⠹⣄⠸⣷⣤⣧⠞⣾⢁⠐⢀⠀⠠⠄⣀⠑⠄⢱⡸⣧⣴⣶⡟⢠⡟⠁⠀⠀⠀⠀⠀⠀⠈⢷⡀⢿⣦⣾⡼⢳⡏⡠⠁⡀⠠⠠⢄⠈⠢⠈⣆⢿⣤⢶⣾⠃⣼⠃\n⠀⡸⢷⠈⠉⠉⢸⡟⣆⠀⢂⠌⣁⠒⡈⠢⢈⣾⣧⠈⠉⠉⢴⢏⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠿⡆⠉⠉⠁⣾⣳⠀⡀⠆⡑⠄⢂⠙⠄⣱⣿⠀⠉⠉⠡⡾⡅⠀\n⠐⡇⠀⠀⠀⠀⠘⣿⡞⣦⣅⣂⣄⣂⣰⣵⣻⣽⠏⠀⠀⠀⠀⢰⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⢺⠀⠀⠀⠀⠀⢻⣷⢳⣴⣈⣐⣈⣄⣮⣞⣯⡿⠂⠀⠀⠀⠀⣸⠀\n⠀⠘⢄⠀⠀⠀⠀⠈⠻⠶⣝⣮⣳⣭⣳⣧⠟⠋⠀⠀⠀⠀⣠⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⡀⠀⠀⠀⠀⠙⠷⢮⣽⣭⣻⣜⣷⠾⠛⠁⠀⠀⠀⠀⡼⠃⠀\n⠀⠀⠈⡗⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⡠⢾⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⢀⠴⣟⠁⠀⠀\n⠀⠀⠀⢸⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠨⡷⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⠇⠀⠀⠀\n⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡅⠀⠀⠀\n⠀⠀⠀⢽⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡇⠀⠀⠀";
|
|
397
|
+
return `<!DOCTYPE html>
|
|
398
|
+
<html lang="en">
|
|
399
|
+
<head>
|
|
400
|
+
<meta charset="UTF-8">
|
|
401
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
402
|
+
<title>${safeBotName} — Locked In</title>
|
|
403
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
404
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
405
|
+
<style>
|
|
406
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
407
|
+
body{
|
|
408
|
+
font-family:'JetBrains Mono',monospace;
|
|
409
|
+
background:${t.bg};color:${t.text};
|
|
410
|
+
min-height:100vh;display:flex;flex-direction:column;
|
|
411
|
+
align-items:center;justify-content:center;
|
|
412
|
+
overflow:hidden;
|
|
413
|
+
}
|
|
414
|
+
.container{text-align:center;position:relative;z-index:1;padding:12px 16px}
|
|
415
|
+
.cat{font-size:7px;line-height:1.2;color:${t.accent};white-space:pre;margin:0 auto 14px;animation:breathe 4s ease-in-out infinite;text-shadow:0 0 0 transparent}
|
|
416
|
+
.label{font-size:11px;text-transform:uppercase;letter-spacing:3px;color:${t.accent};font-weight:600;margin-bottom:8px}
|
|
417
|
+
.timer{font-size:48px;font-weight:700;letter-spacing:4px;color:${t.text};line-height:1;margin-bottom:8px;font-variant-numeric:tabular-nums}
|
|
418
|
+
.progress{width:200px;height:3px;background:${t.border};border-radius:2px;margin:0 auto 10px;overflow:hidden}
|
|
419
|
+
.progress-fill{height:100%;background:${t.accent};border-radius:2px;width:0%;transition:width 1s linear}
|
|
420
|
+
.sub{font-size:11px;color:${t.textDim};margin-bottom:12px}
|
|
421
|
+
.quote{font-size:10px;color:${t.textDim};font-style:italic;height:14px;transition:opacity .8s}
|
|
422
|
+
.session-info{font-size:10px;color:${t.textDim};display:flex;gap:16px;justify-content:center;margin-top:12px}
|
|
423
|
+
.session-info span{display:flex;align-items:center;gap:5px}
|
|
424
|
+
.dot{width:5px;height:5px;border-radius:50%;background:${t.accent};animation:pulse 2s ease-in-out infinite}
|
|
425
|
+
.complete .cat{color:${t.done};animation:none;text-shadow:0 0 12px ${t.done}40}
|
|
426
|
+
.complete .label{color:${t.done}}
|
|
427
|
+
.complete .timer{color:${t.done}}
|
|
428
|
+
.complete .dot{background:${t.done};animation:none}
|
|
429
|
+
.complete .progress-fill{background:${t.done}}
|
|
430
|
+
@keyframes breathe{0%,100%{opacity:.6;text-shadow:0 0 0 transparent}50%{opacity:1;text-shadow:0 0 15px ${t.accent}30}}
|
|
431
|
+
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
|
|
432
|
+
@keyframes flash{0%,100%{opacity:1}50%{opacity:.3}}
|
|
433
|
+
.flash .timer{animation:flash .5s ease-in-out 3}
|
|
434
|
+
.glow{position:fixed;width:200px;height:200px;border-radius:50%;background:${t.accent};opacity:.03;filter:blur(80px);pointer-events:none}
|
|
435
|
+
.glow-1{top:-60px;left:-60px}
|
|
436
|
+
.glow-2{bottom:-60px;right:-60px}
|
|
437
|
+
</style>
|
|
438
|
+
</head>
|
|
439
|
+
<body>
|
|
440
|
+
<div class="glow glow-1"></div>
|
|
441
|
+
<div class="glow glow-2"></div>
|
|
442
|
+
<div class="container" id="container">
|
|
443
|
+
<pre class="cat" id="cat"></pre>
|
|
444
|
+
<div class="label" id="label">Locked In</div>
|
|
445
|
+
<div class="timer" id="timer">--:--</div>
|
|
446
|
+
<div class="progress"><div class="progress-fill" id="progress"></div></div>
|
|
447
|
+
<div class="sub" id="sub">${safeDuration} min session</div>
|
|
448
|
+
<div class="quote" id="quote"></div>
|
|
449
|
+
<div class="session-info">
|
|
450
|
+
<span><span class="dot"></span> Focus active</span>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
<script>
|
|
454
|
+
var catArt = ${JSON.stringify(catArt)};
|
|
455
|
+
document.getElementById("cat").textContent = catArt;
|
|
456
|
+
|
|
457
|
+
var endsAt = "${safeEndsAt}";
|
|
458
|
+
var durationMs = ${safeDuration} * 60000;
|
|
459
|
+
var endTime = endsAt ? new Date(endsAt).getTime() : 0;
|
|
460
|
+
var startTime = endTime - durationMs;
|
|
461
|
+
var done = false;
|
|
462
|
+
var progressEl = document.getElementById("progress");
|
|
463
|
+
var quotes = [
|
|
464
|
+
"Deep work is the superpower of the 21st century.",
|
|
465
|
+
"Focus is not about saying yes. It\u2019s about saying no.",
|
|
466
|
+
"The successful warrior is the average person with laser focus.",
|
|
467
|
+
"What you stay focused on will grow.",
|
|
468
|
+
"Starve your distractions. Feed your focus.",
|
|
469
|
+
"Small daily improvements lead to stunning results.",
|
|
470
|
+
"You don\u2019t need more time. You need more focus.",
|
|
471
|
+
"Discipline is choosing what you want most over what you want now."
|
|
472
|
+
];
|
|
473
|
+
var qIdx = 0;
|
|
474
|
+
|
|
475
|
+
function pad(n) { return n < 10 ? "0" + n : "" + n; }
|
|
476
|
+
|
|
477
|
+
function tick() {
|
|
478
|
+
if (!endTime || done) return;
|
|
479
|
+
var now = Date.now();
|
|
480
|
+
var diff = endTime - now;
|
|
481
|
+
var elapsed = now - startTime;
|
|
482
|
+
var pct = Math.min(100, Math.max(0, (elapsed / durationMs) * 100));
|
|
483
|
+
progressEl.style.width = pct + "%";
|
|
484
|
+
|
|
485
|
+
if (diff <= 0) {
|
|
486
|
+
done = true;
|
|
487
|
+
document.getElementById("timer").textContent = "00:00";
|
|
488
|
+
document.getElementById("label").textContent = "Session Complete";
|
|
489
|
+
document.getElementById("sub").textContent = "Great work! Take a break.";
|
|
490
|
+
document.getElementById("container").classList.add("complete", "flash");
|
|
491
|
+
document.title = "${safeBotName} \u2014 Done!";
|
|
492
|
+
progressEl.style.width = "100%";
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
var h = Math.floor(diff / 3600000);
|
|
497
|
+
var m = Math.floor((diff % 3600000) / 60000);
|
|
498
|
+
var s = Math.floor((diff % 60000) / 1000);
|
|
499
|
+
document.getElementById("timer").textContent = h > 0 ? pad(h) + ":" + pad(m) + ":" + pad(s) : pad(m) + ":" + pad(s);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function rotateQuote() {
|
|
503
|
+
var el = document.getElementById("quote");
|
|
504
|
+
el.style.opacity = "0";
|
|
505
|
+
setTimeout(function() {
|
|
506
|
+
el.textContent = quotes[qIdx % quotes.length];
|
|
507
|
+
el.style.opacity = "1";
|
|
508
|
+
qIdx++;
|
|
509
|
+
}, 800);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
tick();
|
|
513
|
+
setInterval(tick, 1000);
|
|
514
|
+
rotateQuote();
|
|
515
|
+
setInterval(rotateQuote, 12000);
|
|
516
|
+
</script>
|
|
517
|
+
</body>
|
|
518
|
+
</html>`;
|
|
519
|
+
}
|
|
391
520
|
|
|
392
521
|
//#endregion
|
|
393
522
|
//#region src/core/dashboard-server.ts
|
|
@@ -455,6 +584,23 @@ function startDashboard(opts) {
|
|
|
455
584
|
html(res, generateDashboardHTML(current.theme, current.botName));
|
|
456
585
|
return;
|
|
457
586
|
}
|
|
587
|
+
if (method === "GET" && pathname === "/focus") {
|
|
588
|
+
const current = readConfig();
|
|
589
|
+
const ends = url.searchParams.get("ends") || "";
|
|
590
|
+
const dur = url.searchParams.get("duration") || "0";
|
|
591
|
+
html(res, generateFocusTimerHTML(current.theme, current.botName, ends, dur));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (method === "GET" && pathname === "/api/focus") {
|
|
595
|
+
try {
|
|
596
|
+
const sessionPath = path$1.join(CONFIG_DIR, "lockin-session.json");
|
|
597
|
+
const raw = fs$1.readFileSync(sessionPath, "utf-8");
|
|
598
|
+
json(res, JSON.parse(raw));
|
|
599
|
+
} catch {
|
|
600
|
+
json(res, { active: false });
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
458
604
|
if (method === "GET" && pathname === "/api/tasks") {
|
|
459
605
|
json(res, readConfig().tasks);
|
|
460
606
|
return;
|
|
@@ -521,9 +667,11 @@ function startDashboard(opts) {
|
|
|
521
667
|
server.listen(port, () => {
|
|
522
668
|
const url = `http://localhost:${port}`;
|
|
523
669
|
console.log(`\n Dashboard running at ${url}\n`);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
670
|
+
if (!opts.noOpen) {
|
|
671
|
+
const platform = os$1.platform();
|
|
672
|
+
if (platform === "darwin") import("node:child_process").then((cp) => cp.exec(`open ${url}`));
|
|
673
|
+
else if (platform === "linux") import("node:child_process").then((cp) => cp.exec(`xdg-open ${url}`));
|
|
674
|
+
}
|
|
527
675
|
});
|
|
528
676
|
return server;
|
|
529
677
|
}
|