bugcap 0.5.0__tar.gz → 0.6.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.
- {bugcap-0.5.0/src/bugcap.egg-info → bugcap-0.6.0}/PKG-INFO +18 -11
- {bugcap-0.5.0 → bugcap-0.6.0}/README.md +16 -10
- {bugcap-0.5.0 → bugcap-0.6.0}/pyproject.toml +12 -2
- bugcap-0.6.0/src/bugcap/__init__.py +1 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/agent_api.py +10 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/backends.py +43 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/cli.py +59 -18
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/media.py +10 -3
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/server.py +5 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/static/app.js +5 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/ingest.py +93 -10
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/live.py +69 -9
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/live_session.py +41 -8
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/mcp_server.py +28 -5
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/paths.py +7 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/refs.py +5 -4
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/repo.py +65 -2
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/service.py +27 -6
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/store.py +38 -4
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/sync.py +3 -0
- {bugcap-0.5.0 → bugcap-0.6.0/src/bugcap.egg-info}/PKG-INFO +18 -11
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/SOURCES.txt +1 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/requires.txt +1 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_backends.py +57 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_init_capture_list.py +1 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_live_headless.py +5 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_media.py +2 -2
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_no_image_delete.py +1 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_setup.py +38 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_triage.py +4 -2
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_drafts.py +2 -1
- bugcap-0.6.0/tests/test_file_attachments.py +217 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_github_media_sync.py +2 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_github_sync.py +1 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_ingest.py +4 -2
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_mcp.py +1 -2
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_mcp_logging.py +13 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_run.py +3 -3
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_refs_validate_rewrite.py +3 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_regression_output.py +17 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_repo_config.py +46 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_repo_values.py +0 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_store_migration.py +62 -7
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_sync_consent.py +0 -3
- bugcap-0.5.0/src/bugcap/__init__.py +0 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/LICENSE +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/setup.cfg +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/capture.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/config.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/__init__.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/api.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/static/app.css +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/static/index.html +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/drafts.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/errors.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/ghcli.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/logs.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/recorder.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/tomlio.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/validation.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/dependency_links.txt +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/entry_points.txt +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/top_level.txt +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_agent_api.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_capture.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_attach.py +1 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_dashboard.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_record.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_api.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_media.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_mutations.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_security.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_ui_static.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_ghcli.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_github_pull.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_image_refs.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_ingest_url.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_live_session.py +1 -1
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_paths.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_argv.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_devices.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_media_real.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_refs.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_service_media.py +0 -0
- {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_service_refs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bugcap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Local-first, agent-readable bug capture: annotated screenshots + notes, stored locally, synced to trackers on your own schedule.
|
|
5
5
|
Author: Konrad Tagnon Amen ALAHASSA
|
|
6
6
|
License: MIT
|
|
@@ -12,10 +12,11 @@ Provides-Extra: mcp
|
|
|
12
12
|
Requires-Dist: mcp<2,>=1.2; extra == "mcp"
|
|
13
13
|
Provides-Extra: dev
|
|
14
14
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
15
16
|
Dynamic: license-file
|
|
16
17
|
|
|
17
18
|
<p align="center">
|
|
18
|
-
<img src="docs/assets/logo.svg" alt="bugcap" width="96">
|
|
19
|
+
<img src="https://raw.githubusercontent.com/amenalahassa/bugcap/master/docs/assets/logo.svg" alt="bugcap" width="96">
|
|
19
20
|
</p>
|
|
20
21
|
|
|
21
22
|
<h1 align="center">bugcap</h1>
|
|
@@ -28,7 +29,7 @@ Dynamic: license-file
|
|
|
28
29
|
<p align="center">
|
|
29
30
|
<a href="https://github.com/amenalahassa/bugcap/actions/workflows/release.yml"><img alt="tests" src="https://github.com/amenalahassa/bugcap/actions/workflows/release.yml/badge.svg"></a>
|
|
30
31
|
<img alt="python" src="https://img.shields.io/badge/python-3.10%2B-3776ab?logo=python&logoColor=white">
|
|
31
|
-
<a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-green"></a>
|
|
32
|
+
<a href="https://github.com/amenalahassa/bugcap/blob/master/LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-green"></a>
|
|
32
33
|
<a href="https://amenalahassa.github.io/bugcap/"><img alt="docs" src="https://img.shields.io/badge/docs-website-d9480f"></a>
|
|
33
34
|
</p>
|
|
34
35
|
|
|
@@ -37,11 +38,11 @@ Dynamic: license-file
|
|
|
37
38
|
<a href="https://amenalahassa.github.io/bugcap/usage.html">Usage guide</a> ·
|
|
38
39
|
<a href="#quick-start">Quick start</a> ·
|
|
39
40
|
<a href="#connect-your-agent-mcp">Agents (MCP)</a> ·
|
|
40
|
-
<a href="CHANGELOG.md">Changelog</a>
|
|
41
|
+
<a href="https://github.com/amenalahassa/bugcap/blob/master/CHANGELOG.md">Changelog</a>
|
|
41
42
|
</p>
|
|
42
43
|
|
|
43
44
|
<p align="center">
|
|
44
|
-
<img src="docs/assets/demo.gif" alt="bugcap
|
|
45
|
+
<img src="https://raw.githubusercontent.com/amenalahassa/bugcap/master/docs/assets/demo.gif" alt="bugcap live: screenshot a broken page, file a bug with a note, then read it back with bugcap show" width="760">
|
|
45
46
|
</p>
|
|
46
47
|
|
|
47
48
|
---
|
|
@@ -82,6 +83,8 @@ bugcap dashboard --open # browse and triage in the browser
|
|
|
82
83
|
The [usage guide](https://amenalahassa.github.io/bugcap/usage.html) covers every command: images
|
|
83
84
|
and labels, recording, live mode, `@` references, GitHub pull and sync, and the MCP tools.
|
|
84
85
|
|
|
86
|
+
If a report ID is not found, the error suggests `bugcap list --all` to see report IDs across all repos.
|
|
87
|
+
|
|
85
88
|
## Connect your agent (MCP)
|
|
86
89
|
|
|
87
90
|
Requires the `mcp` extra. Then give your agent access to the reports:
|
|
@@ -97,7 +100,7 @@ Or in a project's `.mcp.json`:
|
|
|
97
100
|
```
|
|
98
101
|
|
|
99
102
|
The server exposes six tools: `list_reports`, `get_report` (returns the screenshot), `request_screenshot`,
|
|
100
|
-
`pull_issues`, `attach_image` and `update_notes`. Logs go to `logs/mcp-server.log` in the data
|
|
103
|
+
`pull_issues`, `attach_image`, `attach_file` and `update_notes`. Logs go to `logs/mcp-server.log` in the data
|
|
101
104
|
directory, never to stdout.
|
|
102
105
|
|
|
103
106
|
Ask your agent something like *"list the open bugcap reports for this repo and fix the one with
|
|
@@ -120,7 +123,7 @@ the error toast"*. It can read the picture because it's a local file, not a CDN
|
|
|
120
123
|
Working. Roadmap items 1–5, 7 and 8 are implemented and tested, along with spec 002 (images,
|
|
121
124
|
`@` references, recording, live mode and the dashboard). S3/R2 object storage (item 6) and other
|
|
122
125
|
trackers (item 9) are deferred. The `Destination` seam is in place, so they can be added without
|
|
123
|
-
changing the CLI. See [ROADMAP.md](ROADMAP.md).
|
|
126
|
+
changing the CLI. See [ROADMAP.md](https://github.com/amenalahassa/bugcap/blob/master/ROADMAP.md).
|
|
124
127
|
|
|
125
128
|
## Installation
|
|
126
129
|
|
|
@@ -146,13 +149,17 @@ pull and sync, MCP, and where data lives per OS.
|
|
|
146
149
|
|
|
147
150
|
Notes on the more involved behaviour:
|
|
148
151
|
|
|
149
|
-
- **References.** In a note, `@i1` is image 1, `@v2` a video, `@g3` an animated GIF, `@f4` a frame set, and `@login-error` a labelled item. `#3` points at report 3 (a missing one is kept as text with a warning). Media references are validated on save, and relabelling or removing a referenced item needs `--force`.
|
|
152
|
+
- **References.** In a note, `@i1` is image 1, `@v2` a video, `@g3` an animated GIF, `@f4` a frame set, `@d5` an attached file (`attach --file`, any type), and `@login-error` a labelled item. `#3` points at report 3 (a missing one is kept as text with a warning). Media references are validated on save, and relabelling or removing a referenced item needs `--force`.
|
|
150
153
|
- **Repo values** (`init`, `sync`, `config repo set`) are checked for format and, with `gh`, for existence. Changing the repo identity offers to move existing reports.
|
|
151
154
|
- **Media sync** over `[sync] max_upload_mb` (default 25) is skipped with a warning.
|
|
152
155
|
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
Contributions are welcome. Read [CONTRIBUTING.md](https://github.com/amenalahassa/bugcap/blob/master/CONTRIBUTING.md) for the workflow and project rules, and the [Code of Conduct](https://github.com/amenalahassa/bugcap/blob/master/CODE_OF_CONDUCT.md). Report security issues privately, see [SECURITY.md](https://github.com/amenalahassa/bugcap/blob/master/SECURITY.md).
|
|
159
|
+
|
|
153
160
|
## Development
|
|
154
161
|
|
|
155
|
-
See [DEVELOPMENT.md](DEVELOPMENT.md). Run the tests with:
|
|
162
|
+
See [DEVELOPMENT.md](https://github.com/amenalahassa/bugcap/blob/master/DEVELOPMENT.md). Run the tests with:
|
|
156
163
|
|
|
157
164
|
```bash
|
|
158
165
|
uv run --extra dev --extra mcp pytest -q
|
|
@@ -203,7 +210,7 @@ succeeds, so a re-run skips what is already recorded.
|
|
|
203
210
|
|
|
204
211
|
## Roadmap
|
|
205
212
|
|
|
206
|
-
See [ROADMAP.md](ROADMAP.md).
|
|
213
|
+
See [ROADMAP.md](https://github.com/amenalahassa/bugcap/blob/master/ROADMAP.md).
|
|
207
214
|
|
|
208
215
|
## Design notes / deliberate non-goals
|
|
209
216
|
|
|
@@ -222,4 +229,4 @@ See [ROADMAP.md](ROADMAP.md).
|
|
|
222
229
|
|
|
223
230
|
## License
|
|
224
231
|
|
|
225
|
-
MIT — see [LICENSE](LICENSE).
|
|
232
|
+
MIT — see [LICENSE](https://github.com/amenalahassa/bugcap/blob/master/LICENSE).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/logo.svg" alt="bugcap" width="96">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/amenalahassa/bugcap/master/docs/assets/logo.svg" alt="bugcap" width="96">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">bugcap</h1>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<a href="https://github.com/amenalahassa/bugcap/actions/workflows/release.yml"><img alt="tests" src="https://github.com/amenalahassa/bugcap/actions/workflows/release.yml/badge.svg"></a>
|
|
14
14
|
<img alt="python" src="https://img.shields.io/badge/python-3.10%2B-3776ab?logo=python&logoColor=white">
|
|
15
|
-
<a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-green"></a>
|
|
15
|
+
<a href="https://github.com/amenalahassa/bugcap/blob/master/LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-green"></a>
|
|
16
16
|
<a href="https://amenalahassa.github.io/bugcap/"><img alt="docs" src="https://img.shields.io/badge/docs-website-d9480f"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
<a href="https://amenalahassa.github.io/bugcap/usage.html">Usage guide</a> ·
|
|
22
22
|
<a href="#quick-start">Quick start</a> ·
|
|
23
23
|
<a href="#connect-your-agent-mcp">Agents (MCP)</a> ·
|
|
24
|
-
<a href="CHANGELOG.md">Changelog</a>
|
|
24
|
+
<a href="https://github.com/amenalahassa/bugcap/blob/master/CHANGELOG.md">Changelog</a>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
27
|
<p align="center">
|
|
28
|
-
<img src="docs/assets/demo.gif" alt="bugcap
|
|
28
|
+
<img src="https://raw.githubusercontent.com/amenalahassa/bugcap/master/docs/assets/demo.gif" alt="bugcap live: screenshot a broken page, file a bug with a note, then read it back with bugcap show" width="760">
|
|
29
29
|
</p>
|
|
30
30
|
|
|
31
31
|
---
|
|
@@ -66,6 +66,8 @@ bugcap dashboard --open # browse and triage in the browser
|
|
|
66
66
|
The [usage guide](https://amenalahassa.github.io/bugcap/usage.html) covers every command: images
|
|
67
67
|
and labels, recording, live mode, `@` references, GitHub pull and sync, and the MCP tools.
|
|
68
68
|
|
|
69
|
+
If a report ID is not found, the error suggests `bugcap list --all` to see report IDs across all repos.
|
|
70
|
+
|
|
69
71
|
## Connect your agent (MCP)
|
|
70
72
|
|
|
71
73
|
Requires the `mcp` extra. Then give your agent access to the reports:
|
|
@@ -81,7 +83,7 @@ Or in a project's `.mcp.json`:
|
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
The server exposes six tools: `list_reports`, `get_report` (returns the screenshot), `request_screenshot`,
|
|
84
|
-
`pull_issues`, `attach_image` and `update_notes`. Logs go to `logs/mcp-server.log` in the data
|
|
86
|
+
`pull_issues`, `attach_image`, `attach_file` and `update_notes`. Logs go to `logs/mcp-server.log` in the data
|
|
85
87
|
directory, never to stdout.
|
|
86
88
|
|
|
87
89
|
Ask your agent something like *"list the open bugcap reports for this repo and fix the one with
|
|
@@ -104,7 +106,7 @@ the error toast"*. It can read the picture because it's a local file, not a CDN
|
|
|
104
106
|
Working. Roadmap items 1–5, 7 and 8 are implemented and tested, along with spec 002 (images,
|
|
105
107
|
`@` references, recording, live mode and the dashboard). S3/R2 object storage (item 6) and other
|
|
106
108
|
trackers (item 9) are deferred. The `Destination` seam is in place, so they can be added without
|
|
107
|
-
changing the CLI. See [ROADMAP.md](ROADMAP.md).
|
|
109
|
+
changing the CLI. See [ROADMAP.md](https://github.com/amenalahassa/bugcap/blob/master/ROADMAP.md).
|
|
108
110
|
|
|
109
111
|
## Installation
|
|
110
112
|
|
|
@@ -130,13 +132,17 @@ pull and sync, MCP, and where data lives per OS.
|
|
|
130
132
|
|
|
131
133
|
Notes on the more involved behaviour:
|
|
132
134
|
|
|
133
|
-
- **References.** In a note, `@i1` is image 1, `@v2` a video, `@g3` an animated GIF, `@f4` a frame set, and `@login-error` a labelled item. `#3` points at report 3 (a missing one is kept as text with a warning). Media references are validated on save, and relabelling or removing a referenced item needs `--force`.
|
|
135
|
+
- **References.** In a note, `@i1` is image 1, `@v2` a video, `@g3` an animated GIF, `@f4` a frame set, `@d5` an attached file (`attach --file`, any type), and `@login-error` a labelled item. `#3` points at report 3 (a missing one is kept as text with a warning). Media references are validated on save, and relabelling or removing a referenced item needs `--force`.
|
|
134
136
|
- **Repo values** (`init`, `sync`, `config repo set`) are checked for format and, with `gh`, for existence. Changing the repo identity offers to move existing reports.
|
|
135
137
|
- **Media sync** over `[sync] max_upload_mb` (default 25) is skipped with a warning.
|
|
136
138
|
|
|
139
|
+
## Contributing
|
|
140
|
+
|
|
141
|
+
Contributions are welcome. Read [CONTRIBUTING.md](https://github.com/amenalahassa/bugcap/blob/master/CONTRIBUTING.md) for the workflow and project rules, and the [Code of Conduct](https://github.com/amenalahassa/bugcap/blob/master/CODE_OF_CONDUCT.md). Report security issues privately, see [SECURITY.md](https://github.com/amenalahassa/bugcap/blob/master/SECURITY.md).
|
|
142
|
+
|
|
137
143
|
## Development
|
|
138
144
|
|
|
139
|
-
See [DEVELOPMENT.md](DEVELOPMENT.md). Run the tests with:
|
|
145
|
+
See [DEVELOPMENT.md](https://github.com/amenalahassa/bugcap/blob/master/DEVELOPMENT.md). Run the tests with:
|
|
140
146
|
|
|
141
147
|
```bash
|
|
142
148
|
uv run --extra dev --extra mcp pytest -q
|
|
@@ -187,7 +193,7 @@ succeeds, so a re-run skips what is already recorded.
|
|
|
187
193
|
|
|
188
194
|
## Roadmap
|
|
189
195
|
|
|
190
|
-
See [ROADMAP.md](ROADMAP.md).
|
|
196
|
+
See [ROADMAP.md](https://github.com/amenalahassa/bugcap/blob/master/ROADMAP.md).
|
|
191
197
|
|
|
192
198
|
## Design notes / deliberate non-goals
|
|
193
199
|
|
|
@@ -206,4 +212,4 @@ See [ROADMAP.md](ROADMAP.md).
|
|
|
206
212
|
|
|
207
213
|
## License
|
|
208
214
|
|
|
209
|
-
MIT — see [LICENSE](LICENSE).
|
|
215
|
+
MIT — see [LICENSE](https://github.com/amenalahassa/bugcap/blob/master/LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bugcap"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Local-first, agent-readable bug capture: annotated screenshots + notes, stored locally, synced to trackers on your own schedule."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ dependencies = ["tomli>=2; python_version < '3.11'"]
|
|
|
14
14
|
|
|
15
15
|
[project.optional-dependencies]
|
|
16
16
|
mcp = ["mcp>=1.2,<2"]
|
|
17
|
-
dev = ["pytest>=7"]
|
|
17
|
+
dev = ["pytest>=7", "ruff>=0.6"]
|
|
18
18
|
|
|
19
19
|
[project.scripts]
|
|
20
20
|
bugcap = "bugcap.cli:main"
|
|
@@ -27,3 +27,13 @@ bugcap = ["dashboard/static/*"]
|
|
|
27
27
|
|
|
28
28
|
[tool.pytest.ini_options]
|
|
29
29
|
testpaths = ["tests"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 120
|
|
33
|
+
target-version = "py310"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint.per-file-ignores]
|
|
39
|
+
"tests/*" = ["E702", "E731"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -97,10 +97,19 @@ def get_report(store: Store, id: int) -> dict:
|
|
|
97
97
|
|
|
98
98
|
def attach_image(store: Store, id: int, sources: list[str], labels: Optional[list] = None) -> dict:
|
|
99
99
|
"""Attach images (paths, globs, http(s) URLs) to a report; partial batches are reported."""
|
|
100
|
+
return _attach(store, id, sources, labels, service.add_media)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def attach_file(store: Store, id: int, sources: list[str], labels: Optional[list] = None) -> dict:
|
|
104
|
+
"""Attach files of any type (paths, globs, http(s) URLs) to a report."""
|
|
105
|
+
return _attach(store, id, sources, labels, service.add_files)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _attach(store: Store, id: int, sources: list[str], labels, add) -> dict:
|
|
100
109
|
if not sources or len(sources) > 20:
|
|
101
110
|
return {"code": "bad_query", "error": "sources must contain 1 to 20 entries"}
|
|
102
111
|
try:
|
|
103
|
-
result =
|
|
112
|
+
result = add(store, id, list(sources), labels)
|
|
104
113
|
except ServiceError as exc:
|
|
105
114
|
return {"error": exc.message, **exc.as_dict()}
|
|
106
115
|
return {
|
|
@@ -95,8 +95,51 @@ def by_name(name: str) -> Backend:
|
|
|
95
95
|
raise KeyError(name)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def _sync_path_from_registry() -> None:
|
|
99
|
+
"""Pick up a freshly-installed Windows tool without a new terminal.
|
|
100
|
+
|
|
101
|
+
winget/scoop/choco update the user/machine PATH in the registry, but an
|
|
102
|
+
already-running process (and any shell that was already open when the
|
|
103
|
+
install happened) keeps its original PATH until it restarts. That made
|
|
104
|
+
`bugcap live` report "No capture tool found" right after `bugcap setup`
|
|
105
|
+
had just installed flameshot in the same session. Re-reading PATH from
|
|
106
|
+
the registry before every detection closes that gap. Best-effort: any
|
|
107
|
+
failure here just leaves PATH as-is.
|
|
108
|
+
"""
|
|
109
|
+
if sys.platform != "win32":
|
|
110
|
+
return
|
|
111
|
+
try:
|
|
112
|
+
import winreg
|
|
113
|
+
except ImportError:
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
chunks = [os.environ.get("PATH", "")]
|
|
117
|
+
for hive, subkey in (
|
|
118
|
+
(winreg.HKEY_CURRENT_USER, r"Environment"),
|
|
119
|
+
(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"),
|
|
120
|
+
):
|
|
121
|
+
try:
|
|
122
|
+
with winreg.OpenKey(hive, subkey) as key:
|
|
123
|
+
value, _ = winreg.QueryValueEx(key, "Path")
|
|
124
|
+
chunks.append(value)
|
|
125
|
+
except OSError:
|
|
126
|
+
continue
|
|
127
|
+
seen = set()
|
|
128
|
+
merged = []
|
|
129
|
+
for chunk in chunks:
|
|
130
|
+
for part in chunk.split(os.pathsep):
|
|
131
|
+
key = part.lower()
|
|
132
|
+
if part and key not in seen:
|
|
133
|
+
seen.add(key)
|
|
134
|
+
merged.append(part)
|
|
135
|
+
os.environ["PATH"] = os.pathsep.join(merged)
|
|
136
|
+
except OSError:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
98
140
|
def detect() -> Optional[Backend]:
|
|
99
141
|
"""First available backend, in preference order."""
|
|
142
|
+
_sync_path_from_registry()
|
|
100
143
|
return next((b for b in BACKENDS if b.available()), None)
|
|
101
144
|
|
|
102
145
|
|
|
@@ -7,13 +7,12 @@ import threading
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
from . import backends, capture, config, ghcli, recorder, refs, repo, service, sync, validation
|
|
10
|
+
from . import __version__, backends, capture, config, ghcli, recorder, refs, repo, service, sync, validation
|
|
11
11
|
from .capture import CaptureError
|
|
12
12
|
from .errors import ServiceError
|
|
13
13
|
from .ghcli import GhError
|
|
14
14
|
from .store import STATUSES, Store
|
|
15
15
|
|
|
16
|
-
|
|
17
16
|
# --- shared helpers (T014) ----------------------------------------------------
|
|
18
17
|
|
|
19
18
|
def current_repo():
|
|
@@ -25,7 +24,10 @@ def resolve_report(store: Store, report_id: int):
|
|
|
25
24
|
"""Return the report, or print the standard not-found error and return None."""
|
|
26
25
|
report = store.get(report_id)
|
|
27
26
|
if report is None:
|
|
28
|
-
print(
|
|
27
|
+
print(
|
|
28
|
+
f"error: no report with id {report_id}; run `bugcap list --all` to see report ids",
|
|
29
|
+
file=sys.stderr,
|
|
30
|
+
)
|
|
29
31
|
return report
|
|
30
32
|
|
|
31
33
|
|
|
@@ -41,7 +43,7 @@ def print_service_error(exc: ServiceError) -> None:
|
|
|
41
43
|
def print_ingest(added, rejected) -> None:
|
|
42
44
|
"""One line per input: `added #<idx> ...` on stdout, `skipped ...` on stderr."""
|
|
43
45
|
for media in added:
|
|
44
|
-
name = os.path.basename(media.path or "")
|
|
46
|
+
name = os.path.basename((media.source if media.kind == "file" and media.source else media.path) or "")
|
|
45
47
|
print(f"added {refs.media_token(media)} {name} ({media.kind}, {human_size(media.size_bytes)})")
|
|
46
48
|
for item in rejected:
|
|
47
49
|
print(f"skipped {item['source']}: {item['reason']}", file=sys.stderr)
|
|
@@ -161,7 +163,7 @@ def print_report(report) -> None:
|
|
|
161
163
|
print(f" repo: {report.repo}")
|
|
162
164
|
print(f" tags: {', '.join(report.tags) or '(none)'}")
|
|
163
165
|
print(f" notes: {refs.display_notes(report.notes, report.media) or '(none)'}")
|
|
164
|
-
print(
|
|
166
|
+
print(" images:")
|
|
165
167
|
for path in report.image_paths:
|
|
166
168
|
print(f" - {path}")
|
|
167
169
|
detailed = [m for m in report.media if not (m.kind == "image" and m.source == "legacy")]
|
|
@@ -171,7 +173,7 @@ def print_report(report) -> None:
|
|
|
171
173
|
where = m.path or f"({len(m.frames)} frames)"
|
|
172
174
|
origin = f" (source: {m.source})" if m.source else ""
|
|
173
175
|
print(f" {refs.media_token(m)} {m.kind:8s} {m.label or '-':12s} {where} {human_size(m.size_bytes)}{origin}")
|
|
174
|
-
print(
|
|
176
|
+
print(" synced_refs:")
|
|
175
177
|
if report.synced_refs:
|
|
176
178
|
for tracker, ref in report.synced_refs.items():
|
|
177
179
|
print(f" - {tracker}: {ref}")
|
|
@@ -226,7 +228,13 @@ def _setup_capture(args: argparse.Namespace) -> int:
|
|
|
226
228
|
if rc != 0:
|
|
227
229
|
print(f"error: install command failed (exit {rc}).", file=sys.stderr)
|
|
228
230
|
return 1
|
|
229
|
-
|
|
231
|
+
if backends.detect() is not None:
|
|
232
|
+
print(f"{rec.name} installed.")
|
|
233
|
+
return 0
|
|
234
|
+
print(
|
|
235
|
+
f"{rec.name} installed, but this terminal can't see it on PATH yet.\n"
|
|
236
|
+
f"Close and reopen your terminal (or start a new one), then run `bugcap setup` again."
|
|
237
|
+
)
|
|
230
238
|
return 0
|
|
231
239
|
|
|
232
240
|
|
|
@@ -537,14 +545,22 @@ def cmd_attach(args: argparse.Namespace) -> int:
|
|
|
537
545
|
added: list = []
|
|
538
546
|
rejected: list = []
|
|
539
547
|
shot_path = None
|
|
540
|
-
if args.image:
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
+
if args.image or args.file:
|
|
549
|
+
batches = (
|
|
550
|
+
(args.image, service.add_media, labels),
|
|
551
|
+
(args.file, service.add_files, list(args.file_label or [])),
|
|
552
|
+
)
|
|
553
|
+
for sources, add, batch_labels in batches:
|
|
554
|
+
if not sources:
|
|
555
|
+
continue
|
|
556
|
+
try:
|
|
557
|
+
result = add(store, args.id, list(sources), batch_labels)
|
|
558
|
+
except ServiceError as exc:
|
|
559
|
+
print_service_error(exc)
|
|
560
|
+
return 1
|
|
561
|
+
added += result.added
|
|
562
|
+
rejected += result.rejected
|
|
563
|
+
print_ingest(result.added, result.rejected)
|
|
548
564
|
else:
|
|
549
565
|
try:
|
|
550
566
|
shot_path = capture.capture_screenshot()
|
|
@@ -569,7 +585,7 @@ def cmd_attach(args: argparse.Namespace) -> int:
|
|
|
569
585
|
return 1
|
|
570
586
|
current = store.get(args.id)
|
|
571
587
|
_warn_report_refs(store, current.notes)
|
|
572
|
-
if args.image:
|
|
588
|
+
if args.image or args.file:
|
|
573
589
|
try:
|
|
574
590
|
refs.validate_references(current.notes, current.media)
|
|
575
591
|
except ServiceError as exc:
|
|
@@ -850,6 +866,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
850
866
|
prog="bugcap",
|
|
851
867
|
description="Local-first, agent-readable bug capture: annotated screenshots + notes.",
|
|
852
868
|
)
|
|
869
|
+
parser.add_argument("--version", action="version", version=f"bugcap {__version__}")
|
|
853
870
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
854
871
|
|
|
855
872
|
p = sub.add_parser("setup", help="Detect a capture tool, or recommend/install one.")
|
|
@@ -901,12 +918,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
901
918
|
p.add_argument("tags", nargs="+")
|
|
902
919
|
p.set_defaults(func=cmd_tag)
|
|
903
920
|
|
|
904
|
-
p = sub.add_parser("attach", help="Attach a screenshot (captured or imported) to a report.")
|
|
921
|
+
p = sub.add_parser("attach", help="Attach a screenshot (captured or imported) or any file to a report.")
|
|
905
922
|
p.add_argument("id", type=int)
|
|
906
923
|
p.add_argument("--image", action="append", metavar="SRC",
|
|
907
924
|
help="Import an image (path, glob or http(s) URL) instead of launching a capture tool; repeatable.")
|
|
908
925
|
p.add_argument("--label", action="append", help="Label for the --image at the same position (repeatable).")
|
|
909
|
-
p.add_argument("--
|
|
926
|
+
p.add_argument("--file", action="append", metavar="SRC",
|
|
927
|
+
help="Attach a file of any type (path, glob or http(s) URL); repeatable. "
|
|
928
|
+
"Refer to it in notes as @d1, @d2, ... or by label.")
|
|
929
|
+
p.add_argument("--file-label", action="append", help="Label for the --file at the same position (repeatable).")
|
|
930
|
+
p.add_argument("--note", help="Text appended to the report's notes, tied to the item(s) added here.")
|
|
910
931
|
p.set_defaults(func=cmd_attach)
|
|
911
932
|
|
|
912
933
|
p = sub.add_parser("images", help="List a report's images, or relabel/remove one.")
|
|
@@ -982,7 +1003,27 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
982
1003
|
return parser
|
|
983
1004
|
|
|
984
1005
|
|
|
1006
|
+
def _use_utf8_streams() -> None:
|
|
1007
|
+
"""Make stdout/stderr tolerant of report text the console's codepage can't show.
|
|
1008
|
+
|
|
1009
|
+
Report titles and notes are free-form user text and may contain characters
|
|
1010
|
+
(accents, emoji, …) outside the active console codepage. On Windows that
|
|
1011
|
+
codepage is often cp1252, so a bare `print()` of such text raises
|
|
1012
|
+
UnicodeEncodeError and crashes the CLI (e.g. `bugcap show`). Reconfiguring
|
|
1013
|
+
to UTF-8 with 'replace' keeps the command running and shows readable output
|
|
1014
|
+
instead of a traceback.
|
|
1015
|
+
"""
|
|
1016
|
+
for stream in (sys.stdout, sys.stderr):
|
|
1017
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
1018
|
+
if reconfigure is not None:
|
|
1019
|
+
try:
|
|
1020
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
1021
|
+
except (ValueError, OSError):
|
|
1022
|
+
pass
|
|
1023
|
+
|
|
1024
|
+
|
|
985
1025
|
def main(argv: list[str] | None = None) -> int:
|
|
1026
|
+
_use_utf8_streams()
|
|
986
1027
|
parser = build_parser()
|
|
987
1028
|
args = parser.parse_args(argv)
|
|
988
1029
|
return args.func(args)
|
|
@@ -31,8 +31,10 @@ def parse_range(header: str | None, size: int):
|
|
|
31
31
|
return start, min(end, size - 1)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def serve_media(handler, rel_path, mime: str, range_header: str | None) -> None:
|
|
35
|
-
"""`rel_path` is the stored data-relative path; it must resolve inside
|
|
34
|
+
def serve_media(handler, rel_path, mime: str, range_header: str | None, download_name: str | None = None) -> None:
|
|
35
|
+
"""`rel_path` is the stored data-relative path; it must resolve inside the media store.
|
|
36
|
+
With `download_name` (attached files of any type) the bytes are always sent as an opaque
|
|
37
|
+
download, never rendered, so an uploaded HTML or SVG file cannot run on the dashboard origin."""
|
|
36
38
|
try:
|
|
37
39
|
path = resolve_data_path(rel_path)
|
|
38
40
|
if not path.is_file():
|
|
@@ -53,7 +55,12 @@ def serve_media(handler, rel_path, mime: str, range_header: str | None) -> None:
|
|
|
53
55
|
start, end = rng if rng else (0, size - 1)
|
|
54
56
|
handler.send_response(206 if rng else 200)
|
|
55
57
|
handler.send_security_headers()
|
|
56
|
-
|
|
58
|
+
if download_name is not None:
|
|
59
|
+
safe = re.sub(r"[^A-Za-z0-9._-]", "_", download_name)[:100] or "file"
|
|
60
|
+
handler.send_header("Content-Type", "application/octet-stream")
|
|
61
|
+
handler.send_header("Content-Disposition", f'attachment; filename="{safe}"')
|
|
62
|
+
else:
|
|
63
|
+
handler.send_header("Content-Type", mime)
|
|
57
64
|
handler.send_header("Accept-Ranges", "bytes")
|
|
58
65
|
handler.send_header("Cache-Control", "private, max-age=3600")
|
|
59
66
|
handler.send_header("Content-Length", str(end - start + 1) if size else "0")
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import hmac
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
10
11
|
import re
|
|
11
12
|
import secrets
|
|
12
13
|
import sys
|
|
@@ -182,7 +183,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
182
183
|
if frame_no is None:
|
|
183
184
|
if media.kind == "frames" or not media.path:
|
|
184
185
|
return self.send_json(404, {"code": "not_found", "message": "media not found"})
|
|
185
|
-
|
|
186
|
+
name = None
|
|
187
|
+
if media.kind == "file":
|
|
188
|
+
name = os.path.basename((media.source or "").replace("\\", "/")) or os.path.basename(media.path)
|
|
189
|
+
return serve_media(self, media.path, media.mime, self.headers.get("Range"), name)
|
|
186
190
|
frame = next((f for f in media.frames if f.frame_no == frame_no), None)
|
|
187
191
|
if frame is None:
|
|
188
192
|
return self.send_json(404, {"code": "not_found", "message": "frame not found"})
|
|
@@ -90,11 +90,15 @@
|
|
|
90
90
|
var img = document.createElement("img");
|
|
91
91
|
img.src = f.url; img.alt = "frame " + f.frame_no; fig.appendChild(img);
|
|
92
92
|
});
|
|
93
|
+
} else if (m.kind === "file") {
|
|
94
|
+
var a = document.createElement("a");
|
|
95
|
+
a.href = m.url; a.download = ""; a.textContent = "Download " + (m.label || ("file " + m.index));
|
|
96
|
+
fig.appendChild(a);
|
|
93
97
|
} else {
|
|
94
98
|
var i = document.createElement("img");
|
|
95
99
|
i.src = m.url; i.alt = m.label || ("image " + m.index); fig.appendChild(i);
|
|
96
100
|
}
|
|
97
|
-
var letters = {image: "i", video: "v", animated: "g", frames: "f"};
|
|
101
|
+
var letters = {image: "i", video: "v", animated: "g", frames: "f", file: "d"};
|
|
98
102
|
var label = caption || ("@" + (letters[m.kind] || "i") + m.index);
|
|
99
103
|
if (m.label) label += " (" + m.label + ")";
|
|
100
104
|
fig.appendChild(el("figcaption", label + " · " + m.kind + " · " + Math.round(m.size_bytes / 1024) + " KB"));
|