boring-cli 0.1.1__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.
- boring_cli-0.1.1/.gitignore +52 -0
- boring_cli-0.1.1/LICENSE +21 -0
- boring_cli-0.1.1/PKG-INFO +150 -0
- boring_cli-0.1.1/README.md +116 -0
- boring_cli-0.1.1/pyproject.toml +66 -0
- boring_cli-0.1.1/src/boring/__init__.py +3 -0
- boring_cli-0.1.1/src/boring/client.py +121 -0
- boring_cli-0.1.1/src/boring/commands/__init__.py +1 -0
- boring_cli-0.1.1/src/boring/commands/download.py +89 -0
- boring_cli-0.1.1/src/boring/commands/setup.py +132 -0
- boring_cli-0.1.1/src/boring/commands/solve.py +82 -0
- boring_cli-0.1.1/src/boring/commands/status.py +83 -0
- boring_cli-0.1.1/src/boring/config.py +97 -0
- boring_cli-0.1.1/src/boring/main.py +39 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
venv/
|
|
25
|
+
ENV/
|
|
26
|
+
env/
|
|
27
|
+
.venv/
|
|
28
|
+
|
|
29
|
+
# IDE
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# Testing
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.coverage
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
|
|
43
|
+
# Build
|
|
44
|
+
*.manifest
|
|
45
|
+
*.spec
|
|
46
|
+
|
|
47
|
+
# OS
|
|
48
|
+
.DS_Store
|
|
49
|
+
Thumbs.db
|
|
50
|
+
|
|
51
|
+
# Project specific
|
|
52
|
+
.boring-agents/
|
boring_cli-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Boring Agents Team
|
|
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,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boring-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI tool for managing Lark Suite tasks
|
|
5
|
+
Project-URL: Homepage, https://github.com/anhbinhnguyen/boring-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/anhbinhnguyen/boring-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/anhbinhnguyen/boring-cli/issues
|
|
8
|
+
Author: Boring Agents Team
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,lark,productivity,tasks
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: click>=8.0.0
|
|
25
|
+
Requires-Dist: httpx>=0.26.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: rich>=13.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Boring CLI
|
|
36
|
+
|
|
37
|
+
[](https://badge.fury.io/py/boring-cli)
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
41
|
+
CLI tool for managing Lark Suite tasks from the command line.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install boring-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
### 1. Setup
|
|
52
|
+
|
|
53
|
+
Configure the CLI and login to Lark:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
boring setup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This will prompt you for:
|
|
60
|
+
- Server URL (your Boring Agents API server)
|
|
61
|
+
- Bugs output directory (where tasks will be downloaded)
|
|
62
|
+
- Lark task list GUIDs
|
|
63
|
+
- Lark OAuth login
|
|
64
|
+
|
|
65
|
+
### 2. Download Tasks
|
|
66
|
+
|
|
67
|
+
Download tasks with Critical/Blocked/High labels to your local folder:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
boring download
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Download with specific labels:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
boring download --labels "Critical,Blocked"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Solve Tasks
|
|
80
|
+
|
|
81
|
+
Move completed tasks (from your bugs folder) to the Solved section in Lark:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
boring solve
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Keep local folders after solving:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
boring solve --keep
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 4. Check Status
|
|
94
|
+
|
|
95
|
+
View your current configuration:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
boring status
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Commands
|
|
102
|
+
|
|
103
|
+
| Command | Description |
|
|
104
|
+
|---------|-------------|
|
|
105
|
+
| `boring setup` | Configure CLI and login to Lark |
|
|
106
|
+
| `boring download` | Download tasks to local folder |
|
|
107
|
+
| `boring solve` | Move tasks to Solved section |
|
|
108
|
+
| `boring status` | Show current configuration |
|
|
109
|
+
| `boring --version` | Show version |
|
|
110
|
+
| `boring --help` | Show help |
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
Configuration is stored in `~/.boring-agents/config.yaml`:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
server_url: https://boring.omelet.tech/api
|
|
118
|
+
jwt_token: eyJhbGc...
|
|
119
|
+
bugs_dir: /path/to/bugs
|
|
120
|
+
tasklist_guid: xxxx-xxxx-xxxx
|
|
121
|
+
section_guid: xxxx-xxxx-xxxx
|
|
122
|
+
solved_section_guid: xxxx-xxxx-xxxx
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Requirements
|
|
126
|
+
|
|
127
|
+
- Python 3.9+
|
|
128
|
+
- A running [Boring Agents](https://github.com/anhbinhnguyen/boring-agents) server
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Clone the repo
|
|
134
|
+
git clone https://github.com/anhbinhnguyen/boring-cli.git
|
|
135
|
+
cd boring-cli
|
|
136
|
+
|
|
137
|
+
# Install in development mode
|
|
138
|
+
pip install -e ".[dev]"
|
|
139
|
+
|
|
140
|
+
# Run tests
|
|
141
|
+
pytest
|
|
142
|
+
|
|
143
|
+
# Format code
|
|
144
|
+
black src/
|
|
145
|
+
ruff check src/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Boring CLI
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/boring-cli)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
CLI tool for managing Lark Suite tasks from the command line.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install boring-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### 1. Setup
|
|
18
|
+
|
|
19
|
+
Configure the CLI and login to Lark:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
boring setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This will prompt you for:
|
|
26
|
+
- Server URL (your Boring Agents API server)
|
|
27
|
+
- Bugs output directory (where tasks will be downloaded)
|
|
28
|
+
- Lark task list GUIDs
|
|
29
|
+
- Lark OAuth login
|
|
30
|
+
|
|
31
|
+
### 2. Download Tasks
|
|
32
|
+
|
|
33
|
+
Download tasks with Critical/Blocked/High labels to your local folder:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
boring download
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Download with specific labels:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
boring download --labels "Critical,Blocked"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Solve Tasks
|
|
46
|
+
|
|
47
|
+
Move completed tasks (from your bugs folder) to the Solved section in Lark:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
boring solve
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Keep local folders after solving:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
boring solve --keep
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4. Check Status
|
|
60
|
+
|
|
61
|
+
View your current configuration:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
boring status
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
|---------|-------------|
|
|
71
|
+
| `boring setup` | Configure CLI and login to Lark |
|
|
72
|
+
| `boring download` | Download tasks to local folder |
|
|
73
|
+
| `boring solve` | Move tasks to Solved section |
|
|
74
|
+
| `boring status` | Show current configuration |
|
|
75
|
+
| `boring --version` | Show version |
|
|
76
|
+
| `boring --help` | Show help |
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
Configuration is stored in `~/.boring-agents/config.yaml`:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
server_url: https://boring.omelet.tech/api
|
|
84
|
+
jwt_token: eyJhbGc...
|
|
85
|
+
bugs_dir: /path/to/bugs
|
|
86
|
+
tasklist_guid: xxxx-xxxx-xxxx
|
|
87
|
+
section_guid: xxxx-xxxx-xxxx
|
|
88
|
+
solved_section_guid: xxxx-xxxx-xxxx
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python 3.9+
|
|
94
|
+
- A running [Boring Agents](https://github.com/anhbinhnguyen/boring-agents) server
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Clone the repo
|
|
100
|
+
git clone https://github.com/anhbinhnguyen/boring-cli.git
|
|
101
|
+
cd boring-cli
|
|
102
|
+
|
|
103
|
+
# Install in development mode
|
|
104
|
+
pip install -e ".[dev]"
|
|
105
|
+
|
|
106
|
+
# Run tests
|
|
107
|
+
pytest
|
|
108
|
+
|
|
109
|
+
# Format code
|
|
110
|
+
black src/
|
|
111
|
+
ruff check src/
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "boring-cli"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "CLI tool for managing Lark Suite tasks"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Boring Agents Team" }
|
|
13
|
+
]
|
|
14
|
+
keywords = ["lark", "tasks", "cli", "productivity"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Utilities",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.9"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"click>=8.0.0",
|
|
31
|
+
"httpx>=0.26.0",
|
|
32
|
+
"pyyaml>=6.0",
|
|
33
|
+
"rich>=13.0.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=7.0.0",
|
|
39
|
+
"pytest-cov>=4.0.0",
|
|
40
|
+
"black>=23.0.0",
|
|
41
|
+
"ruff>=0.1.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
boring = "boring.main:main"
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/anhbinhnguyen/boring-cli"
|
|
49
|
+
Repository = "https://github.com/anhbinhnguyen/boring-cli"
|
|
50
|
+
Issues = "https://github.com/anhbinhnguyen/boring-cli/issues"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.sdist]
|
|
53
|
+
include = [
|
|
54
|
+
"/src",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/boring"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py39"
|
|
63
|
+
|
|
64
|
+
[tool.black]
|
|
65
|
+
line-length = 100
|
|
66
|
+
target-version = ["py39"]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""HTTP client for Boring Agents API."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .config import get_jwt_token, get_server_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIClient:
|
|
11
|
+
"""Client for interacting with the Boring Agents API."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
|
|
14
|
+
self.base_url = base_url or get_server_url()
|
|
15
|
+
self.token = token or get_jwt_token()
|
|
16
|
+
|
|
17
|
+
def _headers(self) -> dict:
|
|
18
|
+
return {
|
|
19
|
+
"Authorization": f"Bearer {self.token}",
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def _check_config(self) -> None:
|
|
24
|
+
if not self.base_url:
|
|
25
|
+
raise Exception("Server URL not configured. Run 'boring setup' first.")
|
|
26
|
+
if not self.token:
|
|
27
|
+
raise Exception("Not logged in. Run 'boring setup' first.")
|
|
28
|
+
|
|
29
|
+
def get_login_url(self) -> str:
|
|
30
|
+
"""Get the Lark OAuth login URL."""
|
|
31
|
+
if not self.base_url:
|
|
32
|
+
raise Exception("Server URL not configured.")
|
|
33
|
+
with httpx.Client() as client:
|
|
34
|
+
response = client.get(f"{self.base_url}/api/v1/auth/login")
|
|
35
|
+
response.raise_for_status()
|
|
36
|
+
return response.json().get("auth_url")
|
|
37
|
+
|
|
38
|
+
def complete_login(self, code: str) -> dict:
|
|
39
|
+
"""Complete the OAuth login with the authorization code."""
|
|
40
|
+
if not self.base_url:
|
|
41
|
+
raise Exception("Server URL not configured.")
|
|
42
|
+
with httpx.Client() as client:
|
|
43
|
+
response = client.get(
|
|
44
|
+
f"{self.base_url}/api/v1/auth/callback", params={"code": code}
|
|
45
|
+
)
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
return response.json()
|
|
48
|
+
|
|
49
|
+
def get_me(self) -> dict:
|
|
50
|
+
"""Get current user information."""
|
|
51
|
+
self._check_config()
|
|
52
|
+
with httpx.Client() as client:
|
|
53
|
+
response = client.get(
|
|
54
|
+
f"{self.base_url}/api/v1/auth/me", headers=self._headers()
|
|
55
|
+
)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
return response.json()
|
|
58
|
+
|
|
59
|
+
def get_tasks(
|
|
60
|
+
self, labels: Optional[str] = None, section_guid: Optional[str] = None
|
|
61
|
+
) -> dict:
|
|
62
|
+
"""Get tasks with optional filters."""
|
|
63
|
+
self._check_config()
|
|
64
|
+
params = {}
|
|
65
|
+
if labels:
|
|
66
|
+
params["labels"] = labels
|
|
67
|
+
if section_guid:
|
|
68
|
+
params["section_guid"] = section_guid
|
|
69
|
+
|
|
70
|
+
with httpx.Client() as client:
|
|
71
|
+
response = client.get(
|
|
72
|
+
f"{self.base_url}/api/v1/tasks/",
|
|
73
|
+
headers=self._headers(),
|
|
74
|
+
params=params,
|
|
75
|
+
)
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
return response.json()
|
|
78
|
+
|
|
79
|
+
def get_critical_tasks(self) -> dict:
|
|
80
|
+
"""Get critical and blocked tasks."""
|
|
81
|
+
self._check_config()
|
|
82
|
+
with httpx.Client() as client:
|
|
83
|
+
response = client.get(
|
|
84
|
+
f"{self.base_url}/api/v1/tasks/critical", headers=self._headers()
|
|
85
|
+
)
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
return response.json()
|
|
88
|
+
|
|
89
|
+
def download_tasks(
|
|
90
|
+
self, labels: Optional[str] = None, section_guid: Optional[str] = None
|
|
91
|
+
) -> dict:
|
|
92
|
+
"""Download tasks as markdown content."""
|
|
93
|
+
self._check_config()
|
|
94
|
+
params = {}
|
|
95
|
+
if labels:
|
|
96
|
+
params["labels"] = labels
|
|
97
|
+
if section_guid:
|
|
98
|
+
params["section_guid"] = section_guid
|
|
99
|
+
|
|
100
|
+
with httpx.Client(timeout=120) as client:
|
|
101
|
+
response = client.get(
|
|
102
|
+
f"{self.base_url}/api/v1/tasks/download",
|
|
103
|
+
headers=self._headers(),
|
|
104
|
+
params=params,
|
|
105
|
+
)
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
return response.json()
|
|
108
|
+
|
|
109
|
+
def solve_task(
|
|
110
|
+
self, task_guid: str, tasklist_guid: str, section_guid: str
|
|
111
|
+
) -> dict:
|
|
112
|
+
"""Move a task to the solved section."""
|
|
113
|
+
self._check_config()
|
|
114
|
+
with httpx.Client() as client:
|
|
115
|
+
response = client.post(
|
|
116
|
+
f"{self.base_url}/api/v1/tasks/{task_guid}/solve",
|
|
117
|
+
headers=self._headers(),
|
|
118
|
+
json={"tasklist_guid": tasklist_guid, "section_guid": section_guid},
|
|
119
|
+
)
|
|
120
|
+
response.raise_for_status()
|
|
121
|
+
return response.json()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Boring CLI commands."""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Download command for Boring CLI."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
10
|
+
|
|
11
|
+
from .. import config
|
|
12
|
+
from ..client import APIClient
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command()
|
|
18
|
+
@click.option("--labels", default=None, help="Comma-separated labels to filter")
|
|
19
|
+
def download(labels: str):
|
|
20
|
+
"""Download tasks from Lark and save as markdown files."""
|
|
21
|
+
if not config.is_configured():
|
|
22
|
+
console.print("[bold red]CLI not configured.[/bold red] Run 'boring setup' first.")
|
|
23
|
+
raise click.Abort()
|
|
24
|
+
|
|
25
|
+
bugs_dir = config.get_bugs_dir()
|
|
26
|
+
section_guid = config.get_section_guid()
|
|
27
|
+
|
|
28
|
+
if not bugs_dir:
|
|
29
|
+
console.print("[bold red]Bugs directory not configured.[/bold red] Run 'boring setup' first.")
|
|
30
|
+
raise click.Abort()
|
|
31
|
+
|
|
32
|
+
console.print(f"[bold]Downloading tasks to:[/bold] [cyan]{bugs_dir}[/cyan]")
|
|
33
|
+
if section_guid:
|
|
34
|
+
console.print(f"[dim]Filtering by section: {section_guid}[/dim]")
|
|
35
|
+
if labels:
|
|
36
|
+
console.print(f"[dim]Filtering by labels: {labels}[/dim]")
|
|
37
|
+
|
|
38
|
+
client = APIClient()
|
|
39
|
+
|
|
40
|
+
with Progress(
|
|
41
|
+
SpinnerColumn(),
|
|
42
|
+
TextColumn("[progress.description]{task.description}"),
|
|
43
|
+
console=console,
|
|
44
|
+
) as progress:
|
|
45
|
+
task = progress.add_task("Fetching tasks from server...", total=None)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
result = client.download_tasks(labels=labels, section_guid=section_guid)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
console.print(f"[bold red]Failed to download tasks:[/bold red] {e}")
|
|
51
|
+
raise click.Abort()
|
|
52
|
+
|
|
53
|
+
progress.update(task, description="Processing tasks...")
|
|
54
|
+
|
|
55
|
+
tasks = result.get("tasks", [])
|
|
56
|
+
count = result.get("count", 0)
|
|
57
|
+
|
|
58
|
+
if count == 0:
|
|
59
|
+
console.print("[yellow]No tasks found matching criteria.[/yellow]")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
console.print(f"\n[bold green]Found {count} task(s)[/bold green]\n")
|
|
63
|
+
|
|
64
|
+
os.makedirs(bugs_dir, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
for i, task_data in enumerate(tasks, 1):
|
|
67
|
+
guid = task_data.get("guid")
|
|
68
|
+
summary = task_data.get("summary", "No title")[:50]
|
|
69
|
+
console.print(f"[{i}/{count}] Processing: [cyan]{summary}[/cyan]...")
|
|
70
|
+
|
|
71
|
+
task_dir = Path(bugs_dir) / guid
|
|
72
|
+
task_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
md_path = task_dir / "description.md"
|
|
75
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
76
|
+
f.write(task_data.get("markdown_content", ""))
|
|
77
|
+
|
|
78
|
+
for img in task_data.get("images", []):
|
|
79
|
+
img_index = img.get("index", 1)
|
|
80
|
+
img_data = img.get("data")
|
|
81
|
+
if img_data:
|
|
82
|
+
img_bytes = base64.b64decode(img_data)
|
|
83
|
+
img_path = task_dir / f"image_{img_index}.jpg"
|
|
84
|
+
with open(img_path, "wb") as f:
|
|
85
|
+
f.write(img_bytes)
|
|
86
|
+
|
|
87
|
+
console.print(f" [dim]Saved to {task_dir}/description.md[/dim]")
|
|
88
|
+
|
|
89
|
+
console.print(f"\n[bold green]Done![/bold green] {count} task(s) saved to '{bugs_dir}/'")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Setup command for Boring CLI."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import webbrowser
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from .. import config
|
|
12
|
+
from ..client import APIClient
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
18
|
+
"""Handler for OAuth callback."""
|
|
19
|
+
|
|
20
|
+
code = None
|
|
21
|
+
|
|
22
|
+
def do_GET(self):
|
|
23
|
+
query = urlparse(self.path).query
|
|
24
|
+
params = parse_qs(query)
|
|
25
|
+
if "code" in params:
|
|
26
|
+
OAuthCallbackHandler.code = params["code"][0]
|
|
27
|
+
self.send_response(200)
|
|
28
|
+
self.send_header("Content-type", "text/html")
|
|
29
|
+
self.end_headers()
|
|
30
|
+
self.wfile.write(
|
|
31
|
+
b"""
|
|
32
|
+
<html>
|
|
33
|
+
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
34
|
+
<h1 style="color: #10B981;">Login Successful!</h1>
|
|
35
|
+
<p>You can close this window and return to the terminal.</p>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
38
|
+
"""
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
self.send_response(400)
|
|
42
|
+
self.end_headers()
|
|
43
|
+
self.wfile.write(b"Missing code parameter")
|
|
44
|
+
|
|
45
|
+
def log_message(self, format, *args):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.command()
|
|
50
|
+
@click.option(
|
|
51
|
+
"--server-url",
|
|
52
|
+
prompt="Server URL",
|
|
53
|
+
default=lambda: config.get_server_url() or "https://boring.omelet.tech/api",
|
|
54
|
+
help="URL of the Boring Agents API server",
|
|
55
|
+
)
|
|
56
|
+
def setup(server_url: str):
|
|
57
|
+
"""Configure the CLI and login to Lark."""
|
|
58
|
+
console.print("\n[bold blue]Configuring Boring CLI...[/bold blue]")
|
|
59
|
+
console.print(f"Server URL: [cyan]{server_url}[/cyan]")
|
|
60
|
+
|
|
61
|
+
config.set_server_url(server_url)
|
|
62
|
+
|
|
63
|
+
bugs_dir = click.prompt(
|
|
64
|
+
"Bugs output directory", default=config.get_bugs_dir() or "/tmp/bugs"
|
|
65
|
+
)
|
|
66
|
+
config.set_bugs_dir(bugs_dir)
|
|
67
|
+
|
|
68
|
+
tasklist_guid = click.prompt(
|
|
69
|
+
"Tasklist GUID (from Lark)", default=config.get_tasklist_guid() or ""
|
|
70
|
+
)
|
|
71
|
+
if tasklist_guid:
|
|
72
|
+
config.set_tasklist_guid(tasklist_guid)
|
|
73
|
+
|
|
74
|
+
section_guid = click.prompt(
|
|
75
|
+
"In-progress Section GUID", default=config.get_section_guid() or ""
|
|
76
|
+
)
|
|
77
|
+
if section_guid:
|
|
78
|
+
config.set_section_guid(section_guid)
|
|
79
|
+
|
|
80
|
+
solved_section_guid = click.prompt(
|
|
81
|
+
"Solved Section GUID", default=config.get_solved_section_guid() or ""
|
|
82
|
+
)
|
|
83
|
+
if solved_section_guid:
|
|
84
|
+
config.set_solved_section_guid(solved_section_guid)
|
|
85
|
+
|
|
86
|
+
console.print("\n[bold]Starting Lark OAuth login...[/bold]")
|
|
87
|
+
|
|
88
|
+
server = HTTPServer(("localhost", 9876), OAuthCallbackHandler)
|
|
89
|
+
server_thread = threading.Thread(target=server.handle_request)
|
|
90
|
+
server_thread.start()
|
|
91
|
+
|
|
92
|
+
client = APIClient()
|
|
93
|
+
try:
|
|
94
|
+
auth_url = client.get_login_url()
|
|
95
|
+
full_auth_url = auth_url + "&redirect_uri=http://localhost:9876/callback"
|
|
96
|
+
console.print("\n[yellow]Opening browser for Lark login...[/yellow]")
|
|
97
|
+
console.print(f"If browser doesn't open, visit: [link]{full_auth_url}[/link]")
|
|
98
|
+
webbrowser.open(full_auth_url)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
console.print(f"\n[yellow]Note: Could not get auth URL from server: {e}[/yellow]")
|
|
101
|
+
console.print("Please complete OAuth flow manually and get the code.")
|
|
102
|
+
code = click.prompt("Enter the OAuth code")
|
|
103
|
+
OAuthCallbackHandler.code = code
|
|
104
|
+
|
|
105
|
+
if not OAuthCallbackHandler.code:
|
|
106
|
+
console.print("[dim]Waiting for OAuth callback...[/dim]")
|
|
107
|
+
server_thread.join(timeout=300)
|
|
108
|
+
|
|
109
|
+
if OAuthCallbackHandler.code:
|
|
110
|
+
try:
|
|
111
|
+
result = client.complete_login(OAuthCallbackHandler.code)
|
|
112
|
+
token = result.get("token", {}).get("access_token")
|
|
113
|
+
user = result.get("user", {})
|
|
114
|
+
|
|
115
|
+
config.set_jwt_token(token)
|
|
116
|
+
|
|
117
|
+
console.print("\n[bold green]Login successful![/bold green]")
|
|
118
|
+
console.print(
|
|
119
|
+
f"Logged in as: [cyan]{user.get('name') or user.get('email') or 'User'}[/cyan]"
|
|
120
|
+
)
|
|
121
|
+
console.print(f"\nConfiguration saved to: [dim]{config.CONFIG_FILE}[/dim]")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
console.print(f"\n[bold red]Login failed: {e}[/bold red]")
|
|
124
|
+
raise click.Abort()
|
|
125
|
+
else:
|
|
126
|
+
console.print("\n[bold red]OAuth flow timed out or failed.[/bold red]")
|
|
127
|
+
raise click.Abort()
|
|
128
|
+
|
|
129
|
+
server.server_close()
|
|
130
|
+
console.print(
|
|
131
|
+
"\n[bold green]Setup complete![/bold green] You can now use 'boring download' and 'boring solve'."
|
|
132
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Solve command for Boring CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from .. import config
|
|
10
|
+
from ..client import APIClient
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_task_folders(bugs_dir: str) -> list:
|
|
16
|
+
"""Get all task folders from the bugs directory."""
|
|
17
|
+
folders = []
|
|
18
|
+
if not os.path.exists(bugs_dir):
|
|
19
|
+
return folders
|
|
20
|
+
for name in os.listdir(bugs_dir):
|
|
21
|
+
path = os.path.join(bugs_dir, name)
|
|
22
|
+
# UUID format check
|
|
23
|
+
if os.path.isdir(path) and len(name) == 36 and name.count("-") == 4:
|
|
24
|
+
folders.append((name, path))
|
|
25
|
+
return folders
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command()
|
|
29
|
+
@click.option("--keep", is_flag=True, help="Keep local folders after solving")
|
|
30
|
+
def solve(keep: bool):
|
|
31
|
+
"""Move completed tasks to Solved section in Lark."""
|
|
32
|
+
if not config.is_configured():
|
|
33
|
+
console.print("[bold red]CLI not configured.[/bold red] Run 'boring setup' first.")
|
|
34
|
+
raise click.Abort()
|
|
35
|
+
|
|
36
|
+
bugs_dir = config.get_bugs_dir()
|
|
37
|
+
tasklist_guid = config.get_tasklist_guid()
|
|
38
|
+
solved_section_guid = config.get_solved_section_guid()
|
|
39
|
+
|
|
40
|
+
if not bugs_dir:
|
|
41
|
+
console.print("[bold red]Bugs directory not configured.[/bold red] Run 'boring setup' first.")
|
|
42
|
+
raise click.Abort()
|
|
43
|
+
|
|
44
|
+
if not tasklist_guid or not solved_section_guid:
|
|
45
|
+
console.print(
|
|
46
|
+
"[bold red]Tasklist GUID and Solved Section GUID required.[/bold red] "
|
|
47
|
+
"Run 'boring setup' first."
|
|
48
|
+
)
|
|
49
|
+
raise click.Abort()
|
|
50
|
+
|
|
51
|
+
task_folders = get_task_folders(bugs_dir)
|
|
52
|
+
|
|
53
|
+
if not task_folders:
|
|
54
|
+
console.print("[yellow]No tasks found in bugs folder.[/yellow]")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
console.print(f"[bold]Found {len(task_folders)} task(s) to move to Solved[/bold]\n")
|
|
58
|
+
|
|
59
|
+
client = APIClient()
|
|
60
|
+
success_count = 0
|
|
61
|
+
|
|
62
|
+
for task_guid, folder_path in task_folders:
|
|
63
|
+
try:
|
|
64
|
+
result = client.solve_task(
|
|
65
|
+
task_guid=task_guid,
|
|
66
|
+
tasklist_guid=tasklist_guid,
|
|
67
|
+
section_guid=solved_section_guid,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if result.get("success"):
|
|
71
|
+
console.print(f"[green]OK[/green] - {task_guid}")
|
|
72
|
+
if not keep:
|
|
73
|
+
shutil.rmtree(folder_path)
|
|
74
|
+
success_count += 1
|
|
75
|
+
else:
|
|
76
|
+
console.print(f"[red]FAIL[/red] - {task_guid}: {result.get('message')}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
console.print(f"[red]ERROR[/red] - {task_guid}: {e}")
|
|
79
|
+
|
|
80
|
+
console.print(
|
|
81
|
+
f"\n[bold green]Done![/bold green] Moved {success_count}/{len(task_folders)} task(s) to Solved."
|
|
82
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Status command for Boring CLI."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from .. import config
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
def status():
|
|
14
|
+
"""Show current configuration status."""
|
|
15
|
+
cfg = config.load_config()
|
|
16
|
+
|
|
17
|
+
table = Table(title="Boring CLI Configuration")
|
|
18
|
+
table.add_column("Setting", style="cyan")
|
|
19
|
+
table.add_column("Value", style="green")
|
|
20
|
+
table.add_column("Status", style="yellow")
|
|
21
|
+
|
|
22
|
+
# Server URL
|
|
23
|
+
server_url = cfg.get("server_url", "")
|
|
24
|
+
table.add_row(
|
|
25
|
+
"Server URL",
|
|
26
|
+
server_url or "[dim]Not set[/dim]",
|
|
27
|
+
"[green]OK[/green]" if server_url else "[red]Missing[/red]",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# JWT Token
|
|
31
|
+
jwt_token = cfg.get("jwt_token", "")
|
|
32
|
+
token_display = f"{jwt_token[:20]}..." if jwt_token else "[dim]Not set[/dim]"
|
|
33
|
+
table.add_row(
|
|
34
|
+
"JWT Token",
|
|
35
|
+
token_display,
|
|
36
|
+
"[green]OK[/green]" if jwt_token else "[red]Missing[/red]",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Bugs Directory
|
|
40
|
+
bugs_dir = cfg.get("bugs_dir", "")
|
|
41
|
+
table.add_row(
|
|
42
|
+
"Bugs Directory",
|
|
43
|
+
bugs_dir or "[dim]Not set[/dim]",
|
|
44
|
+
"[green]OK[/green]" if bugs_dir else "[red]Missing[/red]",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Tasklist GUID
|
|
48
|
+
tasklist_guid = cfg.get("tasklist_guid", "")
|
|
49
|
+
table.add_row(
|
|
50
|
+
"Tasklist GUID",
|
|
51
|
+
tasklist_guid or "[dim]Not set[/dim]",
|
|
52
|
+
"[green]OK[/green]" if tasklist_guid else "[yellow]Optional[/yellow]",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Section GUID
|
|
56
|
+
section_guid = cfg.get("section_guid", "")
|
|
57
|
+
table.add_row(
|
|
58
|
+
"Section GUID",
|
|
59
|
+
section_guid or "[dim]Not set[/dim]",
|
|
60
|
+
"[green]OK[/green]" if section_guid else "[yellow]Optional[/yellow]",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Solved Section GUID
|
|
64
|
+
solved_section_guid = cfg.get("solved_section_guid", "")
|
|
65
|
+
table.add_row(
|
|
66
|
+
"Solved Section GUID",
|
|
67
|
+
solved_section_guid or "[dim]Not set[/dim]",
|
|
68
|
+
"[green]OK[/green]" if solved_section_guid else "[yellow]Optional[/yellow]",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
console.print()
|
|
72
|
+
console.print(table)
|
|
73
|
+
console.print()
|
|
74
|
+
|
|
75
|
+
if config.is_configured():
|
|
76
|
+
console.print("[bold green]CLI is properly configured![/bold green]")
|
|
77
|
+
else:
|
|
78
|
+
console.print(
|
|
79
|
+
"[bold yellow]CLI is not fully configured.[/bold yellow] "
|
|
80
|
+
"Run 'boring setup' to complete configuration."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
console.print(f"\n[dim]Config file: {config.CONFIG_FILE}[/dim]")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Configuration management for Boring CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
CONFIG_DIR = Path.home() / ".boring-agents"
|
|
10
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ensure_config_dir() -> None:
|
|
14
|
+
"""Ensure the config directory exists."""
|
|
15
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config() -> dict:
|
|
19
|
+
"""Load configuration from file."""
|
|
20
|
+
if not CONFIG_FILE.exists():
|
|
21
|
+
return {}
|
|
22
|
+
with open(CONFIG_FILE, "r") as f:
|
|
23
|
+
return yaml.safe_load(f) or {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_config(config: dict) -> None:
|
|
27
|
+
"""Save configuration to file."""
|
|
28
|
+
ensure_config_dir()
|
|
29
|
+
with open(CONFIG_FILE, "w") as f:
|
|
30
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_value(key: str) -> Optional[str]:
|
|
34
|
+
"""Get a configuration value."""
|
|
35
|
+
return load_config().get(key)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_value(key: str, value: str) -> None:
|
|
39
|
+
"""Set a configuration value."""
|
|
40
|
+
config = load_config()
|
|
41
|
+
config[key] = value
|
|
42
|
+
save_config(config)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Convenience accessors
|
|
46
|
+
def get_server_url() -> Optional[str]:
|
|
47
|
+
return get_value("server_url")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_jwt_token() -> Optional[str]:
|
|
51
|
+
return get_value("jwt_token")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_bugs_dir() -> Optional[str]:
|
|
55
|
+
return get_value("bugs_dir")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_tasklist_guid() -> Optional[str]:
|
|
59
|
+
return get_value("tasklist_guid")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_section_guid() -> Optional[str]:
|
|
63
|
+
return get_value("section_guid")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_solved_section_guid() -> Optional[str]:
|
|
67
|
+
return get_value("solved_section_guid")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def set_server_url(url: str) -> None:
|
|
71
|
+
set_value("server_url", url)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def set_jwt_token(token: str) -> None:
|
|
75
|
+
set_value("jwt_token", token)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_bugs_dir(path: str) -> None:
|
|
79
|
+
set_value("bugs_dir", path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def set_tasklist_guid(guid: str) -> None:
|
|
83
|
+
set_value("tasklist_guid", guid)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def set_section_guid(guid: str) -> None:
|
|
87
|
+
set_value("section_guid", guid)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def set_solved_section_guid(guid: str) -> None:
|
|
91
|
+
set_value("solved_section_guid", guid)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_configured() -> bool:
|
|
95
|
+
"""Check if the CLI is properly configured."""
|
|
96
|
+
config = load_config()
|
|
97
|
+
return all([config.get("server_url"), config.get("jwt_token"), config.get("bugs_dir")])
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Main entry point for Boring CLI."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
from .commands.download import download
|
|
7
|
+
from .commands.setup import setup
|
|
8
|
+
from .commands.solve import solve
|
|
9
|
+
from .commands.status import status
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
@click.version_option(version=__version__, prog_name="boring")
|
|
14
|
+
def cli():
|
|
15
|
+
"""Boring CLI - Manage Lark tasks from the command line.
|
|
16
|
+
|
|
17
|
+
\b
|
|
18
|
+
Quick start:
|
|
19
|
+
boring setup Configure and login to Lark
|
|
20
|
+
boring download Download tasks to local folder
|
|
21
|
+
boring solve Move completed tasks to Solved
|
|
22
|
+
boring status Show current configuration
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
cli.add_command(setup)
|
|
28
|
+
cli.add_command(download)
|
|
29
|
+
cli.add_command(solve)
|
|
30
|
+
cli.add_command(status)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
"""Main entry point."""
|
|
35
|
+
cli()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|