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.
- {byte_bot-0.1.2 → byte_bot-0.1.4}/PKG-INFO +1 -1
- {byte_bot-0.1.2 → byte_bot-0.1.4}/pyproject.toml +1 -1
- byte_bot-0.1.4/src/byte_bot/__init__.py +3 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/opencode_http.py +217 -6
- byte_bot-0.1.2/src/byte_bot/__init__.py +0 -3
- {byte_bot-0.1.2 → byte_bot-0.1.4}/.gitignore +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/LICENSE +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/README.md +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/__main__.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/__init__.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/codex_exec_jsonl.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/adapters/legacy.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/app.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/cli.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/config.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/demo.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/disclosure.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/discovery.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/doctor.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/http_deadline.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/journal_bridge.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/migration.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/mobile.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/model.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/notifications.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/onboarding.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/platform_migration.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/presentation.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/projects.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/repository.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/service_manager.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/state.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/storage.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/structured_source.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/systemd_unit.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/telegram.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/telegram_setup.py +0 -0
- {byte_bot-0.1.2 → byte_bot-0.1.4}/src/byte_bot/tui.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
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=
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|