cli-dev-tip 0.1.0__tar.gz
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.
- cli_dev_tip-0.1.0/.gitignore +58 -0
- cli_dev_tip-0.1.0/LICENSE +21 -0
- cli_dev_tip-0.1.0/PKG-INFO +223 -0
- cli_dev_tip-0.1.0/README.md +195 -0
- cli_dev_tip-0.1.0/dev_tip/__init__.py +0 -0
- cli_dev_tip-0.1.0/dev_tip/ai/__init__.py +55 -0
- cli_dev_tip-0.1.0/dev_tip/ai/cache.py +114 -0
- cli_dev_tip-0.1.0/dev_tip/ai/gemini.py +28 -0
- cli_dev_tip-0.1.0/dev_tip/ai/openrouter.py +37 -0
- cli_dev_tip-0.1.0/dev_tip/ai/prompt.py +60 -0
- cli_dev_tip-0.1.0/dev_tip/ai/provider.py +26 -0
- cli_dev_tip-0.1.0/dev_tip/cli.py +274 -0
- cli_dev_tip-0.1.0/dev_tip/config.py +98 -0
- cli_dev_tip-0.1.0/dev_tip/data/tips.yaml +2070 -0
- cli_dev_tip-0.1.0/dev_tip/history.py +46 -0
- cli_dev_tip-0.1.0/dev_tip/hook.py +192 -0
- cli_dev_tip-0.1.0/dev_tip/prefetch.py +98 -0
- cli_dev_tip-0.1.0/dev_tip/tips.py +45 -0
- cli_dev_tip-0.1.0/pyproject.toml +47 -0
- cli_dev_tip-0.1.0/tests/conftest.py +23 -0
- cli_dev_tip-0.1.0/tests/test_cache.py +64 -0
- cli_dev_tip-0.1.0/tests/test_cli.py +59 -0
- cli_dev_tip-0.1.0/tests/test_config.py +40 -0
- cli_dev_tip-0.1.0/tests/test_history.py +30 -0
- cli_dev_tip-0.1.0/tests/test_hook.py +68 -0
- cli_dev_tip-0.1.0/tests/test_tips.py +41 -0
- cli_dev_tip-0.1.0/uv.lock +300 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Claude Code
|
|
2
|
+
.claude
|
|
3
|
+
|
|
4
|
+
# VS Code
|
|
5
|
+
.vscode/
|
|
6
|
+
*.code-workspace
|
|
7
|
+
|
|
8
|
+
# ChatGPT / OpenAI
|
|
9
|
+
.chatgpt/
|
|
10
|
+
.openai/
|
|
11
|
+
|
|
12
|
+
# GitHub Copilot
|
|
13
|
+
.copilot/
|
|
14
|
+
|
|
15
|
+
# OS files
|
|
16
|
+
.DS_Store
|
|
17
|
+
Thumbs.db
|
|
18
|
+
|
|
19
|
+
# Environment & secrets
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
!.env.example
|
|
23
|
+
|
|
24
|
+
# Python
|
|
25
|
+
__pycache__/
|
|
26
|
+
*.py[cod]
|
|
27
|
+
*$py.class
|
|
28
|
+
*.egg-info/
|
|
29
|
+
dist/
|
|
30
|
+
build/
|
|
31
|
+
*.egg
|
|
32
|
+
.venv/
|
|
33
|
+
venv/
|
|
34
|
+
.python-version
|
|
35
|
+
|
|
36
|
+
# Node
|
|
37
|
+
node_modules/
|
|
38
|
+
npm-debug.log*
|
|
39
|
+
yarn-debug.log*
|
|
40
|
+
yarn-error.log*
|
|
41
|
+
.pnpm-debug.log*
|
|
42
|
+
|
|
43
|
+
# IDE / Editor
|
|
44
|
+
*.swp
|
|
45
|
+
*.swo
|
|
46
|
+
*~
|
|
47
|
+
.idea/
|
|
48
|
+
*.iml
|
|
49
|
+
|
|
50
|
+
# Logs
|
|
51
|
+
*.log
|
|
52
|
+
|
|
53
|
+
# Coverage & testing
|
|
54
|
+
.coverage
|
|
55
|
+
htmlcov/
|
|
56
|
+
.pytest_cache/
|
|
57
|
+
.mypy_cache/
|
|
58
|
+
.ruff_cache/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 My-CD-ROM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cli-dev-tip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bite-sized developer tips in your terminal
|
|
5
|
+
Project-URL: Homepage, https://github.com/My-CD-ROM/cli-dev-tip
|
|
6
|
+
Project-URL: Repository, https://github.com/My-CD-ROM/cli-dev-tip
|
|
7
|
+
Project-URL: Issues, https://github.com/My-CD-ROM/cli-dev-tip/issues
|
|
8
|
+
Author: My-CD-ROM
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,developer,learning,terminal,tips
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: rich
|
|
26
|
+
Requires-Dist: typer
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# dev-tip
|
|
30
|
+
|
|
31
|
+
A terminal tool that helps developers learn continuously by showing bite-sized technical tips — right in the terminal, between commands.
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
- Shows tips periodically during your shell session (every N commands or M minutes)
|
|
36
|
+
- First tip appears immediately when you open a terminal
|
|
37
|
+
- Covers general IT topics by default — Python, Git, Docker, Linux, Kubernetes, and more
|
|
38
|
+
- Filters by topic or difficulty level
|
|
39
|
+
- Remembers what you've seen so you don't get repeats
|
|
40
|
+
- Optional AI-powered tip generation via Gemini or OpenRouter (free, no extra packages needed)
|
|
41
|
+
- Zero prompt latency — all periodic logic runs as pure shell code, Python only invoked when showing a tip
|
|
42
|
+
- Pause/resume tips without removing the hook
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv tool install cli-dev-tip
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This makes `dev-tip` available globally from any terminal. No extra packages needed.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
The recommended setup — AI-generated tips covering general IT topics, appearing every 15 commands or 30 minutes:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
dev-tip enable --provider gemini --key YOUR_GEMINI_API_KEY
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Get a free Gemini API key at https://aistudio.google.com or a free OpenRouter key at https://openrouter.ai/keys.
|
|
61
|
+
|
|
62
|
+
More examples:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Static tips only (125 built in, works offline)
|
|
66
|
+
dev-tip enable
|
|
67
|
+
|
|
68
|
+
# AI tips, focused on Python, advanced level
|
|
69
|
+
dev-tip enable --provider gemini --key YOUR_KEY --topic python --level advanced
|
|
70
|
+
|
|
71
|
+
# Show tips more frequently — every 5 commands or every 10 minutes
|
|
72
|
+
dev-tip enable --provider gemini --key YOUR_KEY --every-commands 5 --every-minutes 10
|
|
73
|
+
|
|
74
|
+
# Quiet mode — body only, no header/emoji
|
|
75
|
+
dev-tip enable --quiet
|
|
76
|
+
|
|
77
|
+
# Disable
|
|
78
|
+
dev-tip disable
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `enable` options
|
|
82
|
+
|
|
83
|
+
| Option | Short | Description | Default |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `--provider` | `-p` | AI provider (gemini, openrouter) | None (static tips) |
|
|
86
|
+
| `--key` | `-k` | API key for the AI provider | Config or env var |
|
|
87
|
+
| `--topic` | `-t` | Filter tips by topic | General IT topics |
|
|
88
|
+
| `--level` | `-l` | Filter tips by difficulty | All levels |
|
|
89
|
+
| `--every-commands` | | Show a tip every N commands | 15 |
|
|
90
|
+
| `--every-minutes` | | Show a tip every N minutes | 30 |
|
|
91
|
+
| `--quiet` | `-q` | Show tip body only, no header | false |
|
|
92
|
+
|
|
93
|
+
Tips appear when either threshold is reached — whichever comes first. The first tip always shows immediately on shell startup.
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
One-off tips from the command line:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Show a random tip (general IT topics, all levels)
|
|
101
|
+
dev-tip
|
|
102
|
+
|
|
103
|
+
# Filter by topic
|
|
104
|
+
dev-tip --topic python
|
|
105
|
+
dev-tip -t git
|
|
106
|
+
|
|
107
|
+
# Filter by difficulty level
|
|
108
|
+
dev-tip --level beginner
|
|
109
|
+
dev-tip -l advanced
|
|
110
|
+
|
|
111
|
+
# Combine filters
|
|
112
|
+
dev-tip --topic docker --level intermediate
|
|
113
|
+
|
|
114
|
+
# Quiet mode — body only, no header
|
|
115
|
+
dev-tip --quiet
|
|
116
|
+
|
|
117
|
+
# Use AI provider for a one-off tip
|
|
118
|
+
dev-tip --provider gemini --key YOUR_KEY
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### One-off options
|
|
122
|
+
|
|
123
|
+
| Option | Short | Description | Default |
|
|
124
|
+
|---|---|---|---|
|
|
125
|
+
| `--topic` | `-t` | Filter tips by topic | General IT topics |
|
|
126
|
+
| `--level` | `-l` | Filter tips by difficulty | All levels |
|
|
127
|
+
| `--provider` | `-p` | AI provider (gemini, openrouter) | None |
|
|
128
|
+
| `--key` | `-k` | API key for the AI provider | Config or env var |
|
|
129
|
+
| `--quiet` | `-q` | Show tip body only, no header | false |
|
|
130
|
+
|
|
131
|
+
Unknown topics produce a warning when using static tips (AI can handle any topic). Unknown levels always warn.
|
|
132
|
+
|
|
133
|
+
### Available topics
|
|
134
|
+
|
|
135
|
+
| Topic | Count |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `python` | 15 |
|
|
138
|
+
| `git` | 15 |
|
|
139
|
+
| `docker` | 15 |
|
|
140
|
+
| `sql` | 15 |
|
|
141
|
+
| `linux` | 15 |
|
|
142
|
+
| `kubernetes` | 10 |
|
|
143
|
+
| `vim` | 10 |
|
|
144
|
+
| `javascript` | 10 |
|
|
145
|
+
| `terraform` | 10 |
|
|
146
|
+
| `rust` | 10 |
|
|
147
|
+
|
|
148
|
+
125 tips built in. With AI enabled, any topic works and tips are generated fresh.
|
|
149
|
+
|
|
150
|
+
### Difficulty levels
|
|
151
|
+
|
|
152
|
+
| Level | Description |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `beginner` | Fundamentals and common patterns |
|
|
155
|
+
| `intermediate` | Deeper concepts and useful tricks |
|
|
156
|
+
| `advanced` | Expert techniques and edge cases |
|
|
157
|
+
|
|
158
|
+
## Commands
|
|
159
|
+
|
|
160
|
+
### `dev-tip pause` / `dev-tip resume`
|
|
161
|
+
|
|
162
|
+
Temporarily stop tips without removing the hook:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
dev-tip pause # stops tips, hook stays installed
|
|
166
|
+
dev-tip resume # starts tips again
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This creates/removes a `.paused` file in `~/.dev-tip/`. The shell hook checks for it with a fast `[ -f ]` test — no Python invoked when paused.
|
|
170
|
+
|
|
171
|
+
### `dev-tip status`
|
|
172
|
+
|
|
173
|
+
Show current configuration and diagnostics:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
dev-tip status
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Displays hook state, pause status, config values, AI provider info, cache stats, and tip history count.
|
|
180
|
+
|
|
181
|
+
### `dev-tip clear-cache`
|
|
182
|
+
|
|
183
|
+
Clear cached AI tips to force fresh generation:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
dev-tip clear-cache
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## AI-powered tips
|
|
190
|
+
|
|
191
|
+
Generate fresh tips dynamically instead of using the built-in collection. Both providers are free.
|
|
192
|
+
|
|
193
|
+
| Provider | Default model | Free key |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `gemini` | `gemini-2.0-flash` | https://aistudio.google.com |
|
|
196
|
+
| `openrouter` | `google/gemini-2.0-flash-exp:free` | https://openrouter.ai/keys |
|
|
197
|
+
|
|
198
|
+
### How it works
|
|
199
|
+
|
|
200
|
+
- Generates 10 tips per API call and caches them locally (`~/.dev-tip/ai_cache.json`)
|
|
201
|
+
- Cache never expires — use `dev-tip clear-cache` to force refresh
|
|
202
|
+
- Cache is keyed by topic+level combination
|
|
203
|
+
- Falls back to static tips silently on any error (bad key, network failure, rate limit)
|
|
204
|
+
|
|
205
|
+
## Configuration
|
|
206
|
+
|
|
207
|
+
Settings are stored in `~/.dev-tip/config.toml`:
|
|
208
|
+
|
|
209
|
+
```toml
|
|
210
|
+
# topic = "python"
|
|
211
|
+
# level = "beginner"
|
|
212
|
+
# ai_provider = "gemini"
|
|
213
|
+
# ai_model = "gemini-2.0-flash"
|
|
214
|
+
# every_commands = 15
|
|
215
|
+
# every_minutes = 30
|
|
216
|
+
# quiet = false
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Values passed via `dev-tip enable` flags are saved here automatically. Comments in the config file are preserved when values are updated.
|
|
220
|
+
|
|
221
|
+
## Status
|
|
222
|
+
|
|
223
|
+
Early development — not yet published to PyPI.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# dev-tip
|
|
2
|
+
|
|
3
|
+
A terminal tool that helps developers learn continuously by showing bite-sized technical tips — right in the terminal, between commands.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Shows tips periodically during your shell session (every N commands or M minutes)
|
|
8
|
+
- First tip appears immediately when you open a terminal
|
|
9
|
+
- Covers general IT topics by default — Python, Git, Docker, Linux, Kubernetes, and more
|
|
10
|
+
- Filters by topic or difficulty level
|
|
11
|
+
- Remembers what you've seen so you don't get repeats
|
|
12
|
+
- Optional AI-powered tip generation via Gemini or OpenRouter (free, no extra packages needed)
|
|
13
|
+
- Zero prompt latency — all periodic logic runs as pure shell code, Python only invoked when showing a tip
|
|
14
|
+
- Pause/resume tips without removing the hook
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install cli-dev-tip
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This makes `dev-tip` available globally from any terminal. No extra packages needed.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
The recommended setup — AI-generated tips covering general IT topics, appearing every 15 commands or 30 minutes:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
dev-tip enable --provider gemini --key YOUR_GEMINI_API_KEY
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Get a free Gemini API key at https://aistudio.google.com or a free OpenRouter key at https://openrouter.ai/keys.
|
|
33
|
+
|
|
34
|
+
More examples:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Static tips only (125 built in, works offline)
|
|
38
|
+
dev-tip enable
|
|
39
|
+
|
|
40
|
+
# AI tips, focused on Python, advanced level
|
|
41
|
+
dev-tip enable --provider gemini --key YOUR_KEY --topic python --level advanced
|
|
42
|
+
|
|
43
|
+
# Show tips more frequently — every 5 commands or every 10 minutes
|
|
44
|
+
dev-tip enable --provider gemini --key YOUR_KEY --every-commands 5 --every-minutes 10
|
|
45
|
+
|
|
46
|
+
# Quiet mode — body only, no header/emoji
|
|
47
|
+
dev-tip enable --quiet
|
|
48
|
+
|
|
49
|
+
# Disable
|
|
50
|
+
dev-tip disable
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `enable` options
|
|
54
|
+
|
|
55
|
+
| Option | Short | Description | Default |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `--provider` | `-p` | AI provider (gemini, openrouter) | None (static tips) |
|
|
58
|
+
| `--key` | `-k` | API key for the AI provider | Config or env var |
|
|
59
|
+
| `--topic` | `-t` | Filter tips by topic | General IT topics |
|
|
60
|
+
| `--level` | `-l` | Filter tips by difficulty | All levels |
|
|
61
|
+
| `--every-commands` | | Show a tip every N commands | 15 |
|
|
62
|
+
| `--every-minutes` | | Show a tip every N minutes | 30 |
|
|
63
|
+
| `--quiet` | `-q` | Show tip body only, no header | false |
|
|
64
|
+
|
|
65
|
+
Tips appear when either threshold is reached — whichever comes first. The first tip always shows immediately on shell startup.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
One-off tips from the command line:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Show a random tip (general IT topics, all levels)
|
|
73
|
+
dev-tip
|
|
74
|
+
|
|
75
|
+
# Filter by topic
|
|
76
|
+
dev-tip --topic python
|
|
77
|
+
dev-tip -t git
|
|
78
|
+
|
|
79
|
+
# Filter by difficulty level
|
|
80
|
+
dev-tip --level beginner
|
|
81
|
+
dev-tip -l advanced
|
|
82
|
+
|
|
83
|
+
# Combine filters
|
|
84
|
+
dev-tip --topic docker --level intermediate
|
|
85
|
+
|
|
86
|
+
# Quiet mode — body only, no header
|
|
87
|
+
dev-tip --quiet
|
|
88
|
+
|
|
89
|
+
# Use AI provider for a one-off tip
|
|
90
|
+
dev-tip --provider gemini --key YOUR_KEY
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### One-off options
|
|
94
|
+
|
|
95
|
+
| Option | Short | Description | Default |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `--topic` | `-t` | Filter tips by topic | General IT topics |
|
|
98
|
+
| `--level` | `-l` | Filter tips by difficulty | All levels |
|
|
99
|
+
| `--provider` | `-p` | AI provider (gemini, openrouter) | None |
|
|
100
|
+
| `--key` | `-k` | API key for the AI provider | Config or env var |
|
|
101
|
+
| `--quiet` | `-q` | Show tip body only, no header | false |
|
|
102
|
+
|
|
103
|
+
Unknown topics produce a warning when using static tips (AI can handle any topic). Unknown levels always warn.
|
|
104
|
+
|
|
105
|
+
### Available topics
|
|
106
|
+
|
|
107
|
+
| Topic | Count |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `python` | 15 |
|
|
110
|
+
| `git` | 15 |
|
|
111
|
+
| `docker` | 15 |
|
|
112
|
+
| `sql` | 15 |
|
|
113
|
+
| `linux` | 15 |
|
|
114
|
+
| `kubernetes` | 10 |
|
|
115
|
+
| `vim` | 10 |
|
|
116
|
+
| `javascript` | 10 |
|
|
117
|
+
| `terraform` | 10 |
|
|
118
|
+
| `rust` | 10 |
|
|
119
|
+
|
|
120
|
+
125 tips built in. With AI enabled, any topic works and tips are generated fresh.
|
|
121
|
+
|
|
122
|
+
### Difficulty levels
|
|
123
|
+
|
|
124
|
+
| Level | Description |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `beginner` | Fundamentals and common patterns |
|
|
127
|
+
| `intermediate` | Deeper concepts and useful tricks |
|
|
128
|
+
| `advanced` | Expert techniques and edge cases |
|
|
129
|
+
|
|
130
|
+
## Commands
|
|
131
|
+
|
|
132
|
+
### `dev-tip pause` / `dev-tip resume`
|
|
133
|
+
|
|
134
|
+
Temporarily stop tips without removing the hook:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
dev-tip pause # stops tips, hook stays installed
|
|
138
|
+
dev-tip resume # starts tips again
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This creates/removes a `.paused` file in `~/.dev-tip/`. The shell hook checks for it with a fast `[ -f ]` test — no Python invoked when paused.
|
|
142
|
+
|
|
143
|
+
### `dev-tip status`
|
|
144
|
+
|
|
145
|
+
Show current configuration and diagnostics:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
dev-tip status
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Displays hook state, pause status, config values, AI provider info, cache stats, and tip history count.
|
|
152
|
+
|
|
153
|
+
### `dev-tip clear-cache`
|
|
154
|
+
|
|
155
|
+
Clear cached AI tips to force fresh generation:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
dev-tip clear-cache
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## AI-powered tips
|
|
162
|
+
|
|
163
|
+
Generate fresh tips dynamically instead of using the built-in collection. Both providers are free.
|
|
164
|
+
|
|
165
|
+
| Provider | Default model | Free key |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `gemini` | `gemini-2.0-flash` | https://aistudio.google.com |
|
|
168
|
+
| `openrouter` | `google/gemini-2.0-flash-exp:free` | https://openrouter.ai/keys |
|
|
169
|
+
|
|
170
|
+
### How it works
|
|
171
|
+
|
|
172
|
+
- Generates 10 tips per API call and caches them locally (`~/.dev-tip/ai_cache.json`)
|
|
173
|
+
- Cache never expires — use `dev-tip clear-cache` to force refresh
|
|
174
|
+
- Cache is keyed by topic+level combination
|
|
175
|
+
- Falls back to static tips silently on any error (bad key, network failure, rate limit)
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
Settings are stored in `~/.dev-tip/config.toml`:
|
|
180
|
+
|
|
181
|
+
```toml
|
|
182
|
+
# topic = "python"
|
|
183
|
+
# level = "beginner"
|
|
184
|
+
# ai_provider = "gemini"
|
|
185
|
+
# ai_model = "gemini-2.0-flash"
|
|
186
|
+
# every_commands = 15
|
|
187
|
+
# every_minutes = 30
|
|
188
|
+
# quiet = false
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Values passed via `dev-tip enable` flags are saved here automatically. Comments in the config file are preserved when values are updated.
|
|
192
|
+
|
|
193
|
+
## Status
|
|
194
|
+
|
|
195
|
+
Early development — not yet published to PyPI.
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
from dev_tip.ai.cache import is_on_cooldown, load_cache, mark_failure, save_cache
|
|
7
|
+
from dev_tip.ai.provider import create_provider
|
|
8
|
+
from dev_tip.history import get_unseen
|
|
9
|
+
|
|
10
|
+
_ENV_KEYS = {
|
|
11
|
+
"gemini": "GEMINI_API_KEY",
|
|
12
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
BATCH_SIZE = 10
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_ai_tip(
|
|
19
|
+
topic: str | None, level: str | None, config: dict
|
|
20
|
+
) -> tuple[dict | None, int]:
|
|
21
|
+
"""Return (tip, unseen_count) or (None, 0) on any failure."""
|
|
22
|
+
try:
|
|
23
|
+
provider_name = config.get("ai_provider")
|
|
24
|
+
if not provider_name:
|
|
25
|
+
return None, 0
|
|
26
|
+
|
|
27
|
+
api_key = config.get("ai_key")
|
|
28
|
+
if not api_key:
|
|
29
|
+
env_var = _ENV_KEYS.get(provider_name)
|
|
30
|
+
if env_var:
|
|
31
|
+
api_key = os.environ.get(env_var)
|
|
32
|
+
if not api_key:
|
|
33
|
+
return None, 0
|
|
34
|
+
|
|
35
|
+
# Try cache first
|
|
36
|
+
tips = load_cache(topic, level)
|
|
37
|
+
|
|
38
|
+
if not tips:
|
|
39
|
+
if is_on_cooldown():
|
|
40
|
+
return None, 0
|
|
41
|
+
try:
|
|
42
|
+
provider = create_provider(
|
|
43
|
+
provider_name, api_key, model=config.get("ai_model")
|
|
44
|
+
)
|
|
45
|
+
tips = provider.generate_tips(topic, level, BATCH_SIZE)
|
|
46
|
+
save_cache(tips, topic, level)
|
|
47
|
+
except Exception:
|
|
48
|
+
mark_failure()
|
|
49
|
+
return None, 0
|
|
50
|
+
|
|
51
|
+
unseen = get_unseen(tips)
|
|
52
|
+
return random.choice(unseen), len(unseen)
|
|
53
|
+
|
|
54
|
+
except Exception:
|
|
55
|
+
return None, 0
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
CACHE_DIR = Path.home() / ".dev-tip"
|
|
8
|
+
CACHE_FILE = CACHE_DIR / "ai_cache.json"
|
|
9
|
+
COOLDOWN_SECONDS = 5 * 60 # 5 min backoff after API failure
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _cache_key(topic: str | None, level: str | None) -> str:
|
|
13
|
+
"""Build a cache key like 'python:beginner' or 'None:None'."""
|
|
14
|
+
return f"{topic}:{level}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_all() -> dict:
|
|
18
|
+
"""Load full cache, auto-migrating v1 single-key format to v2."""
|
|
19
|
+
if not CACHE_FILE.exists():
|
|
20
|
+
return {"version": 2, "keys": {}}
|
|
21
|
+
|
|
22
|
+
data = json.loads(CACHE_FILE.read_text())
|
|
23
|
+
|
|
24
|
+
# v1 migration: old format had top-level topic/level/tips/generated_at
|
|
25
|
+
if "version" not in data and "tips" in data:
|
|
26
|
+
key = _cache_key(data.get("topic"), data.get("level"))
|
|
27
|
+
migrated = {
|
|
28
|
+
"version": 2,
|
|
29
|
+
"keys": {
|
|
30
|
+
key: {
|
|
31
|
+
"generated_at": data.get("generated_at", 0.0),
|
|
32
|
+
"tips": data.get("tips", []),
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
_save_all(migrated)
|
|
37
|
+
return migrated
|
|
38
|
+
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _save_all(data: dict) -> None:
|
|
43
|
+
"""Write full cache to disk."""
|
|
44
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
CACHE_FILE.write_text(json.dumps(data, indent=2))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_cache(topic: str | None, level: str | None) -> list[dict]:
|
|
49
|
+
"""Return all cached tips for a topic+level combo (never expires)."""
|
|
50
|
+
data = _load_all()
|
|
51
|
+
key = _cache_key(topic, level)
|
|
52
|
+
entry = data.get("keys", {}).get(key)
|
|
53
|
+
if not entry:
|
|
54
|
+
return []
|
|
55
|
+
return entry.get("tips", [])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_cache(tips: list[dict], topic: str | None, level: str | None) -> None:
|
|
59
|
+
"""Merge tips into the multi-key cache structure."""
|
|
60
|
+
data = _load_all()
|
|
61
|
+
key = _cache_key(topic, level)
|
|
62
|
+
existing = data.get("keys", {}).get(key, {}).get("tips", [])
|
|
63
|
+
|
|
64
|
+
# Deduplicate by tip id
|
|
65
|
+
seen_ids = {t["id"] for t in existing}
|
|
66
|
+
merged = existing + [t for t in tips if t["id"] not in seen_ids]
|
|
67
|
+
|
|
68
|
+
data.setdefault("keys", {})[key] = {
|
|
69
|
+
"generated_at": time.time(),
|
|
70
|
+
"tips": merged,
|
|
71
|
+
}
|
|
72
|
+
data["version"] = 2
|
|
73
|
+
_save_all(data)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cache_needs_refill(topic: str | None, level: str | None, unseen_count: int) -> bool:
|
|
77
|
+
"""Return True when unseen tips are running low and cache entry exists."""
|
|
78
|
+
if unseen_count > 3:
|
|
79
|
+
return False
|
|
80
|
+
data = _load_all()
|
|
81
|
+
key = _cache_key(topic, level)
|
|
82
|
+
return key in data.get("keys", {})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_on_cooldown() -> bool:
|
|
86
|
+
"""Return True if a recent API failure means we should skip retrying."""
|
|
87
|
+
data = _load_all()
|
|
88
|
+
failed_at = data.get("last_failure", 0)
|
|
89
|
+
return time.time() - failed_at < COOLDOWN_SECONDS
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mark_failure() -> None:
|
|
93
|
+
"""Record an API failure timestamp for cooldown backoff."""
|
|
94
|
+
data = _load_all()
|
|
95
|
+
data["last_failure"] = time.time()
|
|
96
|
+
_save_all(data)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def clear_cache() -> None:
|
|
100
|
+
"""Delete the AI cache file."""
|
|
101
|
+
if CACHE_FILE.exists():
|
|
102
|
+
CACHE_FILE.unlink()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_cache_stats() -> dict:
|
|
106
|
+
"""Return cache statistics for the status command."""
|
|
107
|
+
data = _load_all()
|
|
108
|
+
keys = data.get("keys", {})
|
|
109
|
+
total_tips = sum(len(entry.get("tips", [])) for entry in keys.values())
|
|
110
|
+
return {
|
|
111
|
+
"keys": len(keys),
|
|
112
|
+
"total_tips": total_tips,
|
|
113
|
+
"cooldown_active": is_on_cooldown(),
|
|
114
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
|
|
6
|
+
from dev_tip.ai.prompt import build_prompt, parse_response
|
|
7
|
+
from dev_tip.ai.provider import AIProvider
|
|
8
|
+
|
|
9
|
+
DEFAULT_MODEL = "gemini-2.0-flash"
|
|
10
|
+
_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GeminiProvider(AIProvider):
|
|
14
|
+
def __init__(self, api_key: str, model: str | None = None) -> None:
|
|
15
|
+
self._api_key = api_key
|
|
16
|
+
self._model = model or DEFAULT_MODEL
|
|
17
|
+
|
|
18
|
+
def generate_tips(self, topic: str | None, level: str | None, count: int) -> list[dict]:
|
|
19
|
+
prompt = build_prompt(topic, level, count)
|
|
20
|
+
url = _ENDPOINT.format(model=self._model, api_key=self._api_key)
|
|
21
|
+
body = json.dumps({
|
|
22
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
23
|
+
}).encode()
|
|
24
|
+
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
|
|
25
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
26
|
+
data = json.loads(resp.read())
|
|
27
|
+
text = data["candidates"][0]["content"]["parts"][0]["text"]
|
|
28
|
+
return parse_response(text)
|