bugcap 0.3.0__tar.gz → 0.4.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 (89) hide show
  1. {bugcap-0.3.0/src/bugcap.egg-info → bugcap-0.4.0}/PKG-INFO +2 -2
  2. {bugcap-0.3.0 → bugcap-0.4.0}/README.md +1 -1
  3. {bugcap-0.3.0 → bugcap-0.4.0}/pyproject.toml +1 -1
  4. bugcap-0.4.0/src/bugcap/__init__.py +1 -0
  5. bugcap-0.4.0/src/bugcap/drafts.py +134 -0
  6. bugcap-0.4.0/src/bugcap/live.py +564 -0
  7. bugcap-0.4.0/src/bugcap/live_session.py +300 -0
  8. {bugcap-0.3.0 → bugcap-0.4.0/src/bugcap.egg-info}/PKG-INFO +2 -2
  9. bugcap-0.4.0/tests/test_drafts.py +77 -0
  10. bugcap-0.4.0/tests/test_live_session.py +289 -0
  11. bugcap-0.3.0/src/bugcap/__init__.py +0 -1
  12. bugcap-0.3.0/src/bugcap/drafts.py +0 -83
  13. bugcap-0.3.0/src/bugcap/live.py +0 -350
  14. bugcap-0.3.0/src/bugcap/live_session.py +0 -125
  15. bugcap-0.3.0/tests/test_drafts.py +0 -43
  16. bugcap-0.3.0/tests/test_live_session.py +0 -125
  17. {bugcap-0.3.0 → bugcap-0.4.0}/LICENSE +0 -0
  18. {bugcap-0.3.0 → bugcap-0.4.0}/setup.cfg +0 -0
  19. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/agent_api.py +0 -0
  20. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/backends.py +0 -0
  21. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/capture.py +0 -0
  22. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/cli.py +0 -0
  23. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/config.py +0 -0
  24. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/dashboard/__init__.py +0 -0
  25. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/dashboard/api.py +0 -0
  26. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/dashboard/media.py +0 -0
  27. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/dashboard/server.py +0 -0
  28. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/dashboard/static/app.css +0 -0
  29. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/dashboard/static/app.js +0 -0
  30. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/dashboard/static/index.html +0 -0
  31. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/errors.py +0 -0
  32. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/ghcli.py +0 -0
  33. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/ingest.py +0 -0
  34. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/logs.py +0 -0
  35. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/mcp_server.py +0 -0
  36. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/paths.py +0 -0
  37. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/recorder.py +0 -0
  38. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/refs.py +0 -0
  39. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/repo.py +0 -0
  40. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/service.py +0 -0
  41. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/store.py +0 -0
  42. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/sync.py +0 -0
  43. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/tomlio.py +0 -0
  44. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap/validation.py +0 -0
  45. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap.egg-info/SOURCES.txt +0 -0
  46. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap.egg-info/dependency_links.txt +0 -0
  47. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap.egg-info/entry_points.txt +0 -0
  48. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap.egg-info/requires.txt +0 -0
  49. {bugcap-0.3.0 → bugcap-0.4.0}/src/bugcap.egg-info/top_level.txt +0 -0
  50. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_agent_api.py +0 -0
  51. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_backends.py +0 -0
  52. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_capture.py +0 -0
  53. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_attach.py +0 -0
  54. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_dashboard.py +0 -0
  55. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_init_capture_list.py +0 -0
  56. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_live_headless.py +0 -0
  57. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_media.py +0 -0
  58. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_no_image_delete.py +0 -0
  59. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_record.py +0 -0
  60. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_setup.py +0 -0
  61. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_cli_triage.py +0 -0
  62. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_dashboard_api.py +0 -0
  63. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_dashboard_media.py +0 -0
  64. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_dashboard_mutations.py +0 -0
  65. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_dashboard_security.py +0 -0
  66. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_dashboard_ui_static.py +0 -0
  67. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_ghcli.py +0 -0
  68. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_github_media_sync.py +0 -0
  69. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_github_pull.py +0 -0
  70. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_github_sync.py +0 -0
  71. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_image_refs.py +0 -0
  72. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_ingest.py +0 -0
  73. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_ingest_url.py +0 -0
  74. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_mcp.py +0 -0
  75. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_mcp_logging.py +0 -0
  76. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_paths.py +0 -0
  77. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_recorder_argv.py +0 -0
  78. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_recorder_devices.py +0 -0
  79. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_recorder_media_real.py +0 -0
  80. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_recorder_run.py +0 -0
  81. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_refs.py +0 -0
  82. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_refs_validate_rewrite.py +0 -0
  83. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_regression_output.py +0 -0
  84. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_repo_config.py +0 -0
  85. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_repo_values.py +0 -0
  86. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_service_media.py +0 -0
  87. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_service_refs.py +0 -0
  88. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_store_migration.py +0 -0
  89. {bugcap-0.3.0 → bugcap-0.4.0}/tests/test_sync_consent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bugcap
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -63,7 +63,7 @@ bugcap keeps the whole loop in one place:
63
63
  | 🔁 **Sync when ready** | Push to a GitHub issue with the usual inline image, plus a plain copy agents can read. |
64
64
  | 🎞️ **Record what happens** | Short screen recordings as animated GIF, video or keyframes, with size caps. |
65
65
  | 🖥️ **Triage in a browser** | A local dashboard on `127.0.0.1` to browse, filter and set statuses. |
66
- | 🪟 **Live mode** | An always-on-top window for reporting many bugs in a row. |
66
+ | 🪟 **Live mode** | An always-on-top window (top-right) to capture or record, stage several items, then file a new bug, add to an existing one, or log a note-only bug. |
67
67
 
68
68
  ## Quick start
69
69
 
@@ -47,7 +47,7 @@ bugcap keeps the whole loop in one place:
47
47
  | 🔁 **Sync when ready** | Push to a GitHub issue with the usual inline image, plus a plain copy agents can read. |
48
48
  | 🎞️ **Record what happens** | Short screen recordings as animated GIF, video or keyframes, with size caps. |
49
49
  | 🖥️ **Triage in a browser** | A local dashboard on `127.0.0.1` to browse, filter and set statuses. |
50
- | 🪟 **Live mode** | An always-on-top window for reporting many bugs in a row. |
50
+ | 🪟 **Live mode** | An always-on-top window (top-right) to capture or record, stage several items, then file a new bug, add to an existing one, or log a note-only bug. |
51
51
 
52
52
  ## Quick start
53
53
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bugcap"
7
- version = "0.3.0"
7
+ version = "0.4.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"
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -0,0 +1,134 @@
1
+ """Live-mode drafts: unsaved staged media (any number of images / recordings, or none) plus
2
+ whatever was typed, kept on disk until it is saved as a report or discarded. One
3
+ `<uuid>.json` sidecar per draft; its media files sit beside it as `<uuid>-*`. A draft made by
4
+ an older version is just `<uuid>.png` + `<uuid>.json` and still loads."""
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import shutil
9
+ import sys
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ from .paths import drafts_dir
17
+
18
+ FIELDS = ("title", "notes", "tags", "status")
19
+
20
+
21
+ @dataclass
22
+ class Draft:
23
+ id: str
24
+ created_at: str
25
+ repo: Optional[str]
26
+ title: str = ""
27
+ notes: str = ""
28
+ tags: list = None # type: ignore[assignment]
29
+ status: str = "open"
30
+ # [{"kind", "path" (absolute or None), "mime", "size_bytes", "frames": [(path, size)]}]
31
+ items: list = field(default_factory=list)
32
+
33
+ def fields(self) -> dict:
34
+ return {"title": self.title, "notes": self.notes, "tags": list(self.tags or []), "status": self.status}
35
+
36
+ @property
37
+ def png_path(self) -> Optional[Path]:
38
+ """The first stored file (the screenshot of a single-image draft), if any."""
39
+ for item in self.items:
40
+ if item["path"]:
41
+ return Path(item["path"])
42
+ return None
43
+
44
+
45
+ def _move(source, target: Path) -> Path:
46
+ shutil.move(str(source), target)
47
+ return target
48
+
49
+
50
+ def save_draft(repo: Optional[str], png_path, fields: dict, items: Optional[list] = None) -> Draft:
51
+ """Move the media into drafts/ and write the sidecar. `items` are dicts shaped like
52
+ `Draft.items`; for a plain screenshot pass `png_path` instead."""
53
+ if items is None:
54
+ items = [{"kind": "image", "path": str(png_path), "mime": "image/png", "size_bytes": 0, "frames": []}] if png_path else []
55
+ draft_id = str(uuid.uuid4())
56
+ directory = drafts_dir()
57
+ listed = []
58
+ for n, item in enumerate(items, start=1):
59
+ path = item.get("path")
60
+ new_path = _move(path, directory / f"{draft_id}-{n}{Path(path).suffix}") if path else None
61
+ frames = []
62
+ for m, (frame_path, size) in enumerate(item.get("frames") or [], start=1):
63
+ frames.append([_move(frame_path, directory / f"{draft_id}-{n}-f{m:03d}.png").name, size])
64
+ listed.append({
65
+ "kind": item["kind"], "file": new_path.name if new_path else None,
66
+ "mime": item.get("mime", "image/png"), "size_bytes": item.get("size_bytes", 0), "frames": frames,
67
+ })
68
+ meta = {
69
+ "created_at": datetime.now(timezone.utc).isoformat(),
70
+ "repo": repo,
71
+ "title": fields.get("title", ""),
72
+ "notes": fields.get("notes", ""),
73
+ "tags": list(fields.get("tags") or []),
74
+ "status": fields.get("status", "open"),
75
+ "media": listed,
76
+ }
77
+ (directory / f"{draft_id}.json").write_text(json.dumps(meta, indent=2))
78
+ return load_draft(draft_id) # type: ignore[return-value]
79
+
80
+
81
+ def _items_from_meta(directory: Path, draft_id: str, meta: dict) -> list:
82
+ if "media" not in meta: # legacy: one screenshot
83
+ png = directory / f"{draft_id}.png"
84
+ if not png.is_file():
85
+ raise ValueError("incomplete draft")
86
+ return [{"kind": "image", "path": str(png), "mime": "image/png", "size_bytes": png.stat().st_size, "frames": []}]
87
+ items = []
88
+ for entry in meta["media"]:
89
+ path = directory / entry["file"] if entry.get("file") else None
90
+ frames = [(str(directory / name), int(size)) for name, size in entry.get("frames", [])]
91
+ files = ([path] if path else []) + [Path(p) for p, _ in frames]
92
+ if not files or not all(f.is_file() for f in files):
93
+ raise ValueError("incomplete draft")
94
+ items.append({
95
+ "kind": str(entry["kind"]), "path": str(path) if path else None, "mime": str(entry.get("mime", "")),
96
+ "size_bytes": int(entry.get("size_bytes", 0)), "frames": frames,
97
+ })
98
+ return items
99
+
100
+
101
+ def load_draft(draft_id: str) -> Optional[Draft]:
102
+ directory = drafts_dir()
103
+ sidecar = directory / f"{draft_id}.json"
104
+ try:
105
+ meta = json.loads(sidecar.read_text())
106
+ if not isinstance(meta, dict):
107
+ raise ValueError("incomplete draft")
108
+ return Draft(
109
+ draft_id, meta.get("created_at", ""), meta.get("repo"),
110
+ str(meta.get("title", "")), str(meta.get("notes", "")),
111
+ [str(t) for t in meta.get("tags", [])], str(meta.get("status", "open")),
112
+ _items_from_meta(directory, draft_id, meta),
113
+ )
114
+ except (OSError, ValueError, TypeError, KeyError) as exc:
115
+ print(f"warning: skipping unreadable draft {draft_id}: {exc}", file=sys.stderr)
116
+ return None
117
+
118
+
119
+ def list_drafts(repo: Optional[str]) -> list[Draft]:
120
+ """Drafts saved for this repo (or without a repo), oldest first."""
121
+ found = []
122
+ for sidecar in sorted(drafts_dir().glob("*.json")):
123
+ draft = load_draft(sidecar.stem)
124
+ if draft is not None and draft.repo == repo:
125
+ found.append(draft)
126
+ return sorted(found, key=lambda d: d.created_at)
127
+
128
+
129
+ def delete_draft(draft_id: str) -> None:
130
+ directory = drafts_dir()
131
+ (directory / f"{draft_id}.json").unlink(missing_ok=True)
132
+ (directory / f"{draft_id}.png").unlink(missing_ok=True)
133
+ for leftover in directory.glob(f"{draft_id}-*"):
134
+ leftover.unlink(missing_ok=True)