crowdtime-cli 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.
- crowdtime_cli-0.1.0/.gitignore +58 -0
- crowdtime_cli-0.1.0/LICENSE +77 -0
- crowdtime_cli-0.1.0/PKG-INFO +140 -0
- crowdtime_cli-0.1.0/README.md +106 -0
- crowdtime_cli-0.1.0/pyproject.toml +49 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/__init__.py +3 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/auth.py +69 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/client.py +177 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/__init__.py +1 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/ai_cmd.py +211 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/auth_cmd.py +160 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/clients_cmd.py +150 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/config_cmd.py +91 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/favorites_cmd.py +128 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/log_cmd.py +298 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/org_cmd.py +134 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/projects_cmd.py +175 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/report_cmd.py +242 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/skill_cmd.py +266 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/tasks_cmd.py +101 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/commands/timer_cmd.py +207 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/config.py +125 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/formatters.py +395 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/main.py +334 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/models.py +146 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/oauth.py +107 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/resolvers.py +80 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/skills/crowdtime/SKILL.md +193 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/skills/crowdtime/references/commands.md +659 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/skills/crowdtime/references/workflows.md +286 -0
- crowdtime_cli-0.1.0/src/crowdtime_cli/utils.py +166 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
ENV/
|
|
15
|
+
|
|
16
|
+
# Environment
|
|
17
|
+
.env
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
*~
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
|
|
30
|
+
# Django
|
|
31
|
+
*.log
|
|
32
|
+
db.sqlite3
|
|
33
|
+
staticfiles/
|
|
34
|
+
media/
|
|
35
|
+
logs/
|
|
36
|
+
|
|
37
|
+
# Celery
|
|
38
|
+
celerybeat-schedule
|
|
39
|
+
celerybeat.pid
|
|
40
|
+
|
|
41
|
+
# Coverage
|
|
42
|
+
.coverage
|
|
43
|
+
htmlcov/
|
|
44
|
+
.pytest_cache/
|
|
45
|
+
|
|
46
|
+
# Docker
|
|
47
|
+
docker-compose.override.yml
|
|
48
|
+
|
|
49
|
+
# Claude Code
|
|
50
|
+
.claude/
|
|
51
|
+
|
|
52
|
+
# Frontend
|
|
53
|
+
node_modules/
|
|
54
|
+
.next/
|
|
55
|
+
.env.local
|
|
56
|
+
.env*.local
|
|
57
|
+
RECOVERY.md
|
|
58
|
+
coolify_deployment.md
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
CrowdTime CLI - Proprietary Software License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 IT Crowd SRL. All rights reserved.
|
|
4
|
+
|
|
5
|
+
NOTICE: This software and associated documentation files (the "Software") are
|
|
6
|
+
the proprietary property of IT Crowd SRL ("Company"). The Software is protected
|
|
7
|
+
by copyright laws and international treaty provisions.
|
|
8
|
+
|
|
9
|
+
1. GRANT OF LICENSE
|
|
10
|
+
|
|
11
|
+
Subject to the terms of this license, the Company grants you a limited,
|
|
12
|
+
non-exclusive, non-transferable, revocable license to install and use the
|
|
13
|
+
Software solely for your internal business purposes in connection with
|
|
14
|
+
a valid CrowdTime subscription.
|
|
15
|
+
|
|
16
|
+
2. RESTRICTIONS
|
|
17
|
+
|
|
18
|
+
You may NOT:
|
|
19
|
+
a) Copy, modify, adapt, translate, or create derivative works of the Software;
|
|
20
|
+
b) Reverse engineer, decompile, disassemble, or otherwise attempt to derive
|
|
21
|
+
the source code of the Software;
|
|
22
|
+
c) Redistribute, sublicense, rent, lease, lend, sell, or otherwise transfer
|
|
23
|
+
the Software or any rights therein to any third party;
|
|
24
|
+
d) Remove, alter, or obscure any proprietary notices, labels, or marks on
|
|
25
|
+
the Software;
|
|
26
|
+
e) Use the Software to build a competing product or service;
|
|
27
|
+
f) Use the Software in any manner that violates applicable laws or
|
|
28
|
+
regulations.
|
|
29
|
+
|
|
30
|
+
3. OWNERSHIP
|
|
31
|
+
|
|
32
|
+
The Software is licensed, not sold. The Company retains all right, title,
|
|
33
|
+
and interest in and to the Software, including all intellectual property
|
|
34
|
+
rights therein. No rights are granted to you other than those expressly
|
|
35
|
+
set forth in this license.
|
|
36
|
+
|
|
37
|
+
4. SUBSCRIPTION REQUIREMENT
|
|
38
|
+
|
|
39
|
+
Use of the Software requires an active CrowdTime subscription. The Company
|
|
40
|
+
reserves the right to verify your subscription status and to disable access
|
|
41
|
+
to the Software if your subscription lapses or is terminated.
|
|
42
|
+
|
|
43
|
+
5. DATA COLLECTION
|
|
44
|
+
|
|
45
|
+
The Software communicates with CrowdTime servers to provide its
|
|
46
|
+
functionality. By using the Software, you acknowledge that data will be
|
|
47
|
+
transmitted to and stored on CrowdTime servers in accordance with the
|
|
48
|
+
CrowdTime Privacy Policy.
|
|
49
|
+
|
|
50
|
+
6. NO WARRANTY
|
|
51
|
+
|
|
52
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
53
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
54
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE COMPANY DOES NOT
|
|
55
|
+
WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR UNINTERRUPTED.
|
|
56
|
+
|
|
57
|
+
7. LIMITATION OF LIABILITY
|
|
58
|
+
|
|
59
|
+
IN NO EVENT SHALL THE COMPANY BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
|
60
|
+
SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR
|
|
61
|
+
REVENUE, WHETHER INCURRED DIRECTLY OR INDIRECTLY, OR ANY LOSS OF DATA, USE,
|
|
62
|
+
GOODWILL, OR OTHER INTANGIBLE LOSSES, ARISING OUT OF OR IN CONNECTION WITH
|
|
63
|
+
YOUR USE OF THE SOFTWARE.
|
|
64
|
+
|
|
65
|
+
8. TERMINATION
|
|
66
|
+
|
|
67
|
+
This license is effective until terminated. The Company may terminate this
|
|
68
|
+
license at any time if you fail to comply with any term herein. Upon
|
|
69
|
+
termination, you must cease all use of the Software and destroy all copies
|
|
70
|
+
in your possession.
|
|
71
|
+
|
|
72
|
+
9. GOVERNING LAW
|
|
73
|
+
|
|
74
|
+
This license shall be governed by and construed in accordance with the laws
|
|
75
|
+
of Argentina, without regard to its conflict of laws principles.
|
|
76
|
+
|
|
77
|
+
For licensing inquiries: dev@itcrowdarg.com
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crowdtime-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest
|
|
5
|
+
Project-URL: Homepage, https://crowdtime.lat
|
|
6
|
+
Project-URL: Documentation, https://crowdtime.lat/docs
|
|
7
|
+
Project-URL: Support, https://crowdtime.lat/support
|
|
8
|
+
Author-email: IT Crowd <dev@itcrowdarg.com>
|
|
9
|
+
License: Proprietary
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,cli,harvest-alternative,productivity,time-tracking
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: Other/Proprietary License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: humanize>=4.0
|
|
26
|
+
Requires-Dist: keyring>=25.0
|
|
27
|
+
Requires-Dist: platformdirs>=4.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0
|
|
29
|
+
Requires-Dist: python-dateutil>=2.9
|
|
30
|
+
Requires-Dist: rich>=13.0
|
|
31
|
+
Requires-Dist: tomlkit>=0.12.0
|
|
32
|
+
Requires-Dist: typer[all]>=0.12.0
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# CrowdTime CLI
|
|
36
|
+
|
|
37
|
+
**AI-powered time tracking from your terminal.** A modern, developer-friendly alternative to Harvest.
|
|
38
|
+
|
|
39
|
+
Track time, manage projects, generate reports, and get AI-powered insights — all without leaving your terminal.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Fast time tracking** — Start/stop timers or log entries directly with natural duration formats (`2h30m`, `1:45`, `0.5d`)
|
|
44
|
+
- **AI-powered** — Natural language time entry, smart suggestions based on your patterns, and automatic standup/slack summaries
|
|
45
|
+
- **Rich terminal UI** — Beautiful tables, dashboards, and reports powered by Rich
|
|
46
|
+
- **Multi-org support** — Switch between organizations seamlessly
|
|
47
|
+
- **Secure by default** — API tokens stored in your OS keychain, never in plain text
|
|
48
|
+
- **Flexible reporting** — Daily, weekly, monthly reports in table, JSON, CSV, or Markdown formats
|
|
49
|
+
- **Favorites & templates** — Save common entries for one-command reuse
|
|
50
|
+
- **Claude Code integration** — Ships as a Claude Code skill for AI-assisted time tracking
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install crowdtime-cli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires Python 3.11+
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Authenticate
|
|
64
|
+
ct config set server.url https://api.crowdtime.lat
|
|
65
|
+
ct auth login
|
|
66
|
+
|
|
67
|
+
# Switch to your organization
|
|
68
|
+
ct org switch my-org
|
|
69
|
+
|
|
70
|
+
# Start tracking
|
|
71
|
+
ct timer start "Building REST API" -p backend -t "Development"
|
|
72
|
+
ct timer stop
|
|
73
|
+
|
|
74
|
+
# Or log time directly
|
|
75
|
+
ct log -p backend -t "Code Review" 2h "Reviewed auth module PR"
|
|
76
|
+
|
|
77
|
+
# Check your day
|
|
78
|
+
ct status
|
|
79
|
+
|
|
80
|
+
# Weekly report
|
|
81
|
+
ct report --week
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Command Overview
|
|
85
|
+
|
|
86
|
+
| Command | Alias | Description |
|
|
87
|
+
|---------|-------|-------------|
|
|
88
|
+
| `ct status` | `ct s` | Dashboard: running timer, today's entries, weekly total |
|
|
89
|
+
| `ct timer start` | `ct ts` | Start a timer with project and task |
|
|
90
|
+
| `ct timer stop` | `ct tx` | Stop the running timer and save the entry |
|
|
91
|
+
| `ct log <duration> <desc>` | `ct l` | Log a time entry directly |
|
|
92
|
+
| `ct log list` | `ct ll` | List time entries (today, --week, --month) |
|
|
93
|
+
| `ct report` | `ct r` | Generate reports with grouping and filters |
|
|
94
|
+
| `ct projects list` | `ct p` | List projects |
|
|
95
|
+
| `ct ai suggest` | — | Get AI suggestions based on your patterns |
|
|
96
|
+
| `ct ai summarize` | — | AI-generated summaries (--for standup, --for slack) |
|
|
97
|
+
|
|
98
|
+
## Duration Formats
|
|
99
|
+
|
|
100
|
+
`2h` | `2h30m` | `2:30` | `150m` | `0.25d` (1 day = 8h) | `1.5` (hours)
|
|
101
|
+
|
|
102
|
+
## Date Formats
|
|
103
|
+
|
|
104
|
+
`today` | `yesterday` | `monday`..`sunday` | `last friday` | `2026-03-10` | `3/10`
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
ct config set server.url https://api.crowdtime.lat # API server
|
|
110
|
+
ct config set defaults.project my-project # Default project
|
|
111
|
+
ct config set defaults.daily_target 7h # Daily hour target
|
|
112
|
+
ct config list # Show all settings
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Config is stored at `~/.crowdtime/config.toml`.
|
|
116
|
+
|
|
117
|
+
## Authentication
|
|
118
|
+
|
|
119
|
+
CrowdTime supports multiple auth methods:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ct auth login # Browser-based Google OAuth (default)
|
|
123
|
+
ct auth login --token <token> # API token (for CI/scripts)
|
|
124
|
+
ct auth login --no-browser # Token prompt (headless environments)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
API tokens are stored securely in your OS keychain via [keyring](https://pypi.org/project/keyring/).
|
|
128
|
+
|
|
129
|
+
## Requirements
|
|
130
|
+
|
|
131
|
+
- Python 3.11+
|
|
132
|
+
- A CrowdTime account at [crowdtime.lat](https://crowdtime.lat)
|
|
133
|
+
|
|
134
|
+
## Support
|
|
135
|
+
|
|
136
|
+
For issues and feature requests, contact [dev@itcrowdarg.com](mailto:dev@itcrowdarg.com).
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
Proprietary. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# CrowdTime CLI
|
|
2
|
+
|
|
3
|
+
**AI-powered time tracking from your terminal.** A modern, developer-friendly alternative to Harvest.
|
|
4
|
+
|
|
5
|
+
Track time, manage projects, generate reports, and get AI-powered insights — all without leaving your terminal.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Fast time tracking** — Start/stop timers or log entries directly with natural duration formats (`2h30m`, `1:45`, `0.5d`)
|
|
10
|
+
- **AI-powered** — Natural language time entry, smart suggestions based on your patterns, and automatic standup/slack summaries
|
|
11
|
+
- **Rich terminal UI** — Beautiful tables, dashboards, and reports powered by Rich
|
|
12
|
+
- **Multi-org support** — Switch between organizations seamlessly
|
|
13
|
+
- **Secure by default** — API tokens stored in your OS keychain, never in plain text
|
|
14
|
+
- **Flexible reporting** — Daily, weekly, monthly reports in table, JSON, CSV, or Markdown formats
|
|
15
|
+
- **Favorites & templates** — Save common entries for one-command reuse
|
|
16
|
+
- **Claude Code integration** — Ships as a Claude Code skill for AI-assisted time tracking
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install crowdtime-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Python 3.11+
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Authenticate
|
|
30
|
+
ct config set server.url https://api.crowdtime.lat
|
|
31
|
+
ct auth login
|
|
32
|
+
|
|
33
|
+
# Switch to your organization
|
|
34
|
+
ct org switch my-org
|
|
35
|
+
|
|
36
|
+
# Start tracking
|
|
37
|
+
ct timer start "Building REST API" -p backend -t "Development"
|
|
38
|
+
ct timer stop
|
|
39
|
+
|
|
40
|
+
# Or log time directly
|
|
41
|
+
ct log -p backend -t "Code Review" 2h "Reviewed auth module PR"
|
|
42
|
+
|
|
43
|
+
# Check your day
|
|
44
|
+
ct status
|
|
45
|
+
|
|
46
|
+
# Weekly report
|
|
47
|
+
ct report --week
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Command Overview
|
|
51
|
+
|
|
52
|
+
| Command | Alias | Description |
|
|
53
|
+
|---------|-------|-------------|
|
|
54
|
+
| `ct status` | `ct s` | Dashboard: running timer, today's entries, weekly total |
|
|
55
|
+
| `ct timer start` | `ct ts` | Start a timer with project and task |
|
|
56
|
+
| `ct timer stop` | `ct tx` | Stop the running timer and save the entry |
|
|
57
|
+
| `ct log <duration> <desc>` | `ct l` | Log a time entry directly |
|
|
58
|
+
| `ct log list` | `ct ll` | List time entries (today, --week, --month) |
|
|
59
|
+
| `ct report` | `ct r` | Generate reports with grouping and filters |
|
|
60
|
+
| `ct projects list` | `ct p` | List projects |
|
|
61
|
+
| `ct ai suggest` | — | Get AI suggestions based on your patterns |
|
|
62
|
+
| `ct ai summarize` | — | AI-generated summaries (--for standup, --for slack) |
|
|
63
|
+
|
|
64
|
+
## Duration Formats
|
|
65
|
+
|
|
66
|
+
`2h` | `2h30m` | `2:30` | `150m` | `0.25d` (1 day = 8h) | `1.5` (hours)
|
|
67
|
+
|
|
68
|
+
## Date Formats
|
|
69
|
+
|
|
70
|
+
`today` | `yesterday` | `monday`..`sunday` | `last friday` | `2026-03-10` | `3/10`
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ct config set server.url https://api.crowdtime.lat # API server
|
|
76
|
+
ct config set defaults.project my-project # Default project
|
|
77
|
+
ct config set defaults.daily_target 7h # Daily hour target
|
|
78
|
+
ct config list # Show all settings
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Config is stored at `~/.crowdtime/config.toml`.
|
|
82
|
+
|
|
83
|
+
## Authentication
|
|
84
|
+
|
|
85
|
+
CrowdTime supports multiple auth methods:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ct auth login # Browser-based Google OAuth (default)
|
|
89
|
+
ct auth login --token <token> # API token (for CI/scripts)
|
|
90
|
+
ct auth login --no-browser # Token prompt (headless environments)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
API tokens are stored securely in your OS keychain via [keyring](https://pypi.org/project/keyring/).
|
|
94
|
+
|
|
95
|
+
## Requirements
|
|
96
|
+
|
|
97
|
+
- Python 3.11+
|
|
98
|
+
- A CrowdTime account at [crowdtime.lat](https://crowdtime.lat)
|
|
99
|
+
|
|
100
|
+
## Support
|
|
101
|
+
|
|
102
|
+
For issues and feature requests, contact [dev@itcrowdarg.com](mailto:dev@itcrowdarg.com).
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
Proprietary. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "crowdtime-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "Proprietary"}
|
|
7
|
+
authors = [{name = "IT Crowd", email = "dev@itcrowdarg.com"}]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
keywords = ["time-tracking", "cli", "productivity", "harvest-alternative", "ai"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: Other/Proprietary License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Office/Business",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"typer[all]>=0.12.0",
|
|
25
|
+
"rich>=13.0",
|
|
26
|
+
"httpx>=0.27.0",
|
|
27
|
+
"pydantic>=2.0",
|
|
28
|
+
"tomlkit>=0.12.0",
|
|
29
|
+
"keyring>=25.0",
|
|
30
|
+
"platformdirs>=4.0",
|
|
31
|
+
"python-dateutil>=2.9",
|
|
32
|
+
"humanize>=4.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://crowdtime.lat"
|
|
37
|
+
Documentation = "https://crowdtime.lat/docs"
|
|
38
|
+
Support = "https://crowdtime.lat/support"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
crowdtime = "crowdtime_cli.main:_original_main"
|
|
42
|
+
ct = "crowdtime_cli.main:_original_main"
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["hatchling"]
|
|
46
|
+
build-backend = "hatchling.build"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/crowdtime_cli"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Authentication and token management for CrowdTime CLI.
|
|
2
|
+
|
|
3
|
+
Stores API tokens in the system keyring, with file-based fallback.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .config import CONFIG_DIR
|
|
11
|
+
|
|
12
|
+
SERVICE_NAME = "crowdtime"
|
|
13
|
+
USERNAME = "api_token"
|
|
14
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def save_token(token: str) -> None:
|
|
18
|
+
"""Store the API token, preferring keyring with file fallback."""
|
|
19
|
+
try:
|
|
20
|
+
import keyring
|
|
21
|
+
|
|
22
|
+
keyring.set_password(SERVICE_NAME, USERNAME, token)
|
|
23
|
+
except Exception:
|
|
24
|
+
_save_token_file(token)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_token() -> str | None:
|
|
28
|
+
"""Retrieve the stored API token."""
|
|
29
|
+
try:
|
|
30
|
+
import keyring
|
|
31
|
+
|
|
32
|
+
token = keyring.get_password(SERVICE_NAME, USERNAME)
|
|
33
|
+
if token:
|
|
34
|
+
return token
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
return _get_token_file()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def clear_token() -> None:
|
|
41
|
+
"""Remove the stored API token."""
|
|
42
|
+
try:
|
|
43
|
+
import keyring
|
|
44
|
+
|
|
45
|
+
keyring.delete_password(SERVICE_NAME, USERNAME)
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
_clear_token_file()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _save_token_file(token: str) -> None:
|
|
52
|
+
"""File-based fallback for token storage."""
|
|
53
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
CREDENTIALS_FILE.write_text(token)
|
|
55
|
+
CREDENTIALS_FILE.chmod(0o600)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_token_file() -> str | None:
|
|
59
|
+
"""File-based fallback for token retrieval."""
|
|
60
|
+
if CREDENTIALS_FILE.exists():
|
|
61
|
+
content = CREDENTIALS_FILE.read_text().strip()
|
|
62
|
+
return content if content else None
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _clear_token_file() -> None:
|
|
67
|
+
"""Remove file-based token."""
|
|
68
|
+
if CREDENTIALS_FILE.exists():
|
|
69
|
+
CREDENTIALS_FILE.unlink()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""HTTP client for the CrowdTime API.
|
|
2
|
+
|
|
3
|
+
Wraps httpx with auth header injection, org-scoped URL prefixing,
|
|
4
|
+
and friendly error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from .auth import get_token
|
|
15
|
+
from .config import get_config
|
|
16
|
+
|
|
17
|
+
console = Console(stderr=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class APIError(Exception):
|
|
21
|
+
"""Raised when an API call fails."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str, status_code: int | None = None, detail: Any = None) -> None:
|
|
24
|
+
self.message = message
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.detail = detail
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CrowdTimeClient:
|
|
31
|
+
"""Synchronous HTTP client for the CrowdTime API."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, require_auth: bool = True, require_org: bool = False) -> None:
|
|
34
|
+
self.config = get_config()
|
|
35
|
+
self.base_url = self.config.server_url
|
|
36
|
+
self.token = get_token()
|
|
37
|
+
self.org_slug = self.config.organization
|
|
38
|
+
|
|
39
|
+
if require_auth and not self.token:
|
|
40
|
+
console.print(
|
|
41
|
+
"[red]Not authenticated.[/red] Please run [bold]ct login[/bold] first."
|
|
42
|
+
)
|
|
43
|
+
raise SystemExit(1)
|
|
44
|
+
|
|
45
|
+
if require_org and not self.org_slug:
|
|
46
|
+
console.print(
|
|
47
|
+
"[red]No organization set.[/red] Please run [bold]ct org switch <slug>[/bold] first."
|
|
48
|
+
)
|
|
49
|
+
raise SystemExit(1)
|
|
50
|
+
|
|
51
|
+
headers: dict[str, str] = {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"Accept": "application/json",
|
|
54
|
+
}
|
|
55
|
+
if self.token:
|
|
56
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
57
|
+
|
|
58
|
+
self._client = httpx.Client(
|
|
59
|
+
base_url=self.base_url,
|
|
60
|
+
headers=headers,
|
|
61
|
+
timeout=30.0,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _org_prefix(self) -> str:
|
|
65
|
+
"""Return the org-scoped URL prefix."""
|
|
66
|
+
return f"/api/v1/organizations/{self.org_slug}"
|
|
67
|
+
|
|
68
|
+
def _url(self, path: str, org_scoped: bool = True) -> str:
|
|
69
|
+
"""Build the full URL path."""
|
|
70
|
+
if path.startswith("/api/"):
|
|
71
|
+
return path
|
|
72
|
+
if org_scoped and self.org_slug:
|
|
73
|
+
return f"{self._org_prefix()}/{path.lstrip('/')}"
|
|
74
|
+
return f"/api/v1/{path.lstrip('/')}"
|
|
75
|
+
|
|
76
|
+
def get(self, path: str, params: dict[str, Any] | None = None,
|
|
77
|
+
org_scoped: bool = True) -> Any:
|
|
78
|
+
"""Make a GET request."""
|
|
79
|
+
url = self._url(path, org_scoped=org_scoped)
|
|
80
|
+
response = self._request("GET", url, params=params)
|
|
81
|
+
return response
|
|
82
|
+
|
|
83
|
+
def post(self, path: str, data: dict[str, Any] | None = None,
|
|
84
|
+
org_scoped: bool = True) -> Any:
|
|
85
|
+
"""Make a POST request."""
|
|
86
|
+
url = self._url(path, org_scoped=org_scoped)
|
|
87
|
+
response = self._request("POST", url, json=data)
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
def patch(self, path: str, data: dict[str, Any] | None = None,
|
|
91
|
+
org_scoped: bool = True) -> Any:
|
|
92
|
+
"""Make a PATCH request."""
|
|
93
|
+
url = self._url(path, org_scoped=org_scoped)
|
|
94
|
+
response = self._request("PATCH", url, json=data)
|
|
95
|
+
return response
|
|
96
|
+
|
|
97
|
+
def delete(self, path: str, org_scoped: bool = True) -> Any:
|
|
98
|
+
"""Make a DELETE request."""
|
|
99
|
+
url = self._url(path, org_scoped=org_scoped)
|
|
100
|
+
response = self._request("DELETE", url)
|
|
101
|
+
return response
|
|
102
|
+
|
|
103
|
+
def _request(self, method: str, url: str, **kwargs: Any) -> Any:
|
|
104
|
+
"""Execute an HTTP request with error handling."""
|
|
105
|
+
try:
|
|
106
|
+
response = self._client.request(method, url, **kwargs)
|
|
107
|
+
return self._handle_response(response)
|
|
108
|
+
except httpx.ConnectError:
|
|
109
|
+
console.print(
|
|
110
|
+
f"[red]Cannot connect to server at {self.base_url}[/red]\n"
|
|
111
|
+
"Is the CrowdTime server running?"
|
|
112
|
+
)
|
|
113
|
+
raise SystemExit(1)
|
|
114
|
+
except httpx.TimeoutException:
|
|
115
|
+
console.print("[red]Request timed out.[/red] Please try again.")
|
|
116
|
+
raise SystemExit(1)
|
|
117
|
+
except APIError:
|
|
118
|
+
raise
|
|
119
|
+
except httpx.HTTPError as e:
|
|
120
|
+
console.print(f"[red]HTTP error:[/red] {e}")
|
|
121
|
+
raise SystemExit(1)
|
|
122
|
+
|
|
123
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
124
|
+
"""Check response status and parse JSON."""
|
|
125
|
+
if response.status_code == 204:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
if response.status_code == 401:
|
|
129
|
+
console.print(
|
|
130
|
+
"[red]Authentication failed.[/red] Please run [bold]ct login[/bold] to re-authenticate."
|
|
131
|
+
)
|
|
132
|
+
raise SystemExit(1)
|
|
133
|
+
|
|
134
|
+
if response.status_code == 403:
|
|
135
|
+
console.print("[red]Permission denied.[/red] You don't have access to this resource.")
|
|
136
|
+
raise SystemExit(1)
|
|
137
|
+
|
|
138
|
+
if response.status_code == 404:
|
|
139
|
+
raise APIError("Not found", status_code=404)
|
|
140
|
+
|
|
141
|
+
if response.status_code >= 500:
|
|
142
|
+
# Server errors — show a friendly message, not raw HTML
|
|
143
|
+
try:
|
|
144
|
+
detail = response.json()
|
|
145
|
+
msg = detail.get("detail", "Internal server error")
|
|
146
|
+
except Exception:
|
|
147
|
+
msg = "Internal server error"
|
|
148
|
+
raise APIError(
|
|
149
|
+
f"Server error ({response.status_code}): {msg}",
|
|
150
|
+
status_code=response.status_code,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if response.status_code >= 400:
|
|
154
|
+
try:
|
|
155
|
+
detail = response.json()
|
|
156
|
+
except Exception:
|
|
157
|
+
detail = response.text
|
|
158
|
+
msg = detail.get("detail", str(detail)) if isinstance(detail, dict) else str(detail)
|
|
159
|
+
raise APIError(msg, status_code=response.status_code, detail=detail)
|
|
160
|
+
|
|
161
|
+
if not response.content:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
return response.json()
|
|
166
|
+
except Exception:
|
|
167
|
+
return response.text
|
|
168
|
+
|
|
169
|
+
def close(self) -> None:
|
|
170
|
+
"""Close the underlying HTTP client."""
|
|
171
|
+
self._client.close()
|
|
172
|
+
|
|
173
|
+
def __enter__(self) -> CrowdTimeClient:
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def __exit__(self, *args: Any) -> None:
|
|
177
|
+
self.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CrowdTime CLI commands."""
|