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.
Files changed (85) hide show
  1. {bugcap-0.5.0/src/bugcap.egg-info → bugcap-0.6.0}/PKG-INFO +18 -11
  2. {bugcap-0.5.0 → bugcap-0.6.0}/README.md +16 -10
  3. {bugcap-0.5.0 → bugcap-0.6.0}/pyproject.toml +12 -2
  4. bugcap-0.6.0/src/bugcap/__init__.py +1 -0
  5. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/agent_api.py +10 -1
  6. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/backends.py +43 -0
  7. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/cli.py +59 -18
  8. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/media.py +10 -3
  9. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/server.py +5 -1
  10. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/static/app.js +5 -1
  11. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/ingest.py +93 -10
  12. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/live.py +69 -9
  13. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/live_session.py +41 -8
  14. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/mcp_server.py +28 -5
  15. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/paths.py +7 -1
  16. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/refs.py +5 -4
  17. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/repo.py +65 -2
  18. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/service.py +27 -6
  19. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/store.py +38 -4
  20. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/sync.py +3 -0
  21. {bugcap-0.5.0 → bugcap-0.6.0/src/bugcap.egg-info}/PKG-INFO +18 -11
  22. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/SOURCES.txt +1 -0
  23. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/requires.txt +1 -0
  24. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_backends.py +57 -1
  25. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_init_capture_list.py +1 -1
  26. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_live_headless.py +5 -1
  27. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_media.py +2 -2
  28. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_no_image_delete.py +1 -1
  29. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_setup.py +38 -0
  30. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_triage.py +4 -2
  31. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_drafts.py +2 -1
  32. bugcap-0.6.0/tests/test_file_attachments.py +217 -0
  33. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_github_media_sync.py +2 -1
  34. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_github_sync.py +1 -1
  35. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_ingest.py +4 -2
  36. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_mcp.py +1 -2
  37. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_mcp_logging.py +13 -1
  38. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_run.py +3 -3
  39. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_refs_validate_rewrite.py +3 -1
  40. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_regression_output.py +17 -1
  41. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_repo_config.py +46 -0
  42. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_repo_values.py +0 -1
  43. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_store_migration.py +62 -7
  44. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_sync_consent.py +0 -3
  45. bugcap-0.5.0/src/bugcap/__init__.py +0 -1
  46. {bugcap-0.5.0 → bugcap-0.6.0}/LICENSE +0 -0
  47. {bugcap-0.5.0 → bugcap-0.6.0}/setup.cfg +0 -0
  48. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/capture.py +0 -0
  49. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/config.py +0 -0
  50. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/__init__.py +0 -0
  51. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/api.py +0 -0
  52. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/static/app.css +0 -0
  53. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/dashboard/static/index.html +0 -0
  54. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/drafts.py +0 -0
  55. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/errors.py +0 -0
  56. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/ghcli.py +0 -0
  57. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/logs.py +0 -0
  58. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/recorder.py +0 -0
  59. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/tomlio.py +0 -0
  60. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap/validation.py +0 -0
  61. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/dependency_links.txt +0 -0
  62. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/entry_points.txt +0 -0
  63. {bugcap-0.5.0 → bugcap-0.6.0}/src/bugcap.egg-info/top_level.txt +0 -0
  64. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_agent_api.py +0 -0
  65. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_capture.py +0 -0
  66. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_attach.py +1 -1
  67. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_dashboard.py +0 -0
  68. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_cli_record.py +0 -0
  69. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_api.py +0 -0
  70. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_media.py +0 -0
  71. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_mutations.py +0 -0
  72. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_security.py +0 -0
  73. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_dashboard_ui_static.py +0 -0
  74. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_ghcli.py +0 -0
  75. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_github_pull.py +0 -0
  76. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_image_refs.py +0 -0
  77. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_ingest_url.py +0 -0
  78. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_live_session.py +1 -1
  79. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_paths.py +0 -0
  80. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_argv.py +0 -0
  81. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_devices.py +0 -0
  82. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_recorder_media_real.py +0 -0
  83. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_refs.py +0 -0
  84. {bugcap-0.5.0 → bugcap-0.6.0}/tests/test_service_media.py +0 -0
  85. {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.5.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 capturing an annotated bug and listing the report" width="760">
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 capturing an annotated bug and listing the report" width="760">
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.5.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 = service.add_media(store, id, list(sources), labels)
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(f"error: no report with id {report_id}", file=sys.stderr)
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(f" images:")
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(f" synced_refs:")
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
- print(f"{rec.name} installed.")
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
- try:
542
- result = service.add_media(store, args.id, list(args.image), labels)
543
- except ServiceError as exc:
544
- print_service_error(exc)
545
- return 1
546
- added, rejected = result.added, result.rejected
547
- print_ingest(added, rejected)
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("--note", help="Text appended to the report's notes, tied to the image(s) added here.")
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 images/ or media/."""
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
- handler.send_header("Content-Type", mime)
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
- return serve_media(self, media.path, media.mime, self.headers.get("Range"))
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"));