byte-bot 0.1.2__tar.gz → 0.1.4__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 (39) hide show
  1. {byte_bot-0.1.2 → byte_bot-0.1.4}/PKG-INFO +1 -1
  2. {byte_bot-0.1.2 → byte_bot-0.1.4}/pyproject.toml +1 -1
  3. byte_bot-0.1.4/src/byte_bot/__init__.py +3 -0
  4. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/opencode_http.py +217 -6
  5. byte_bot-0.1.2/src/byte_bot/__init__.py +0 -3
  6. {byte_bot-0.1.2 → byte_bot-0.1.4}/.gitignore +0 -0
  7. {byte_bot-0.1.2 → byte_bot-0.1.4}/LICENSE +0 -0
  8. {byte_bot-0.1.2 → byte_bot-0.1.4}/README.md +0 -0
  9. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/__main__.py +0 -0
  10. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/__init__.py +0 -0
  11. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/codex_exec_jsonl.py +0 -0
  12. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/legacy.py +0 -0
  13. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/app.py +0 -0
  14. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/cli.py +0 -0
  15. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/config.py +0 -0
  16. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/demo.py +0 -0
  17. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/disclosure.py +0 -0
  18. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/discovery.py +0 -0
  19. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/doctor.py +0 -0
  20. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/http_deadline.py +0 -0
  21. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/journal_bridge.py +0 -0
  22. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/migration.py +0 -0
  23. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/mobile.py +0 -0
  24. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/model.py +0 -0
  25. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/notifications.py +0 -0
  26. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/onboarding.py +0 -0
  27. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/platform_migration.py +0 -0
  28. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/presentation.py +0 -0
  29. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/projects.py +0 -0
  30. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/repository.py +0 -0
  31. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/service_manager.py +0 -0
  32. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/state.py +0 -0
  33. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/storage.py +0 -0
  34. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/structured_source.py +0 -0
  35. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/systemd_unit.py +0 -0
  36. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/telegram.py +0 -0
  37. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/telegram_setup.py +0 -0
  38. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/tui.py +0 -0
  39. {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/updater.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: byte-bot
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Local dashboard for monitoring coding agents with optional Telegram status
5
5
  Project-URL: Homepage, https://github.com/erajtob/byte-bot
6
6
  Project-URL: Source, https://github.com/erajtob/byte-bot
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "byte-bot"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Local dashboard for monitoring coding agents with optional Telegram status"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -0,0 +1,3 @@
1
+ """byte-bot."""
2
+
3
+ __version__ = "0.1.4"
@@ -1,9 +1,11 @@
1
1
  """Contract-checked, read-only OpenCode HTTP observation.
2
2
 
3
3
  Only an explicitly configured loopback origin is contacted. The adapter issues
4
- fixed read-only GET requests for server info, active-session identity and bounded
5
- metadata for those active sessions. It never starts OpenCode, follows redirects,
6
- reads transcripts/context routes or reads the live OpenCode database.
4
+ fixed read-only GET requests for server health, active-session identity, a bounded
5
+ newest-first session-presence list, bounded metadata, and bounded projected activity
6
+ needed to classify current work. It never starts OpenCode, follows redirects, stores
7
+ message content, reads arbitrary transcript/context routes, or reads the live
8
+ OpenCode database.
7
9
  """
8
10
 
9
11
  from __future__ import annotations
@@ -18,7 +20,7 @@ import re
18
20
  import ssl
19
21
  import time
20
22
  from collections.abc import Callable
21
- from dataclasses import dataclass
23
+ from dataclasses import dataclass, replace
22
24
  from datetime import UTC, datetime, timedelta
23
25
  from pathlib import Path
24
26
  from typing import Any
@@ -45,6 +47,7 @@ from byte_bot.storage import read_private_token
45
47
 
46
48
  ADAPTER_NAME = "opencode-http"
47
49
  FIELD_VERSION = "opencode-session-active-v1"
50
+ PRESENCE_FIELD_VERSION = "opencode-session-presence-v1"
48
51
  USAGE_FIELD_VERSION = "opencode-session-metadata-v1"
49
52
  WORKSPACE_FIELD_VERSION = "opencode-session-workspace-v1"
50
53
  INTERVENTION_FIELD_VERSION = "opencode-pending-intervention-v1"
@@ -58,17 +61,20 @@ INFO_PATH = "/api/info"
58
61
  GLOBAL_HEALTH_PATH = "/global/health"
59
62
  API_HEALTH_PATH = "/api/health"
60
63
  ACTIVE_PATH = "/api/session/active"
64
+ _SESSION_LIST_QUERY = "limit=20&order=desc"
65
+ SESSION_LIST_PATH = f"/api/session?{_SESSION_LIST_QUERY}"
61
66
  SESSION_PATH_PREFIX = "/api/session/"
62
67
  PERMISSION_SUFFIX = "/permission"
63
68
  FORM_SUFFIX = "/form"
64
69
  MESSAGE_SUFFIX = "/message"
65
- _ACTIVITY_QUERY = "limit=1&order=desc&type=assistant"
70
+ _ACTIVITY_QUERY = "limit=2&order=desc"
66
71
  _SESSION_ID_RE = re.compile(r"^ses[A-Za-z0-9_-]{1,125}$")
67
72
  _MAX_USAGE_SESSIONS = 16
68
73
  _MAX_INTERVENTION_SESSIONS = 16
69
74
  _MAX_ACTIVITY_SESSIONS = 16
75
+ _MAX_IDLE_PROJECT_SESSIONS = 12
70
76
  _MAX_PENDING_ITEMS = 32
71
- _MAX_ACTIVITY_MESSAGES = 1
77
+ _MAX_ACTIVITY_MESSAGES = 2
72
78
  _MAX_ACTIVITY_PARTS = 64
73
79
  _MAX_WORKSPACE_PATH = 4096
74
80
  _MAX_USAGE_COUNT = (1 << 63) - 1
@@ -296,6 +302,8 @@ def _route_allowed(path: str) -> bool:
296
302
  query = parsed.query
297
303
  if route in {INFO_PATH, GLOBAL_HEALTH_PATH, API_HEALTH_PATH, ACTIVE_PATH}:
298
304
  return not query
305
+ if route == "/api/session":
306
+ return query == _SESSION_LIST_QUERY
299
307
  if not route.startswith(SESSION_PATH_PREFIX):
300
308
  return False
301
309
  remainder = route.removeprefix(SESSION_PATH_PREFIX)
@@ -528,6 +536,54 @@ def _session_data(payload: object, *, native_id: str) -> dict[str, Any]:
528
536
  return data
529
537
 
530
538
 
539
+ def _session_list_data(payload: object) -> list[dict[str, Any]]:
540
+ if not isinstance(payload, dict) or "data" not in payload:
541
+ raise _OpenCodeFailure(
542
+ "malformed_session_list",
543
+ "OpenCode session-list shape is unsupported",
544
+ )
545
+ data = payload["data"]
546
+ if not isinstance(data, list):
547
+ raise _OpenCodeFailure(
548
+ "malformed_session_list",
549
+ "OpenCode session-list shape is unsupported",
550
+ )
551
+ if len(data) > 20:
552
+ raise _OpenCodeFailure(
553
+ "too_many_sessions",
554
+ "OpenCode session-list limit exceeded",
555
+ )
556
+ if not all(isinstance(item, dict) for item in data):
557
+ raise _OpenCodeFailure(
558
+ "malformed_session_list",
559
+ "OpenCode session-list shape is unsupported",
560
+ )
561
+ return data
562
+
563
+
564
+ def _session_updated_at(data: dict[str, Any]) -> datetime | None:
565
+ time_data = data.get("time")
566
+ if not isinstance(time_data, dict):
567
+ return None
568
+ value = time_data.get("updated")
569
+ if (
570
+ isinstance(value, bool)
571
+ or not isinstance(value, (int, float))
572
+ or not math.isfinite(value)
573
+ or value < 0
574
+ ):
575
+ return None
576
+ try:
577
+ return datetime.fromtimestamp(float(value) / 1000.0, tz=UTC)
578
+ except (OverflowError, OSError, ValueError):
579
+ return None
580
+
581
+
582
+ def _session_is_archived(data: dict[str, Any]) -> bool:
583
+ time_data = data.get("time")
584
+ return isinstance(time_data, dict) and time_data.get("archived") is not None
585
+
586
+
531
587
  def _session_metadata(
532
588
  data: dict[str, Any],
533
589
  *,
@@ -653,6 +709,89 @@ def _match_project_id(
653
709
  return next(iter(matches)) if len(matches) == 1 else None
654
710
 
655
711
 
712
+ def _idle_project_sessions(
713
+ rows: list[dict[str, Any]],
714
+ *,
715
+ source_id: str,
716
+ observed_at: datetime,
717
+ project_roots: tuple[tuple[str, str], ...],
718
+ ) -> tuple[Session, ...]:
719
+ """Project-scoped idle presence from the bounded newest-first session list."""
720
+
721
+ sessions: list[Session] = []
722
+ seen_projects: set[str] = set()
723
+ adapter_id = f"{ADAPTER_NAME}:{source_id}"
724
+ for data in rows:
725
+ if len(sessions) >= _MAX_IDLE_PROJECT_SESSIONS:
726
+ break
727
+ native_id = data.get("id")
728
+ if not _valid_session_id(native_id) or _session_is_archived(data):
729
+ continue
730
+ try:
731
+ workspace = _session_workspace(data)
732
+ except _OpenCodeFailure:
733
+ continue
734
+ project_id = _match_project_id(workspace, project_roots)
735
+ if project_id is None or project_id in seen_projects:
736
+ continue
737
+
738
+ model: str | None = None
739
+ provider: str | None = None
740
+ usage: Usage | None = None
741
+ evidence = [
742
+ Evidence(
743
+ source_kind=ADAPTER_NAME,
744
+ source_id=source_id,
745
+ field_version=PRESENCE_FIELD_VERSION,
746
+ observed_at=observed_at,
747
+ ),
748
+ Evidence(
749
+ source_kind=ADAPTER_NAME,
750
+ source_id=source_id,
751
+ field_version=WORKSPACE_FIELD_VERSION,
752
+ observed_at=observed_at,
753
+ ),
754
+ ]
755
+ try:
756
+ model, provider, usage = _session_metadata(
757
+ data,
758
+ observed_at=observed_at,
759
+ )
760
+ except _OpenCodeFailure:
761
+ pass
762
+ else:
763
+ evidence.append(
764
+ Evidence(
765
+ source_kind=ADAPTER_NAME,
766
+ source_id=source_id,
767
+ field_version=USAGE_FIELD_VERSION,
768
+ observed_at=observed_at,
769
+ )
770
+ )
771
+
772
+ session_id = f"{adapter_id}/{native_id}"
773
+ sessions.append(
774
+ Session(
775
+ id=session_id,
776
+ adapter_instance_id=adapter_id,
777
+ native_id=native_id,
778
+ work_id=native_id,
779
+ project_id=project_id,
780
+ model=model,
781
+ provider=provider,
782
+ lifecycle=Lifecycle.IDLE,
783
+ freshness=Freshness.FRESH,
784
+ observed_at=observed_at,
785
+ last_activity_at=_session_updated_at(data),
786
+ usage=usage,
787
+ evidence=tuple(evidence),
788
+ )
789
+ )
790
+ seen_projects.add(project_id)
791
+
792
+ return tuple(sessions)
793
+
794
+
656
795
  def _failure(
657
796
  source_id: str,
658
797
  failure: _OpenCodeFailure,
@@ -783,6 +922,78 @@ def observe_opencode(
783
922
  metadata_attempts = 0
784
923
  intervention_attempts = 0
785
924
  activity_attempts = 0
925
+
926
+ if not data and project_roots:
927
+ try:
928
+ listed_payload = fetch_json(
929
+ origin,
930
+ SESSION_LIST_PATH,
931
+ password,
932
+ timeout_seconds,
933
+ )
934
+ listed_rows = _session_list_data(listed_payload)
935
+ except _OpenCodeFailure as exc:
936
+ if exc.code not in {"http_error", "malformed_response"}:
937
+ errors.append(
938
+ CollectionError(
939
+ code=exc.code,
940
+ message=exc.message,
941
+ source_id=source_id,
942
+ )
943
+ )
944
+ else:
945
+ idle_sessions = list(
946
+ _idle_project_sessions(
947
+ listed_rows,
948
+ source_id=source_id,
949
+ observed_at=observed_at,
950
+ project_roots=project_roots,
951
+ )
952
+ )
953
+ for index, session in enumerate(idle_sessions):
954
+ if activity_attempts >= _MAX_ACTIVITY_SESSIONS:
955
+ break
956
+ activity_attempts += 1
957
+ try:
958
+ activity_payload = fetch_json(
959
+ origin,
960
+ _session_message_path(session.native_id),
961
+ password,
962
+ timeout_seconds,
963
+ )
964
+ activity = _message_activity(
965
+ activity_payload,
966
+ source_id=source_id,
967
+ native_id=session.native_id,
968
+ session_id=session.id,
969
+ observed_at=observed_at,
970
+ )
971
+ except _OpenCodeFailure as exc:
972
+ if exc.code not in {
973
+ "http_error",
974
+ "malformed_response",
975
+ "malformed_session_activity",
976
+ }:
977
+ errors.append(
978
+ CollectionError(
979
+ code=exc.code,
980
+ message=exc.message,
981
+ source_id=source_id,
982
+ )
983
+ )
984
+ continue
985
+ if (
986
+ activity is None
987
+ or activity.payload.get("state") != "RUNNING"
988
+ ):
989
+ continue
990
+ events.append(activity)
991
+ idle_sessions[index] = replace(
992
+ session,
993
+ lifecycle=Lifecycle.RUNNING,
994
+ )
995
+ sessions.extend(idle_sessions)
996
+
786
997
  for native_id, record in sorted(data.items(), key=lambda item: str(item[0])):
787
998
  if (
788
999
  not _valid_session_id(native_id)
@@ -1,3 +0,0 @@
1
- """byte-bot."""
2
-
3
- __version__ = "0.1.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes