@opslane/claude-code-game 0.1.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.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/routes/auth.d.ts +1 -0
- package/dist/routes/auth.js +123 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/levels.d.ts +44 -0
- package/dist/routes/levels.js +78 -0
- package/dist/routes/levels.js.map +1 -0
- package/dist/routes/sessions.d.ts +17 -0
- package/dist/routes/sessions.js +303 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +58 -0
- package/dist/server.js.map +1 -0
- package/dist/terminal.d.ts +6 -0
- package/dist/terminal.js +23 -0
- package/dist/terminal.js.map +1 -0
- package/dist/verification.d.ts +31 -0
- package/dist/verification.js +239 -0
- package/dist/verification.js.map +1 -0
- package/frontend/assets/index-CNVEnbfs.css +1 -0
- package/frontend/assets/index-D70xl9zu.js +27 -0
- package/frontend/index.html +14 -0
- package/frontend/vite.svg +1 -0
- package/keys/v1.pem +9 -0
- package/levels/01-context-is-everything/exercise/README.md +152 -0
- package/levels/01-context-is-everything/exercise/data/expenses.db +0 -0
- package/levels/01-context-is-everything/exercise/database.py +171 -0
- package/levels/01-context-is-everything/exercise/docs/FIRECRAWL_QUICKSTART.md +212 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_01.json +2306 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_02.json +2394 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_03.json +2251 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_04.json +1987 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_05.json +2229 -0
- package/levels/01-context-is-everything/exercise/main.py +97 -0
- package/levels/01-context-is-everything/exercise/models.py +141 -0
- package/levels/01-context-is-everything/exercise/pyproject.toml +52 -0
- package/levels/01-context-is-everything/exercise/reports.py +138 -0
- package/levels/01-context-is-everything/exercise/seed_data.py +91 -0
- package/levels/01-context-is-everything/exercise/tests/__init__.py +1 -0
- package/levels/01-context-is-everything/exercise/tests/conftest.py +69 -0
- package/levels/01-context-is-everything/exercise/tests/test_database.py +244 -0
- package/levels/01-context-is-everything/exercise/tests/test_models.py +240 -0
- package/levels/01-context-is-everything/exercise/tests/test_reports.py +190 -0
- package/levels/01-context-is-everything/exercise/utils.py +163 -0
- package/levels/01-context-is-everything/lesson.yaml +82 -0
- package/levels/02-claude-md/exercise/README.md +152 -0
- package/levels/02-claude-md/exercise/data/expenses.db +0 -0
- package/levels/02-claude-md/exercise/database.py +171 -0
- package/levels/02-claude-md/exercise/main.py +97 -0
- package/levels/02-claude-md/exercise/models.py +141 -0
- package/levels/02-claude-md/exercise/pyproject.toml +52 -0
- package/levels/02-claude-md/exercise/reports.py +138 -0
- package/levels/02-claude-md/exercise/seed_data.py +91 -0
- package/levels/02-claude-md/exercise/tests/__init__.py +1 -0
- package/levels/02-claude-md/exercise/tests/conftest.py +69 -0
- package/levels/02-claude-md/exercise/tests/test_database.py +244 -0
- package/levels/02-claude-md/exercise/tests/test_models.py +240 -0
- package/levels/02-claude-md/exercise/tests/test_reports.py +190 -0
- package/levels/02-claude-md/exercise/utils.py +163 -0
- package/levels/02-claude-md/lesson.yaml +60 -0
- package/levels/03-read-edit-verify/exercise/CLAUDE.md +15 -0
- package/levels/03-read-edit-verify/exercise/README.md +152 -0
- package/levels/03-read-edit-verify/exercise/data/expenses.db +0 -0
- package/levels/03-read-edit-verify/exercise/database.py +171 -0
- package/levels/03-read-edit-verify/exercise/main.py +97 -0
- package/levels/03-read-edit-verify/exercise/models.py +141 -0
- package/levels/03-read-edit-verify/exercise/pyproject.toml +52 -0
- package/levels/03-read-edit-verify/exercise/reports.py +138 -0
- package/levels/03-read-edit-verify/exercise/seed_data.py +91 -0
- package/levels/03-read-edit-verify/exercise/tests/__init__.py +1 -0
- package/levels/03-read-edit-verify/exercise/tests/conftest.py +69 -0
- package/levels/03-read-edit-verify/exercise/tests/test_database.py +244 -0
- package/levels/03-read-edit-verify/exercise/tests/test_models.py +240 -0
- package/levels/03-read-edit-verify/exercise/tests/test_reports.py +190 -0
- package/levels/03-read-edit-verify/exercise/utils.py +163 -0
- package/levels/03-read-edit-verify/lesson.yaml +60 -0
- package/levels/04-planning-mode/exercise/README.md +152 -0
- package/levels/04-planning-mode/exercise/data/expenses.db +0 -0
- package/levels/04-planning-mode/exercise/database.py +171 -0
- package/levels/04-planning-mode/exercise/main.py +97 -0
- package/levels/04-planning-mode/exercise/models.py +116 -0
- package/levels/04-planning-mode/exercise/pyproject.toml +52 -0
- package/levels/04-planning-mode/exercise/reports.py +138 -0
- package/levels/04-planning-mode/exercise/seed_data.py +91 -0
- package/levels/04-planning-mode/exercise/tests/__init__.py +1 -0
- package/levels/04-planning-mode/exercise/tests/conftest.py +69 -0
- package/levels/04-planning-mode/exercise/tests/test_database.py +244 -0
- package/levels/04-planning-mode/exercise/tests/test_expenses.db +0 -0
- package/levels/04-planning-mode/exercise/tests/test_models.py +240 -0
- package/levels/04-planning-mode/exercise/tests/test_reports.py +190 -0
- package/levels/04-planning-mode/exercise/utils.py +163 -0
- package/levels/04-planning-mode/lesson.yaml +53 -0
- package/levels/05-spec-driven/exercise/README.md +152 -0
- package/levels/05-spec-driven/exercise/data/expenses.db +0 -0
- package/levels/05-spec-driven/exercise/database.py +171 -0
- package/levels/05-spec-driven/exercise/main.py +97 -0
- package/levels/05-spec-driven/exercise/models.py +116 -0
- package/levels/05-spec-driven/exercise/pyproject.toml +52 -0
- package/levels/05-spec-driven/exercise/reports.py +138 -0
- package/levels/05-spec-driven/exercise/seed_data.py +91 -0
- package/levels/05-spec-driven/exercise/tests/__init__.py +1 -0
- package/levels/05-spec-driven/exercise/tests/conftest.py +69 -0
- package/levels/05-spec-driven/exercise/tests/test_database.py +244 -0
- package/levels/05-spec-driven/exercise/tests/test_expenses.db +0 -0
- package/levels/05-spec-driven/exercise/tests/test_models.py +240 -0
- package/levels/05-spec-driven/exercise/tests/test_reports.py +190 -0
- package/levels/05-spec-driven/exercise/utils.py +163 -0
- package/levels/05-spec-driven/lesson.yaml +53 -0
- package/levels/06-sub-agents/exercise/README.md +152 -0
- package/levels/06-sub-agents/exercise/data/expenses.db +0 -0
- package/levels/06-sub-agents/exercise/database.py +171 -0
- package/levels/06-sub-agents/exercise/main.py +97 -0
- package/levels/06-sub-agents/exercise/models.py +116 -0
- package/levels/06-sub-agents/exercise/pyproject.toml +52 -0
- package/levels/06-sub-agents/exercise/reports.py +63 -0
- package/levels/06-sub-agents/exercise/seed_data.py +91 -0
- package/levels/06-sub-agents/exercise/tests/__init__.py +1 -0
- package/levels/06-sub-agents/exercise/tests/conftest.py +69 -0
- package/levels/06-sub-agents/exercise/tests/test_database.py +244 -0
- package/levels/06-sub-agents/exercise/tests/test_models.py +240 -0
- package/levels/06-sub-agents/exercise/tests/test_reports.py +190 -0
- package/levels/06-sub-agents/exercise/utils.py +163 -0
- package/levels/06-sub-agents/lesson.yaml +49 -0
- package/levels/07-skills/exercise/README.md +152 -0
- package/levels/07-skills/exercise/data/expenses.db +0 -0
- package/levels/07-skills/exercise/database.py +171 -0
- package/levels/07-skills/exercise/main.py +97 -0
- package/levels/07-skills/exercise/models.py +116 -0
- package/levels/07-skills/exercise/pyproject.toml +52 -0
- package/levels/07-skills/exercise/reports.py +63 -0
- package/levels/07-skills/exercise/seed_data.py +91 -0
- package/levels/07-skills/exercise/tests/__init__.py +1 -0
- package/levels/07-skills/exercise/tests/conftest.py +69 -0
- package/levels/07-skills/exercise/tests/test_database.py +244 -0
- package/levels/07-skills/exercise/tests/test_models.py +240 -0
- package/levels/07-skills/exercise/tests/test_reports.py +190 -0
- package/levels/07-skills/exercise/utils.py +163 -0
- package/levels/07-skills/lesson.yaml +49 -0
- package/levels/08-mcp-servers/exercise/README.md +152 -0
- package/levels/08-mcp-servers/exercise/data/expenses.db +0 -0
- package/levels/08-mcp-servers/exercise/database.py +171 -0
- package/levels/08-mcp-servers/exercise/main.py +97 -0
- package/levels/08-mcp-servers/exercise/models.py +116 -0
- package/levels/08-mcp-servers/exercise/pyproject.toml +52 -0
- package/levels/08-mcp-servers/exercise/reports.py +63 -0
- package/levels/08-mcp-servers/exercise/seed_data.py +91 -0
- package/levels/08-mcp-servers/exercise/tests/__init__.py +1 -0
- package/levels/08-mcp-servers/exercise/tests/conftest.py +69 -0
- package/levels/08-mcp-servers/exercise/tests/test_database.py +244 -0
- package/levels/08-mcp-servers/exercise/tests/test_models.py +240 -0
- package/levels/08-mcp-servers/exercise/tests/test_reports.py +190 -0
- package/levels/08-mcp-servers/exercise/utils.py +163 -0
- package/levels/08-mcp-servers/lesson.yaml +59 -0
- package/levels/09-plugins/exercise/README.md +152 -0
- package/levels/09-plugins/exercise/data/expenses.db +0 -0
- package/levels/09-plugins/exercise/database.py +171 -0
- package/levels/09-plugins/exercise/main.py +97 -0
- package/levels/09-plugins/exercise/models.py +116 -0
- package/levels/09-plugins/exercise/pyproject.toml +52 -0
- package/levels/09-plugins/exercise/reports.py +63 -0
- package/levels/09-plugins/exercise/seed_data.py +91 -0
- package/levels/09-plugins/exercise/tests/__init__.py +1 -0
- package/levels/09-plugins/exercise/tests/conftest.py +69 -0
- package/levels/09-plugins/exercise/tests/test_database.py +244 -0
- package/levels/09-plugins/exercise/tests/test_models.py +240 -0
- package/levels/09-plugins/exercise/tests/test_reports.py +190 -0
- package/levels/09-plugins/exercise/utils.py +163 -0
- package/levels/09-plugins/lesson.yaml +51 -0
- package/levels/10-hooks/exercise/README.md +152 -0
- package/levels/10-hooks/exercise/data/expenses.db +0 -0
- package/levels/10-hooks/exercise/database.py +171 -0
- package/levels/10-hooks/exercise/main.py +97 -0
- package/levels/10-hooks/exercise/models.py +116 -0
- package/levels/10-hooks/exercise/pyproject.toml +52 -0
- package/levels/10-hooks/exercise/reports.py +63 -0
- package/levels/10-hooks/exercise/seed_data.py +91 -0
- package/levels/10-hooks/exercise/tests/__init__.py +1 -0
- package/levels/10-hooks/exercise/tests/conftest.py +69 -0
- package/levels/10-hooks/exercise/tests/test_database.py +244 -0
- package/levels/10-hooks/exercise/tests/test_models.py +240 -0
- package/levels/10-hooks/exercise/tests/test_reports.py +190 -0
- package/levels/10-hooks/exercise/utils.py +163 -0
- package/levels/10-hooks/lesson.yaml +58 -0
- package/levels/11-worktrees/exercise/README.md +152 -0
- package/levels/11-worktrees/exercise/data/expenses.db +0 -0
- package/levels/11-worktrees/exercise/database.py +171 -0
- package/levels/11-worktrees/exercise/main.py +97 -0
- package/levels/11-worktrees/exercise/models.py +116 -0
- package/levels/11-worktrees/exercise/pyproject.toml +52 -0
- package/levels/11-worktrees/exercise/reports.py +63 -0
- package/levels/11-worktrees/exercise/seed_data.py +91 -0
- package/levels/11-worktrees/exercise/tests/__init__.py +1 -0
- package/levels/11-worktrees/exercise/tests/conftest.py +69 -0
- package/levels/11-worktrees/exercise/tests/test_database.py +244 -0
- package/levels/11-worktrees/exercise/tests/test_models.py +240 -0
- package/levels/11-worktrees/exercise/tests/test_reports.py +190 -0
- package/levels/11-worktrees/exercise/utils.py +163 -0
- package/levels/11-worktrees/lesson.yaml +68 -0
- package/package.json +38 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Utility functions for the expense tracker."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import locale
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_currency(amount: float) -> str:
|
|
9
|
+
"""Format amount as currency.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
amount: The numeric amount
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Formatted string like '$1,234.56'
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> format_currency(1234.5)
|
|
19
|
+
'$1,234.50'
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
locale.setlocale(locale.LC_ALL, '')
|
|
23
|
+
return locale.currency(amount, grouping=True)
|
|
24
|
+
except (locale.Error, ValueError):
|
|
25
|
+
# Fallback if locale not available
|
|
26
|
+
return f"${amount:,.2f}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_date(date_str: Optional[str]) -> Optional[datetime]:
|
|
30
|
+
"""Parse a date string in various formats.
|
|
31
|
+
|
|
32
|
+
Supports:
|
|
33
|
+
- YYYY-MM-DD (2026-01-15)
|
|
34
|
+
- MM/DD/YYYY (01/15/2026)
|
|
35
|
+
- 'today'
|
|
36
|
+
- 'yesterday'
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
date_str: The date string to parse
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
datetime object or None if input is None
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If date format is not recognized
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> parse_date('2026-01-15')
|
|
49
|
+
datetime(2026, 1, 15, 0, 0, 0)
|
|
50
|
+
>>> parse_date('today')
|
|
51
|
+
datetime(...) # Today at midnight
|
|
52
|
+
"""
|
|
53
|
+
if not date_str:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
date_str = date_str.lower().strip()
|
|
57
|
+
|
|
58
|
+
# Handle relative dates
|
|
59
|
+
if date_str == 'today':
|
|
60
|
+
return datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
61
|
+
|
|
62
|
+
if date_str == 'yesterday':
|
|
63
|
+
return (datetime.now() - timedelta(days=1)).replace(
|
|
64
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Try YYYY-MM-DD (ISO format)
|
|
68
|
+
try:
|
|
69
|
+
return datetime.strptime(date_str, '%Y-%m-%d')
|
|
70
|
+
except ValueError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Try MM/DD/YYYY (US format)
|
|
74
|
+
try:
|
|
75
|
+
return datetime.strptime(date_str, '%m/%d/%Y')
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Try DD/MM/YYYY (European format)
|
|
80
|
+
try:
|
|
81
|
+
return datetime.strptime(date_str, '%d/%m/%Y')
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
raise ValueError(f"Could not parse date: {date_str}. Use YYYY-MM-DD format.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_category(category: str) -> str:
|
|
89
|
+
"""Normalize category name.
|
|
90
|
+
|
|
91
|
+
Capitalizes first letter, strips whitespace.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
category: Raw category input
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Normalized category name
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> validate_category(' food ')
|
|
101
|
+
'Food'
|
|
102
|
+
"""
|
|
103
|
+
return category.strip().capitalize()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def format_date(dt: datetime) -> str:
|
|
107
|
+
"""Format a datetime for display.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
dt: The datetime to format
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Formatted string like 'Jan 15, 2026'
|
|
114
|
+
"""
|
|
115
|
+
return dt.strftime('%b %d, %Y')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_month(month_str: str) -> str:
|
|
119
|
+
"""Format a YYYY-MM string for display.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
month_str: Month in YYYY-MM format
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Formatted string like 'January 2026'
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> format_month('2026-01')
|
|
129
|
+
'January 2026'
|
|
130
|
+
"""
|
|
131
|
+
dt = datetime.strptime(month_str, '%Y-%m')
|
|
132
|
+
return dt.strftime('%B %Y')
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Valid categories for validation/suggestions
|
|
136
|
+
VALID_CATEGORIES = [
|
|
137
|
+
"Food",
|
|
138
|
+
"Transport",
|
|
139
|
+
"Entertainment",
|
|
140
|
+
"Shopping",
|
|
141
|
+
"Bills",
|
|
142
|
+
"Health",
|
|
143
|
+
"Education",
|
|
144
|
+
"Travel",
|
|
145
|
+
"Other"
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def suggest_category(partial: str) -> list[str]:
|
|
150
|
+
"""Suggest categories matching a partial input.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
partial: Partial category name
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of matching categories
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> suggest_category('fo')
|
|
160
|
+
['Food']
|
|
161
|
+
"""
|
|
162
|
+
partial_lower = partial.lower()
|
|
163
|
+
return [cat for cat in VALID_CATEGORIES if cat.lower().startswith(partial_lower)]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
id: "context-is-everything"
|
|
2
|
+
number: 1
|
|
3
|
+
module: "Mental Model"
|
|
4
|
+
title: "Context is Everything"
|
|
5
|
+
|
|
6
|
+
video:
|
|
7
|
+
url: "https://youtu.be/WU7EncQmo-I"
|
|
8
|
+
duration_seconds: 300
|
|
9
|
+
|
|
10
|
+
exercise:
|
|
11
|
+
intro: |
|
|
12
|
+
See how outdated documentation misleads Claude.
|
|
13
|
+
You'll watch Claude trust docs over code — and write broken code.
|
|
14
|
+
objective: "Understand that context quality determines output quality"
|
|
15
|
+
|
|
16
|
+
intro: |
|
|
17
|
+
Context quality determines output quality. Let's prove it.
|
|
18
|
+
|
|
19
|
+
Experiment: Outdated Documentation
|
|
20
|
+
|
|
21
|
+
There's a FIRECRAWL_QUICKSTART.md in docs/ that describes the v1 API.
|
|
22
|
+
But Firecrawl released v2 with breaking changes (different method names).
|
|
23
|
+
|
|
24
|
+
1. Start Claude: `claude`
|
|
25
|
+
2. Point Claude to the docs:
|
|
26
|
+
"Read docs/FIRECRAWL_QUICKSTART.md to understand the Firecrawl API."
|
|
27
|
+
3. Ask Claude to write code:
|
|
28
|
+
"Write a function that scrapes a URL and returns the markdown content,
|
|
29
|
+
using the Firecrawl SDK as documented."
|
|
30
|
+
4. Watch what happens:
|
|
31
|
+
→ Claude will likely use the documented v1 methods (scrape_url,
|
|
32
|
+
FirecrawlApp, maxDepth) which are OUTDATED.
|
|
33
|
+
→ If you tried to run this code with the current SDK, it would fail.
|
|
34
|
+
5. Now ask Claude to verify:
|
|
35
|
+
"Check if these method names are current in the Firecrawl v2 SDK."
|
|
36
|
+
→ Claude should realize the docs describe v1, not v2.
|
|
37
|
+
|
|
38
|
+
Key insight: Claude trusts whatever context you give it.
|
|
39
|
+
Outdated docs, stale examples, or old tutorials = broken output.
|
|
40
|
+
|
|
41
|
+
This happens constantly with real APIs:
|
|
42
|
+
- Docs lag behind code
|
|
43
|
+
- Tutorials use deprecated methods
|
|
44
|
+
- Stack Overflow answers are from old versions
|
|
45
|
+
- Even official quickstart guides can be outdated
|
|
46
|
+
|
|
47
|
+
The fix? When working with external APIs, ask Claude to verify
|
|
48
|
+
against the latest documentation or the actual SDK source.
|
|
49
|
+
|
|
50
|
+
verification:
|
|
51
|
+
- type: min_user_messages
|
|
52
|
+
min_count: 2
|
|
53
|
+
- type: message_exists
|
|
54
|
+
|
|
55
|
+
success: |
|
|
56
|
+
You've seen how context quality affects Claude's output!
|
|
57
|
+
|
|
58
|
+
What happened:
|
|
59
|
+
- Claude trusted the API docs you pointed it to
|
|
60
|
+
- It wrote code using the documented (but outdated) v1 API
|
|
61
|
+
- The v2 SDK uses different method names:
|
|
62
|
+
• FirecrawlApp → Firecrawl
|
|
63
|
+
• scrape_url() → scrape()
|
|
64
|
+
• crawl_url() → crawl()
|
|
65
|
+
• maxDepth → maxDiscoveryDepth
|
|
66
|
+
|
|
67
|
+
This is the #1 source of bugs with AI coding assistants:
|
|
68
|
+
- Outdated documentation in your codebase
|
|
69
|
+
- Stale examples copied into context
|
|
70
|
+
- Training data from old library versions
|
|
71
|
+
- Conflicting information from multiple sources
|
|
72
|
+
|
|
73
|
+
The fix? Give Claude access to current sources:
|
|
74
|
+
- Official docs (Claude can fetch URLs)
|
|
75
|
+
- The actual SDK source code
|
|
76
|
+
- Your CLAUDE.md with project-specific guidance
|
|
77
|
+
|
|
78
|
+
In Lesson 2, you'll create CLAUDE.md — your source of truth.
|
|
79
|
+
|
|
80
|
+
limits:
|
|
81
|
+
max_duration_minutes: 15
|
|
82
|
+
max_claude_messages: 30
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Expense Tracker
|
|
2
|
+
|
|
3
|
+
A simple CLI expense tracker built with Python and SQLite.
|
|
4
|
+
|
|
5
|
+
This is the project used throughout the Claude Code course. You'll learn to use
|
|
6
|
+
Claude Code to understand, debug, and extend this codebase.
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# 1. Seed the database with sample data
|
|
12
|
+
python seed_data.py
|
|
13
|
+
|
|
14
|
+
# 2. Try some commands
|
|
15
|
+
python main.py list
|
|
16
|
+
python main.py summary
|
|
17
|
+
python main.py list --category Food
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### Add an expense
|
|
23
|
+
```bash
|
|
24
|
+
python main.py add <amount> <category> <description> [--date YYYY-MM-DD]
|
|
25
|
+
|
|
26
|
+
# Examples:
|
|
27
|
+
python main.py add 45.50 Food "Grocery shopping"
|
|
28
|
+
python main.py add 25.00 Transport "Uber" --date 2026-01-15
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### List expenses
|
|
32
|
+
```bash
|
|
33
|
+
python main.py list [--category CATEGORY] [--month YYYY-MM] [--limit N]
|
|
34
|
+
|
|
35
|
+
# Examples:
|
|
36
|
+
python main.py list
|
|
37
|
+
python main.py list --category Food
|
|
38
|
+
python main.py list --month 2026-01
|
|
39
|
+
python main.py list --limit 5
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Show expense details
|
|
43
|
+
```bash
|
|
44
|
+
python main.py show <expense_id>
|
|
45
|
+
|
|
46
|
+
# Example:
|
|
47
|
+
python main.py show abc123
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Delete an expense
|
|
51
|
+
```bash
|
|
52
|
+
python main.py delete <expense_id>
|
|
53
|
+
|
|
54
|
+
# Example:
|
|
55
|
+
python main.py delete abc123
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### View spending summary
|
|
59
|
+
```bash
|
|
60
|
+
python main.py summary
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Running Tests
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Run all tests
|
|
67
|
+
pytest
|
|
68
|
+
|
|
69
|
+
# Run with verbose output
|
|
70
|
+
pytest -v
|
|
71
|
+
|
|
72
|
+
# Run specific test file
|
|
73
|
+
pytest tests/test_models.py
|
|
74
|
+
|
|
75
|
+
# Run tests matching a pattern
|
|
76
|
+
pytest -k "category"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Project Structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
expense_tracker/
|
|
83
|
+
├── main.py # CLI entry point
|
|
84
|
+
├── models.py # Expense data model and core functions
|
|
85
|
+
├── database.py # SQLite database operations
|
|
86
|
+
├── utils.py # Helper functions (formatting, parsing)
|
|
87
|
+
├── reports.py # Report generation (Lesson 4)
|
|
88
|
+
├── seed_data.py # Script to populate sample data
|
|
89
|
+
├── tests/
|
|
90
|
+
│ ├── conftest.py # Pytest fixtures
|
|
91
|
+
│ ├── test_models.py # Model tests
|
|
92
|
+
│ ├── test_database.py # Database tests
|
|
93
|
+
│ └── test_reports.py # Report tests (Lesson 4)
|
|
94
|
+
├── data/
|
|
95
|
+
│ └── expenses.db # SQLite database (created on first run)
|
|
96
|
+
└── README.md
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Course Lessons
|
|
100
|
+
|
|
101
|
+
This codebase is used in the following lessons:
|
|
102
|
+
|
|
103
|
+
| Lesson | Focus | What You'll Do |
|
|
104
|
+
|--------|-------|----------------|
|
|
105
|
+
| 1 | Context | Explore the codebase, see context limits |
|
|
106
|
+
| 2 | CLAUDE.md | Create project memory |
|
|
107
|
+
| 3 | Bug Fix | Fix the negative amount validation bug |
|
|
108
|
+
| 4 | Debugging | Fix the case-sensitive category bug |
|
|
109
|
+
| 5 | Specs | Add recurring expenses feature |
|
|
110
|
+
| 6 | Planning | Implement the reports module |
|
|
111
|
+
| 7+ | Building Blocks | Create commands, skills, workflows |
|
|
112
|
+
|
|
113
|
+
## Known Issues (For Learning)
|
|
114
|
+
|
|
115
|
+
This codebase has intentional bugs for learning purposes:
|
|
116
|
+
|
|
117
|
+
1. **Negative amounts accepted** (Lesson 3)
|
|
118
|
+
- `add_expense()` doesn't validate that amount > 0
|
|
119
|
+
- Tests `test_add_negative_amount_should_fail` and `test_add_zero_amount_should_fail` fail
|
|
120
|
+
|
|
121
|
+
2. **Case-sensitive category filtering** (a later lesson)
|
|
122
|
+
- `list_expenses(category="Food")` won't find expenses with category "food" or "FOOD"
|
|
123
|
+
- Test `test_list_by_category_case_insensitive` fails
|
|
124
|
+
|
|
125
|
+
3. **Reports not implemented** (Lesson 4)
|
|
126
|
+
- `reports.py` has stub functions that raise `NotImplementedError`
|
|
127
|
+
- All tests in `test_reports.py` are skipped until implementation
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
### Requirements
|
|
132
|
+
- Python 3.10+
|
|
133
|
+
- pytest (for running tests)
|
|
134
|
+
|
|
135
|
+
### Setup
|
|
136
|
+
```bash
|
|
137
|
+
# Create virtual environment (optional but recommended)
|
|
138
|
+
python -m venv venv
|
|
139
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
140
|
+
|
|
141
|
+
# Install dev dependencies
|
|
142
|
+
pip install pytest ruff
|
|
143
|
+
|
|
144
|
+
# Initialize database
|
|
145
|
+
python seed_data.py
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Code Style
|
|
149
|
+
- Type hints on all function signatures
|
|
150
|
+
- Docstrings on public functions
|
|
151
|
+
- Black-compatible formatting
|
|
152
|
+
- No unused imports
|
|
Binary file
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""SQLite database operations for expense storage."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional, TYPE_CHECKING
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from models import Expense
|
|
12
|
+
|
|
13
|
+
# Database path - use environment variable for testing, otherwise default location
|
|
14
|
+
DB_PATH = Path(os.environ.get(
|
|
15
|
+
"EXPENSE_DB_PATH",
|
|
16
|
+
Path(__file__).parent / "data" / "expenses.db"
|
|
17
|
+
))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def get_connection():
|
|
22
|
+
"""Get a database connection with proper cleanup.
|
|
23
|
+
|
|
24
|
+
Yields:
|
|
25
|
+
sqlite3.Connection with Row factory enabled
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
with get_connection() as conn:
|
|
29
|
+
conn.execute("SELECT * FROM expenses")
|
|
30
|
+
"""
|
|
31
|
+
conn = sqlite3.connect(DB_PATH)
|
|
32
|
+
conn.row_factory = sqlite3.Row
|
|
33
|
+
try:
|
|
34
|
+
yield conn
|
|
35
|
+
conn.commit()
|
|
36
|
+
finally:
|
|
37
|
+
conn.close()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def init_db() -> None:
|
|
41
|
+
"""Initialize the database schema.
|
|
42
|
+
|
|
43
|
+
Creates the expenses table if it doesn't exist.
|
|
44
|
+
Also creates the data directory if needed.
|
|
45
|
+
"""
|
|
46
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
with get_connection() as conn:
|
|
49
|
+
conn.execute('''
|
|
50
|
+
CREATE TABLE IF NOT EXISTS expenses (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
amount REAL NOT NULL,
|
|
53
|
+
category TEXT NOT NULL,
|
|
54
|
+
description TEXT,
|
|
55
|
+
date TEXT NOT NULL,
|
|
56
|
+
recurring INTEGER DEFAULT 0,
|
|
57
|
+
recurring_frequency TEXT
|
|
58
|
+
)
|
|
59
|
+
''')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_expense(expense: "Expense") -> None:
|
|
63
|
+
"""Save an expense to the database.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
expense: The Expense object to save
|
|
67
|
+
"""
|
|
68
|
+
with get_connection() as conn:
|
|
69
|
+
conn.execute('''
|
|
70
|
+
INSERT INTO expenses (id, amount, category, description, date, recurring, recurring_frequency)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
72
|
+
''', (
|
|
73
|
+
expense.id,
|
|
74
|
+
expense.amount,
|
|
75
|
+
expense.category,
|
|
76
|
+
expense.description,
|
|
77
|
+
expense.date.isoformat(),
|
|
78
|
+
1 if expense.recurring else 0,
|
|
79
|
+
expense.recurring_frequency
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_expenses() -> list["Expense"]:
|
|
84
|
+
"""Load all expenses from the database.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of all Expense objects, ordered by date descending
|
|
88
|
+
"""
|
|
89
|
+
from models import Expense # Import here to avoid circular import
|
|
90
|
+
|
|
91
|
+
with get_connection() as conn:
|
|
92
|
+
rows = conn.execute('SELECT * FROM expenses ORDER BY date DESC').fetchall()
|
|
93
|
+
|
|
94
|
+
expenses = []
|
|
95
|
+
for row in rows:
|
|
96
|
+
expenses.append(Expense(
|
|
97
|
+
id=row['id'],
|
|
98
|
+
amount=row['amount'],
|
|
99
|
+
category=row['category'],
|
|
100
|
+
description=row['description'],
|
|
101
|
+
date=datetime.fromisoformat(row['date']),
|
|
102
|
+
recurring=bool(row['recurring']),
|
|
103
|
+
recurring_frequency=row['recurring_frequency']
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
return expenses
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_expense_by_id(expense_id: str) -> Optional["Expense"]:
|
|
110
|
+
"""Get a single expense by ID.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
expense_id: The unique expense ID
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The Expense if found, None otherwise
|
|
117
|
+
"""
|
|
118
|
+
from models import Expense # Import here to avoid circular import
|
|
119
|
+
|
|
120
|
+
with get_connection() as conn:
|
|
121
|
+
row = conn.execute(
|
|
122
|
+
'SELECT * FROM expenses WHERE id = ?',
|
|
123
|
+
(expense_id,)
|
|
124
|
+
).fetchone()
|
|
125
|
+
|
|
126
|
+
if not row:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
return Expense(
|
|
130
|
+
id=row['id'],
|
|
131
|
+
amount=row['amount'],
|
|
132
|
+
category=row['category'],
|
|
133
|
+
description=row['description'],
|
|
134
|
+
date=datetime.fromisoformat(row['date']),
|
|
135
|
+
recurring=bool(row['recurring']),
|
|
136
|
+
recurring_frequency=row['recurring_frequency']
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def remove_expense(expense_id: str) -> bool:
|
|
141
|
+
"""Remove an expense from the database.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
expense_id: The unique expense ID
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if an expense was deleted, False if not found
|
|
148
|
+
"""
|
|
149
|
+
with get_connection() as conn:
|
|
150
|
+
cursor = conn.execute('DELETE FROM expenses WHERE id = ?', (expense_id,))
|
|
151
|
+
return cursor.rowcount > 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def clear_all_expenses() -> None:
|
|
155
|
+
"""Clear all expenses from the database.
|
|
156
|
+
|
|
157
|
+
WARNING: This deletes all data! Use only for testing.
|
|
158
|
+
"""
|
|
159
|
+
with get_connection() as conn:
|
|
160
|
+
conn.execute('DELETE FROM expenses')
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_expense_count() -> int:
|
|
164
|
+
"""Get the total number of expenses.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Count of expenses in database
|
|
168
|
+
"""
|
|
169
|
+
with get_connection() as conn:
|
|
170
|
+
row = conn.execute('SELECT COUNT(*) as count FROM expenses').fetchone()
|
|
171
|
+
return row['count']
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Expense Tracker CLI - Track your daily expenses."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from models import add_expense, list_expenses, delete_expense, get_expense
|
|
7
|
+
from utils import format_currency, parse_date
|
|
8
|
+
from database import init_db
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description="Track your expenses")
|
|
13
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
14
|
+
|
|
15
|
+
# Add expense
|
|
16
|
+
add_parser = subparsers.add_parser("add", help="Add a new expense")
|
|
17
|
+
add_parser.add_argument("amount", type=float, help="Amount spent")
|
|
18
|
+
add_parser.add_argument("category", help="Category (Food, Transport, etc.)")
|
|
19
|
+
add_parser.add_argument("description", help="What was the expense for?")
|
|
20
|
+
add_parser.add_argument("--date", help="Date (YYYY-MM-DD), defaults to today")
|
|
21
|
+
|
|
22
|
+
# List expenses
|
|
23
|
+
list_parser = subparsers.add_parser("list", help="List expenses")
|
|
24
|
+
list_parser.add_argument("--category", help="Filter by category")
|
|
25
|
+
list_parser.add_argument("--month", help="Filter by month (YYYY-MM)")
|
|
26
|
+
list_parser.add_argument("--limit", type=int, default=10, help="Max results")
|
|
27
|
+
|
|
28
|
+
# Delete expense
|
|
29
|
+
delete_parser = subparsers.add_parser("delete", help="Delete an expense")
|
|
30
|
+
delete_parser.add_argument("id", help="Expense ID to delete")
|
|
31
|
+
|
|
32
|
+
# Show single expense
|
|
33
|
+
show_parser = subparsers.add_parser("show", help="Show expense details")
|
|
34
|
+
show_parser.add_argument("id", help="Expense ID")
|
|
35
|
+
|
|
36
|
+
# Summary
|
|
37
|
+
subparsers.add_parser("summary", help="Show spending summary")
|
|
38
|
+
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
# Initialize database
|
|
42
|
+
init_db()
|
|
43
|
+
|
|
44
|
+
if args.command == "add":
|
|
45
|
+
date = parse_date(args.date) if args.date else datetime.now()
|
|
46
|
+
expense = add_expense(args.amount, args.category, args.description, date)
|
|
47
|
+
print(f"Added expense: {expense.id} - {format_currency(expense.amount)}")
|
|
48
|
+
|
|
49
|
+
elif args.command == "list":
|
|
50
|
+
expenses = list_expenses(
|
|
51
|
+
category=args.category,
|
|
52
|
+
month=args.month,
|
|
53
|
+
limit=args.limit
|
|
54
|
+
)
|
|
55
|
+
if not expenses:
|
|
56
|
+
print("No expenses found.")
|
|
57
|
+
for exp in expenses:
|
|
58
|
+
print(f"{exp.id} | {exp.date.strftime('%Y-%m-%d')} | "
|
|
59
|
+
f"{exp.category:12} | {format_currency(exp.amount):>10} | {exp.description}")
|
|
60
|
+
|
|
61
|
+
elif args.command == "delete":
|
|
62
|
+
if delete_expense(args.id):
|
|
63
|
+
print(f"Deleted expense {args.id}")
|
|
64
|
+
else:
|
|
65
|
+
print(f"Expense {args.id} not found")
|
|
66
|
+
|
|
67
|
+
elif args.command == "show":
|
|
68
|
+
expense = get_expense(args.id)
|
|
69
|
+
if expense:
|
|
70
|
+
print(f"ID: {expense.id}")
|
|
71
|
+
print(f"Amount: {format_currency(expense.amount)}")
|
|
72
|
+
print(f"Category: {expense.category}")
|
|
73
|
+
print(f"Description: {expense.description}")
|
|
74
|
+
print(f"Date: {expense.date.strftime('%Y-%m-%d %H:%M')}")
|
|
75
|
+
else:
|
|
76
|
+
print(f"Expense {args.id} not found")
|
|
77
|
+
|
|
78
|
+
elif args.command == "summary":
|
|
79
|
+
expenses = list_expenses()
|
|
80
|
+
total = sum(e.amount for e in expenses)
|
|
81
|
+
print(f"Total expenses: {format_currency(total)}")
|
|
82
|
+
|
|
83
|
+
# Group by category
|
|
84
|
+
categories: dict[str, float] = {}
|
|
85
|
+
for exp in expenses:
|
|
86
|
+
categories[exp.category] = categories.get(exp.category, 0) + exp.amount
|
|
87
|
+
|
|
88
|
+
print("\nBy category:")
|
|
89
|
+
for cat, amount in sorted(categories.items(), key=lambda x: x[1], reverse=True):
|
|
90
|
+
print(f" {cat:15} {format_currency(amount):>10}")
|
|
91
|
+
|
|
92
|
+
else:
|
|
93
|
+
parser.print_help()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|