vibego 0.2.52__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of vibego might be problematic. Click here for more details.
- bot.py +1557 -1431
- logging_setup.py +25 -18
- master.py +799 -508
- project_repository.py +42 -40
- scripts/__init__.py +1 -2
- scripts/bump_version.sh +57 -55
- scripts/log_writer.py +19 -16
- scripts/master_healthcheck.py +44 -44
- scripts/models/claudecode.sh +4 -4
- scripts/models/codex.sh +1 -1
- scripts/models/common.sh +24 -6
- scripts/models/gemini.sh +2 -2
- scripts/publish.sh +50 -50
- scripts/run_bot.sh +38 -17
- scripts/start.sh +136 -116
- scripts/start_tmux_codex.sh +8 -8
- scripts/stop_all.sh +21 -21
- scripts/stop_bot.sh +31 -10
- scripts/test_deps_check.sh +32 -28
- tasks/__init__.py +1 -1
- tasks/commands.py +4 -4
- tasks/constants.py +1 -1
- tasks/fsm.py +9 -9
- tasks/models.py +7 -7
- tasks/service.py +56 -56
- vibego-1.0.0.dist-info/METADATA +236 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/RECORD +36 -35
- vibego-1.0.0.dist-info/licenses/LICENSE +201 -0
- vibego_cli/__init__.py +5 -4
- vibego_cli/__main__.py +1 -2
- vibego_cli/config.py +9 -9
- vibego_cli/deps.py +8 -9
- vibego_cli/main.py +63 -63
- vibego-0.2.52.dist-info/METADATA +0 -197
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/WHEEL +0 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/entry_points.txt +0 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/top_level.txt +0 -0
tasks/service.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Persistence and business logic for the task subsystem."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
@@ -28,10 +28,10 @@ logger = logging.getLogger(__name__)
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class TaskService:
|
|
31
|
-
"""
|
|
31
|
+
"""Wrap task-related database operations."""
|
|
32
32
|
|
|
33
33
|
def __init__(self, db_path: Path, project_slug: str) -> None:
|
|
34
|
-
"""
|
|
34
|
+
"""Initialise the service with the database path and project slug."""
|
|
35
35
|
|
|
36
36
|
self.db_path = Path(db_path)
|
|
37
37
|
self.project_slug = project_slug
|
|
@@ -40,7 +40,7 @@ class TaskService:
|
|
|
40
40
|
self._valid_statuses = set(TASK_STATUSES)
|
|
41
41
|
|
|
42
42
|
async def initialize(self) -> None:
|
|
43
|
-
"""
|
|
43
|
+
"""Ensure the schema exists and run required migrations."""
|
|
44
44
|
|
|
45
45
|
if self._initialized:
|
|
46
46
|
return
|
|
@@ -58,7 +58,7 @@ class TaskService:
|
|
|
58
58
|
self._initialized = True
|
|
59
59
|
|
|
60
60
|
async def _create_tables(self, db: aiosqlite.Connection) -> None:
|
|
61
|
-
"""
|
|
61
|
+
"""Create or augment all tables and indexes for tasks."""
|
|
62
62
|
|
|
63
63
|
await db.execute(
|
|
64
64
|
"""
|
|
@@ -191,7 +191,7 @@ class TaskService:
|
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
async def _migrate_timezones(self, db: aiosqlite.Connection) -> None:
|
|
194
|
-
"""
|
|
194
|
+
"""Convert legacy UTC timestamps to their Shanghai equivalents."""
|
|
195
195
|
|
|
196
196
|
db.row_factory = aiosqlite.Row
|
|
197
197
|
tables: Sequence[tuple[str, str, tuple[str, ...]]] = (
|
|
@@ -245,7 +245,7 @@ class TaskService:
|
|
|
245
245
|
)
|
|
246
246
|
|
|
247
247
|
async def _migrate_task_ids_to_underscore(self, db: aiosqlite.Connection) -> None:
|
|
248
|
-
"""
|
|
248
|
+
"""Rewrite legacy task IDs with underscores so Telegram commands remain clickable."""
|
|
249
249
|
|
|
250
250
|
db.row_factory = aiosqlite.Row
|
|
251
251
|
async with db.execute(
|
|
@@ -265,7 +265,7 @@ class TaskService:
|
|
|
265
265
|
if not legacy_row:
|
|
266
266
|
return
|
|
267
267
|
|
|
268
|
-
logger.info("
|
|
268
|
+
logger.info("Detected legacy task IDs, starting migration: project=%s", self.project_slug)
|
|
269
269
|
await db.execute("PRAGMA foreign_keys = OFF")
|
|
270
270
|
await db.execute("PRAGMA defer_foreign_keys = ON")
|
|
271
271
|
mapping: Dict[str, str] = {}
|
|
@@ -289,27 +289,27 @@ class TaskService:
|
|
|
289
289
|
continue
|
|
290
290
|
if new_id is None:
|
|
291
291
|
logger.error(
|
|
292
|
-
"
|
|
292
|
+
"Task ID migration encountered a non-normalisable value: project=%s value=%s",
|
|
293
293
|
self.project_slug,
|
|
294
294
|
old_id,
|
|
295
295
|
)
|
|
296
|
-
raise ValueError("
|
|
296
|
+
raise ValueError("Task ID migration failed: unable to normalise ID")
|
|
297
297
|
if new_id != old_id and new_id in existing_ids:
|
|
298
298
|
logger.error(
|
|
299
|
-
"
|
|
299
|
+
"Task ID migration detected a potential conflict: project=%s old=%s new=%s",
|
|
300
300
|
self.project_slug,
|
|
301
301
|
old_id,
|
|
302
302
|
new_id,
|
|
303
303
|
)
|
|
304
|
-
raise ValueError("
|
|
304
|
+
raise ValueError("Task ID migration conflict: target ID already exists")
|
|
305
305
|
if new_id in mapping.values() or new_id in mapping:
|
|
306
306
|
logger.error(
|
|
307
|
-
"
|
|
307
|
+
"Task ID migration detected a conflict: project=%s old=%s new=%s",
|
|
308
308
|
self.project_slug,
|
|
309
309
|
old_id,
|
|
310
310
|
new_id,
|
|
311
311
|
)
|
|
312
|
-
raise ValueError("
|
|
312
|
+
raise ValueError("Task ID migration conflict")
|
|
313
313
|
mapping[old_id] = new_id
|
|
314
314
|
|
|
315
315
|
if not mapping:
|
|
@@ -338,13 +338,13 @@ class TaskService:
|
|
|
338
338
|
|
|
339
339
|
self._write_id_migration_report(mapping)
|
|
340
340
|
logger.info(
|
|
341
|
-
"
|
|
341
|
+
"Task ID migration completed: project=%s changed=%s",
|
|
342
342
|
self.project_slug,
|
|
343
343
|
len(mapping),
|
|
344
344
|
)
|
|
345
345
|
|
|
346
346
|
async def _archive_legacy_child_tasks(self, db: aiosqlite.Connection) -> None:
|
|
347
|
-
"""
|
|
347
|
+
"""Archive legacy child tasks so they stop appearing in listings."""
|
|
348
348
|
|
|
349
349
|
now = shanghai_now_iso()
|
|
350
350
|
cursor = await db.execute(
|
|
@@ -364,15 +364,15 @@ class TaskService:
|
|
|
364
364
|
changed = 0
|
|
365
365
|
await cursor.close()
|
|
366
366
|
if changed > 0:
|
|
367
|
-
logger.info("
|
|
367
|
+
logger.info("Archived legacy child tasks: project=%s count=%s", self.project_slug, changed)
|
|
368
368
|
|
|
369
369
|
async def _drop_child_sequences_table(self, db: aiosqlite.Connection) -> None:
|
|
370
|
-
"""
|
|
370
|
+
"""Remove the defunct child sequence table to prevent stale lookups."""
|
|
371
371
|
|
|
372
372
|
await db.execute("DROP TABLE IF EXISTS child_sequences")
|
|
373
373
|
|
|
374
374
|
async def _verify_status_values(self, db: aiosqlite.Connection) -> None:
|
|
375
|
-
"""
|
|
375
|
+
"""Validate task status values against the allowed enumeration."""
|
|
376
376
|
|
|
377
377
|
async with db.execute(
|
|
378
378
|
"SELECT DISTINCT status FROM tasks WHERE project_slug = ?",
|
|
@@ -382,14 +382,14 @@ class TaskService:
|
|
|
382
382
|
for (status,) in rows:
|
|
383
383
|
if status is None:
|
|
384
384
|
logger.error(
|
|
385
|
-
"
|
|
385
|
+
"Task status integrity check found NULL value: project=%s",
|
|
386
386
|
self.project_slug,
|
|
387
387
|
)
|
|
388
388
|
continue
|
|
389
389
|
normalized = self._normalize_status_token(status, context="integrity_check")
|
|
390
390
|
if normalized not in self._valid_statuses:
|
|
391
391
|
logger.error(
|
|
392
|
-
"
|
|
392
|
+
"Task status integrity check found unknown value: project=%s value=%s",
|
|
393
393
|
self.project_slug,
|
|
394
394
|
status,
|
|
395
395
|
)
|
|
@@ -405,7 +405,7 @@ class TaskService:
|
|
|
405
405
|
description: Optional[str] = None,
|
|
406
406
|
actor: Optional[str],
|
|
407
407
|
) -> TaskRecord:
|
|
408
|
-
"""
|
|
408
|
+
"""Create a root task and capture the initial history entry."""
|
|
409
409
|
|
|
410
410
|
async with self._lock:
|
|
411
411
|
async with aiosqlite.connect(self.db_path) as db:
|
|
@@ -483,7 +483,7 @@ class TaskService:
|
|
|
483
483
|
include_archived: bool = False,
|
|
484
484
|
exclude_statuses: Optional[Sequence[str]] = None,
|
|
485
485
|
) -> List[TaskRecord]:
|
|
486
|
-
"""
|
|
486
|
+
"""List tasks with optional filters, status exclusions, and pagination."""
|
|
487
487
|
|
|
488
488
|
query = [
|
|
489
489
|
"SELECT * FROM tasks WHERE project_slug = ?",
|
|
@@ -515,7 +515,7 @@ class TaskService:
|
|
|
515
515
|
page: int,
|
|
516
516
|
page_size: int = DEFAULT_LIMIT,
|
|
517
517
|
) -> Tuple[List[TaskRecord], int, int]:
|
|
518
|
-
"""
|
|
518
|
+
"""Search tasks by title or description and return results, pages, and totals."""
|
|
519
519
|
|
|
520
520
|
if page_size <= 0:
|
|
521
521
|
page_size = DEFAULT_LIMIT
|
|
@@ -556,7 +556,7 @@ class TaskService:
|
|
|
556
556
|
return [self._row_to_task(row, context="search") for row in rows], pages, total
|
|
557
557
|
|
|
558
558
|
async def get_task(self, task_id: str) -> Optional[TaskRecord]:
|
|
559
|
-
"""
|
|
559
|
+
"""Return a task by ID, or ``None`` when it does not exist."""
|
|
560
560
|
|
|
561
561
|
canonical_task_id = self._canonical_task_id(task_id)
|
|
562
562
|
if not canonical_task_id:
|
|
@@ -588,11 +588,11 @@ class TaskService:
|
|
|
588
588
|
description: Optional[str] = None,
|
|
589
589
|
archived: Optional[bool] = None,
|
|
590
590
|
) -> TaskRecord:
|
|
591
|
-
"""
|
|
591
|
+
"""Update a task, write history entries, and return the refreshed record."""
|
|
592
592
|
|
|
593
593
|
canonical_task_id = self._canonical_task_id(task_id)
|
|
594
594
|
if not canonical_task_id:
|
|
595
|
-
raise ValueError("
|
|
595
|
+
raise ValueError("Task does not exist")
|
|
596
596
|
task_id = canonical_task_id
|
|
597
597
|
async with self._lock:
|
|
598
598
|
async with aiosqlite.connect(self.db_path) as db:
|
|
@@ -602,7 +602,7 @@ class TaskService:
|
|
|
602
602
|
row = await self._fetch_task_row(db, task_id)
|
|
603
603
|
if row is None:
|
|
604
604
|
await db.execute("ROLLBACK")
|
|
605
|
-
raise ValueError("
|
|
605
|
+
raise ValueError("Task does not exist")
|
|
606
606
|
updates = []
|
|
607
607
|
params: List[object] = []
|
|
608
608
|
history_items: List[Tuple[str, Optional[str], Optional[str]]] = []
|
|
@@ -614,7 +614,7 @@ class TaskService:
|
|
|
614
614
|
normalized_status = self._normalize_status_token(status, context="update")
|
|
615
615
|
if normalized_status != status:
|
|
616
616
|
logger.warning(
|
|
617
|
-
"
|
|
617
|
+
"Task status input corrected automatically: task_id=%s raw=%s normalized=%s",
|
|
618
618
|
task_id,
|
|
619
619
|
status,
|
|
620
620
|
normalized_status,
|
|
@@ -676,7 +676,7 @@ class TaskService:
|
|
|
676
676
|
await db.commit()
|
|
677
677
|
updated = await self.get_task(task_id)
|
|
678
678
|
if updated is None:
|
|
679
|
-
raise ValueError("
|
|
679
|
+
raise ValueError("Task does not exist")
|
|
680
680
|
return updated
|
|
681
681
|
|
|
682
682
|
async def add_note(
|
|
@@ -687,11 +687,11 @@ class TaskService:
|
|
|
687
687
|
content: str,
|
|
688
688
|
actor: Optional[str],
|
|
689
689
|
) -> TaskNoteRecord:
|
|
690
|
-
"""
|
|
690
|
+
"""Append a note to a task and persist a corresponding history entry."""
|
|
691
691
|
|
|
692
692
|
canonical_task_id = self._canonical_task_id(task_id)
|
|
693
693
|
if not canonical_task_id:
|
|
694
|
-
raise ValueError("
|
|
694
|
+
raise ValueError("Task does not exist")
|
|
695
695
|
task_id = canonical_task_id
|
|
696
696
|
now = shanghai_now_iso()
|
|
697
697
|
async with self._lock:
|
|
@@ -702,7 +702,7 @@ class TaskService:
|
|
|
702
702
|
task_row = await self._fetch_task_row(db, task_id)
|
|
703
703
|
if task_row is None:
|
|
704
704
|
await db.execute("ROLLBACK")
|
|
705
|
-
raise ValueError("
|
|
705
|
+
raise ValueError("Task does not exist")
|
|
706
706
|
cursor = await db.execute(
|
|
707
707
|
"""
|
|
708
708
|
INSERT INTO task_notes(task_id, note_type, content, created_at)
|
|
@@ -738,7 +738,7 @@ class TaskService:
|
|
|
738
738
|
)
|
|
739
739
|
|
|
740
740
|
async def list_notes(self, task_id: str) -> List[TaskNoteRecord]:
|
|
741
|
-
"""
|
|
741
|
+
"""Return every note for a task ordered by creation time."""
|
|
742
742
|
|
|
743
743
|
canonical_task_id = self._canonical_task_id(task_id)
|
|
744
744
|
if not canonical_task_id:
|
|
@@ -766,7 +766,7 @@ class TaskService:
|
|
|
766
766
|
]
|
|
767
767
|
|
|
768
768
|
async def list_history(self, task_id: str) -> List[TaskHistoryRecord]:
|
|
769
|
-
"""
|
|
769
|
+
"""Return the full history list for a task."""
|
|
770
770
|
|
|
771
771
|
canonical_task_id = self._canonical_task_id(task_id)
|
|
772
772
|
if not canonical_task_id:
|
|
@@ -809,11 +809,11 @@ class TaskService:
|
|
|
809
809
|
payload: Optional[Dict[str, Any]] = None,
|
|
810
810
|
created_at: Optional[str] = None,
|
|
811
811
|
) -> None:
|
|
812
|
-
"""
|
|
812
|
+
"""Record a structured task event."""
|
|
813
813
|
|
|
814
814
|
canonical_task_id = self._canonical_task_id(task_id)
|
|
815
815
|
if not canonical_task_id:
|
|
816
|
-
raise ValueError("
|
|
816
|
+
raise ValueError("Task does not exist")
|
|
817
817
|
task_id = canonical_task_id
|
|
818
818
|
|
|
819
819
|
event_token = (event_type or "task_action").strip() or "task_action"
|
|
@@ -825,7 +825,7 @@ class TaskService:
|
|
|
825
825
|
try:
|
|
826
826
|
payload_text = json.dumps(payload, ensure_ascii=False)
|
|
827
827
|
except (TypeError, ValueError) as exc:
|
|
828
|
-
logger.warning("
|
|
828
|
+
logger.warning("Failed to serialise event payload: task_id=%s error=%s", task_id, exc)
|
|
829
829
|
payload_text = None
|
|
830
830
|
async with self._lock:
|
|
831
831
|
async with aiosqlite.connect(self.db_path) as db:
|
|
@@ -835,7 +835,7 @@ class TaskService:
|
|
|
835
835
|
row = await self._fetch_task_row(db, task_id)
|
|
836
836
|
if row is None:
|
|
837
837
|
await db.execute("ROLLBACK")
|
|
838
|
-
raise ValueError("
|
|
838
|
+
raise ValueError("Task does not exist")
|
|
839
839
|
await self._insert_history(
|
|
840
840
|
db,
|
|
841
841
|
task_id,
|
|
@@ -850,7 +850,7 @@ class TaskService:
|
|
|
850
850
|
await db.commit()
|
|
851
851
|
|
|
852
852
|
async def delete_task(self, task_id: str, *, actor: Optional[str]) -> TaskRecord:
|
|
853
|
-
"""
|
|
853
|
+
"""Perform a logical delete by marking the task archived and return the state."""
|
|
854
854
|
|
|
855
855
|
updated = await self.update_task(task_id, actor=actor, archived=True)
|
|
856
856
|
return updated
|
|
@@ -863,7 +863,7 @@ class TaskService:
|
|
|
863
863
|
page_size: int = DEFAULT_LIMIT,
|
|
864
864
|
exclude_statuses: Optional[Sequence[str]] = None,
|
|
865
865
|
) -> Tuple[List[TaskRecord], int]:
|
|
866
|
-
"""
|
|
866
|
+
"""Fetch a specific page of tasks and return both the page data and total count."""
|
|
867
867
|
|
|
868
868
|
total = await self.count_tasks(
|
|
869
869
|
status=status,
|
|
@@ -887,7 +887,7 @@ class TaskService:
|
|
|
887
887
|
include_archived: bool,
|
|
888
888
|
exclude_statuses: Optional[Sequence[str]] = None,
|
|
889
889
|
) -> int:
|
|
890
|
-
"""
|
|
890
|
+
"""Count tasks that satisfy the provided filters."""
|
|
891
891
|
|
|
892
892
|
query = "SELECT COUNT(1) AS c FROM tasks WHERE project_slug = ?"
|
|
893
893
|
params: List[object] = [self.project_slug]
|
|
@@ -908,7 +908,7 @@ class TaskService:
|
|
|
908
908
|
return int(row["c"] if row else 0)
|
|
909
909
|
|
|
910
910
|
async def backup(self, target_path: Path) -> None:
|
|
911
|
-
"""
|
|
911
|
+
"""Backup the current database to the target path."""
|
|
912
912
|
|
|
913
913
|
target_path = target_path.expanduser()
|
|
914
914
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -921,7 +921,7 @@ class TaskService:
|
|
|
921
921
|
|
|
922
922
|
@staticmethod
|
|
923
923
|
def _convert_task_id_token(value: Optional[str]) -> Optional[str]:
|
|
924
|
-
"""
|
|
924
|
+
"""Normalise task ID separators to remain compatible with legacy formats."""
|
|
925
925
|
|
|
926
926
|
if value is None:
|
|
927
927
|
return None
|
|
@@ -930,14 +930,14 @@ class TaskService:
|
|
|
930
930
|
if token.startswith("TASK"):
|
|
931
931
|
suffix = token[4:]
|
|
932
932
|
if suffix and not suffix.startswith("_"):
|
|
933
|
-
#
|
|
933
|
+
# Legacy formats like TASK0001/TASK0001_1 require an underscore.
|
|
934
934
|
token = f"TASK_{suffix}"
|
|
935
935
|
else:
|
|
936
936
|
token = f"TASK{suffix}"
|
|
937
937
|
return token
|
|
938
938
|
|
|
939
939
|
def _canonical_task_id(self, value: Optional[str]) -> Optional[str]:
|
|
940
|
-
"""
|
|
940
|
+
"""Normalise externally provided task IDs into the canonical format."""
|
|
941
941
|
|
|
942
942
|
if value is None:
|
|
943
943
|
return None
|
|
@@ -948,7 +948,7 @@ class TaskService:
|
|
|
948
948
|
return self._convert_task_id_token(token)
|
|
949
949
|
|
|
950
950
|
def _write_id_migration_report(self, mapping: Dict[str, str]) -> None:
|
|
951
|
-
"""
|
|
951
|
+
"""Write a JSON report describing task ID migration results."""
|
|
952
952
|
|
|
953
953
|
if not mapping:
|
|
954
954
|
return
|
|
@@ -969,13 +969,13 @@ class TaskService:
|
|
|
969
969
|
report_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
970
970
|
except Exception as exc:
|
|
971
971
|
logger.warning(
|
|
972
|
-
"
|
|
972
|
+
"Failed to write task ID migration report: project=%s error=%s",
|
|
973
973
|
self.project_slug,
|
|
974
974
|
exc,
|
|
975
975
|
)
|
|
976
976
|
|
|
977
977
|
async def _fetch_task_row(self, db: aiosqlite.Connection, task_id: str):
|
|
978
|
-
"""
|
|
978
|
+
"""Fetch the raw task row from the database."""
|
|
979
979
|
|
|
980
980
|
canonical_task_id = self._canonical_task_id(task_id)
|
|
981
981
|
if not canonical_task_id:
|
|
@@ -988,7 +988,7 @@ class TaskService:
|
|
|
988
988
|
return await cursor.fetchone()
|
|
989
989
|
|
|
990
990
|
async def _next_root_sequence(self, db: aiosqlite.Connection) -> int:
|
|
991
|
-
"""
|
|
991
|
+
"""Increment and return the next root task sequence."""
|
|
992
992
|
|
|
993
993
|
async with db.execute(
|
|
994
994
|
"SELECT last_root FROM task_sequences WHERE project_slug = ?",
|
|
@@ -1022,7 +1022,7 @@ class TaskService:
|
|
|
1022
1022
|
payload: Optional[str] = None,
|
|
1023
1023
|
created_at: Optional[str] = None,
|
|
1024
1024
|
) -> None:
|
|
1025
|
-
"""
|
|
1025
|
+
"""Insert a task history entry while filling timestamps automatically."""
|
|
1026
1026
|
|
|
1027
1027
|
normalized = ensure_shanghai_iso(created_at) if created_at else None
|
|
1028
1028
|
timestamp = normalized or shanghai_now_iso()
|
|
@@ -1044,16 +1044,16 @@ class TaskService:
|
|
|
1044
1044
|
)
|
|
1045
1045
|
|
|
1046
1046
|
def _normalize_status_token(self, value: Optional[str], *, context: str) -> str:
|
|
1047
|
-
"""
|
|
1047
|
+
"""Normalise status strings, providing compatibility with legacy aliases."""
|
|
1048
1048
|
|
|
1049
1049
|
if not value:
|
|
1050
|
-
logger.warning("
|
|
1050
|
+
logger.warning("Encountered empty task status; falling back to default: context=%s", context)
|
|
1051
1051
|
return TASK_STATUSES[0]
|
|
1052
1052
|
token = str(value).strip().lower()
|
|
1053
1053
|
mapped = STATUS_ALIASES.get(token, token)
|
|
1054
1054
|
if mapped not in self._valid_statuses:
|
|
1055
1055
|
logger.warning(
|
|
1056
|
-
"
|
|
1056
|
+
"Unknown task status detected: value=%s mapped=%s context=%s",
|
|
1057
1057
|
value,
|
|
1058
1058
|
mapped,
|
|
1059
1059
|
context,
|
|
@@ -1061,7 +1061,7 @@ class TaskService:
|
|
|
1061
1061
|
return mapped
|
|
1062
1062
|
if mapped != token:
|
|
1063
1063
|
logger.info(
|
|
1064
|
-
"
|
|
1064
|
+
"Task status converted via alias: raw=%s normalized=%s context=%s",
|
|
1065
1065
|
value,
|
|
1066
1066
|
mapped,
|
|
1067
1067
|
context,
|
|
@@ -1074,7 +1074,7 @@ class TaskService:
|
|
|
1074
1074
|
*,
|
|
1075
1075
|
context: str,
|
|
1076
1076
|
) -> TaskRecord:
|
|
1077
|
-
"""
|
|
1077
|
+
"""Convert a sqlite row into a ``TaskRecord`` instance."""
|
|
1078
1078
|
|
|
1079
1079
|
tags_raw = row["tags"] or "[]"
|
|
1080
1080
|
try:
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vibego
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: vibego CLI: tools for bootstrapping and managing the Telegram Master Bot
|
|
5
|
+
Author: DavidChan
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: aiogram<4.0.0,>=3.0.0
|
|
17
|
+
Requires-Dist: aiohttp-socks>=0.10.0
|
|
18
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
19
|
+
Requires-Dist: markdown-it-py<4.0.0,>=3.0.0
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# vibego - vibe coding via Telegram anytime, anywhere
|
|
23
|
+
|
|
24
|
+
**Drive your terminal AI CLI via Telegram anytime and anywhere (supports Codex / ClaudeCode)**
|
|
25
|
+
|
|
26
|
+
For the Simplified Chinese version, see [README-zh.md](README-zh.md).
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
1. Control your terminal AI CLI through Telegram whenever you need it.
|
|
31
|
+
2. Capture lightweight task management and bug reports directly inside Telegram.
|
|
32
|
+
3. Switch between Codex / ClaudeCode terminal CLIs in one tap from Telegram.
|
|
33
|
+
4. Send commands over the HTTPS channel provided by the Telegram Bot API, protected by end‑to‑end TLS.
|
|
34
|
+
5. Keep runtime logs and state files under `~/.config/vibego/` so sensitive data never leaves the machine.
|
|
35
|
+
|
|
36
|
+
## Environment Requirements
|
|
37
|
+
|
|
38
|
+
- Install the core CLI dependencies with Homebrew to keep scripts aligned:
|
|
39
|
+
```bash
|
|
40
|
+
brew install python@3.11 tmux
|
|
41
|
+
```
|
|
42
|
+
The CLI verifies Python >= 3.11 when you run `vibego start` (see `pyproject.toml` `requires-python` and the Python
|
|
43
|
+
guard in `vibego_cli/main.py`). Validate with `python3 --version` (must show 3.11+) and `tmux -V`.
|
|
44
|
+
- To run the Codex model you also need the Codex CLI:
|
|
45
|
+
```bash
|
|
46
|
+
brew install codex
|
|
47
|
+
```
|
|
48
|
+
`scripts/run_bot.sh` checks for the `codex` executable before the worker launches. Confirm with `codex --version`.
|
|
49
|
+
- The scripts use the `python3` available on your `PATH` and automatically create `~/.config/vibego/runtime/venv` on
|
|
50
|
+
first launch to install Python dependencies. If you prefer to bootstrap manually:
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv ~/.config/vibego/runtime/venv
|
|
53
|
+
source ~/.config/vibego/runtime/venv/bin/activate
|
|
54
|
+
```
|
|
55
|
+
Set `VIBEGO_RUNTIME_ROOT` if you need a custom runtime location (pipx users typically do not).
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Create and retrieve a Telegram bot token
|
|
60
|
+
|
|
61
|
+
Use the official Telegram BotFather guide (<https://core.telegram.org/bots#botfather>):
|
|
62
|
+
|
|
63
|
+
1. Search for `@BotFather` in the Telegram client and start a chat.
|
|
64
|
+
2. Send `/start`, then `/newbot`, and follow the prompts for bot name and username.
|
|
65
|
+
3. BotFather returns an HTTP API token resembling `123456789:ABC...`; store it safely.
|
|
66
|
+
4. To regenerate or reset the token, send `/token` in the same chat and pick the bot.
|
|
67
|
+
|
|
68
|
+
### Install and start vibego
|
|
69
|
+
|
|
70
|
+
Before continuing, make sure Codex / ClaudeCode CLIs are installed and logged in, and that you have a Telegram bot token
|
|
71
|
+
ready.
|
|
72
|
+
|
|
73
|
+
- Consider merging the contents of [AGENTS-en.md](AGENTS-en.md) into your `$HOME/.codex/AGENTS.md` or
|
|
74
|
+
`$HOME/.claude/CLAUDE.md`.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pipx install vibego # or pip install --user vibego
|
|
78
|
+
vibego init # initialise the config directory and persist the Master Bot Token
|
|
79
|
+
vibego start # start the master service
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Directory Layout
|
|
83
|
+
|
|
84
|
+
- `bot.py`: aiogram 3 worker that supports multiple model sessions (Codex / ClaudeCode / reserved Gemini).
|
|
85
|
+
- `scripts/run_bot.sh`: one-click bootstrap (builds venv, starts tmux + model CLI + bot).
|
|
86
|
+
- `scripts/stop_bot.sh`: terminates the worker for a project (tmux session + bot process).
|
|
87
|
+
- `scripts/start_tmux_codex.sh`: low-level tmux/CLI launcher invoked by `run_bot.sh`, forces UTF‑8 via `tmux -u`.
|
|
88
|
+
- `scripts/models/`: model configuration modules (`common.sh` / `codex.sh` / `claudecode.sh` / `gemini.sh`).
|
|
89
|
+
- `logs/<model>/<project>/`: runtime logs (`run_bot.log`, `model.log`, `bot.pid`, `current_session.txt`), defaulted to
|
|
90
|
+
`~/.config/vibego/logs/`.
|
|
91
|
+
- `model.log` is rotated by `scripts/log_writer.py`, with 20 MB cap per file and 24-hour retention (override via
|
|
92
|
+
`MODEL_LOG_MAX_BYTES`, `MODEL_LOG_RETENTION_SECONDS`).
|
|
93
|
+
- `.env.example`: configuration template to copy to `.env` and adjust.
|
|
94
|
+
|
|
95
|
+
## Logs & Directories
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
~/.config/vibego/logs/
|
|
99
|
+
└─ codex/
|
|
100
|
+
└─ mall-backend/
|
|
101
|
+
├─ run_bot.log # output from run_bot.sh
|
|
102
|
+
├─ model.log # model CLI output captured through tmux pipe-pane
|
|
103
|
+
├─ bot.pid # current bot process PID (used by stop_bot.sh)
|
|
104
|
+
└─ current_session.txt # pointer to the latest JSONL session
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> Starting in 2025, all logs, databases, and state files default to `~/.config/vibego/`. Use
|
|
108
|
+
`./scripts/migrate_runtime.sh` to migrate legacy files created inside the repository back into the runtime directory.
|
|
109
|
+
|
|
110
|
+
## Model Switching
|
|
111
|
+
|
|
112
|
+
- Supported model parameters: `codex`, `claudecode`, `gemini` (placeholder).
|
|
113
|
+
- Switch flow: `stop_bot.sh --model <old>` → `run_bot.sh --model <new>`.
|
|
114
|
+
- Each model keeps an isolated configuration in `scripts/models/<model>.sh`; shared logic lives in
|
|
115
|
+
`scripts/models/common.sh`.
|
|
116
|
+
- `ACTIVE_MODEL` is echoed in `/start` replies and logs, and exported to the environment for `bot.py`.
|
|
117
|
+
|
|
118
|
+
### Codex
|
|
119
|
+
|
|
120
|
+
| Variable | Description |
|
|
121
|
+
|----------------------|----------------------------------------------------------------------------------|
|
|
122
|
+
| `CODEX_WORKDIR` | Codex CLI working directory (defaults to the value in `.env` or repository root) |
|
|
123
|
+
| `CODEX_CMD` | Launch command, default `codex --dangerously-bypass-...` |
|
|
124
|
+
| `CODEX_SESSION_ROOT` | JSONL root directory (default `~/.codex/sessions`) |
|
|
125
|
+
| `CODEX_SESSION_GLOB` | JSONL file pattern (default `rollout-*.jsonl`) |
|
|
126
|
+
|
|
127
|
+
### ClaudeCode
|
|
128
|
+
|
|
129
|
+
| Variable | Description |
|
|
130
|
+
|-----------------------|--------------------------------------------------------------|
|
|
131
|
+
| `CLAUDE_WORKDIR` | Project directory (defaults to the same value used by Codex) |
|
|
132
|
+
| `CLAUDE_CMD` | CLI launch command, for example `claude --project <path>` |
|
|
133
|
+
| `CLAUDE_PROJECT_ROOT` | JSONL root directory (default `~/.claude/projects`) |
|
|
134
|
+
| `CLAUDE_SESSION_GLOB` | JSONL file pattern (default `*.jsonl`) |
|
|
135
|
+
| `CLAUDE_PROJECT_KEY` | Optional: explicitly set `~/.claude/projects/<key>` |
|
|
136
|
+
|
|
137
|
+
### Gemini (placeholder)
|
|
138
|
+
|
|
139
|
+
- `scripts/models/gemini.sh` currently contains a placeholder command to be expanded once the official CLI is available.
|
|
140
|
+
|
|
141
|
+
## aiogram Worker Behaviour
|
|
142
|
+
|
|
143
|
+
- `/start`: returns `chat_id`, `MODE`, and `ACTIVE_MODEL`; logs record `chat_id` and `user_id`.
|
|
144
|
+
- Text messages:
|
|
145
|
+
1. Pick the `SessionAdapter` based on `ACTIVE_MODEL`, read `current_session.txt`, and search `MODEL_SESSION_ROOT` if
|
|
146
|
+
necessary.
|
|
147
|
+
2. Inject the prompt into tmux (send `Esc` to clear modes, `Ctrl+J` for newline, `Enter` to submit).
|
|
148
|
+
3. Initialise offsets from `SESSION_OFFSETS`; `_deliver_pending_messages()` streams tail updates from the JSONL log.
|
|
149
|
+
4. During the watcher phase, the bot informs the user the `ACTIVE_MODEL` is processing and pushes the result once
|
|
150
|
+
ready (Markdown preserved).
|
|
151
|
+
- MODE = A still honours `AGENT_CMD` for direct CLI execution.
|
|
152
|
+
|
|
153
|
+
## New Scripts
|
|
154
|
+
|
|
155
|
+
- `run_bot.sh`
|
|
156
|
+
- `--model <name>`: codex / claudecode / gemini.
|
|
157
|
+
- `--project <slug>`: directory name for logs/sessions; defaults to a slug derived from the working directory.
|
|
158
|
+
- `--foreground`: keep the process in the foreground (default: background via `nohup`).
|
|
159
|
+
- `--no-stop`: skip the pre-launch stop step (defaults to invoking `stop_bot.sh` for idempotency).
|
|
160
|
+
- `stop_bot.sh`
|
|
161
|
+
- Idempotent stop: issues `tmux kill-session`, terminates the process from `bot.pid`, and clears cached files.
|
|
162
|
+
- Example: `./scripts/stop_bot.sh --model codex --project mall-backend`.
|
|
163
|
+
|
|
164
|
+
## Configuration Highlights
|
|
165
|
+
|
|
166
|
+
### `.env` (master global configuration)
|
|
167
|
+
|
|
168
|
+
- Location: `~/.config/vibego/.env` (override with `VIBEGO_CONFIG_DIR`).
|
|
169
|
+
- `MASTER_BOT_TOKEN`: token for the master bot; collected by `vibego init` and required for startup.
|
|
170
|
+
- `MASTER_CHAT_ID` / `MASTER_USER_ID`: captured automatically the first time you interact with the master in Telegram to
|
|
171
|
+
mark authorised admins.
|
|
172
|
+
- `MASTER_WHITELIST`: comma-separated list of chat IDs. Leave empty to allow any chat; latest value always wins if auto
|
|
173
|
+
updates occur.
|
|
174
|
+
- You can add optional variables for proxies, log level, default model, etc. Scripts fall back to sensible defaults if
|
|
175
|
+
unspecified.
|
|
176
|
+
|
|
177
|
+
- Project definitions persist in `~/.config/vibego/config/master.db` (SQLite) with a JSON mirror at
|
|
178
|
+
`~/.config/vibego/config/projects.json`. For offline edits, use the `config/projects.json.example` template in the
|
|
179
|
+
repository.
|
|
180
|
+
- The master “⚙️ Project Management” menu can create/edit/delete projects; offline JSON edits are imported at startup
|
|
181
|
+
and synced to the database.
|
|
182
|
+
- Required fields: `bot_name`, `bot_token`, `project_slug`, `default_model`.
|
|
183
|
+
- Optional fields: `workdir` (project path), `allowed_chat_id` (pre-authorised chat). Leave blank to let the worker
|
|
184
|
+
capture the first valid chat and persist it to `~/.config/vibego/state/master_state.json`.
|
|
185
|
+
- Other custom fields are currently ignored.
|
|
186
|
+
|
|
187
|
+
### Automatic Authorisation & State
|
|
188
|
+
|
|
189
|
+
- If `allowed_chat_id` is empty when the worker starts, the first authorised message writes to `state/state.json` and is
|
|
190
|
+
applied immediately.
|
|
191
|
+
- Master restarts: call `stop_bot.sh` first, then restore running projects from the saved state.
|
|
192
|
+
|
|
193
|
+
## Roadmap
|
|
194
|
+
|
|
195
|
+
- Master bot will poll all project bots and invoke run/stop scripts to orchestrate workers; current version ships the
|
|
196
|
+
worker layout and logging standard first.
|
|
197
|
+
- Gemini CLI support will be added once an official integration path is available.
|
|
198
|
+
|
|
199
|
+
## Notes
|
|
200
|
+
|
|
201
|
+
- `~/.config/vibego/.env` stores sensitive tokens and admin metadata—never commit it.
|
|
202
|
+
- To trim log size, clean up `logs/<model>/<project>/` as needed or adjust script thresholds.
|
|
203
|
+
- If you previously ran an old source checkout, run `./scripts/migrate_runtime.sh` and ensure only `.example` templates
|
|
204
|
+
remain in the repository to avoid committing databases or logs.
|
|
205
|
+
- The master caches version checks and reminds you once per release; rerun `/projects` or restart the master to force a
|
|
206
|
+
new check.
|
|
207
|
+
|
|
208
|
+
## Master Control
|
|
209
|
+
|
|
210
|
+
- Launch the admin bot with `MASTER_BOT_TOKEN` (running `python master.py`).
|
|
211
|
+
- Master stores the project list in `~/.config/vibego/config/master.db`; use the project management menu or edit
|
|
212
|
+
`~/.config/vibego/config/projects.json` directly. Key fields:
|
|
213
|
+
- `bot_name`: Telegram bot username (with or without `@`; CLI adds `@` when displaying).
|
|
214
|
+
- `bot_token`: Telegram token for the worker.
|
|
215
|
+
- `default_model`: default model (codex / claudecode / gemini).
|
|
216
|
+
- `project_slug`: directory/log slug.
|
|
217
|
+
- `workdir`: project working directory (optional).
|
|
218
|
+
- `allowed_chat_id`: authorised chat injected into the runtime environment.
|
|
219
|
+
- State snapshot: `~/.config/vibego/state/master_state.json` records the active model and running status for each
|
|
220
|
+
project. On restart the master calls `stop_bot.sh`, then restores workers according to the state file.
|
|
221
|
+
|
|
222
|
+
### Management Commands
|
|
223
|
+
|
|
224
|
+
| Command | Description |
|
|
225
|
+
|-----------------------------|-----------------------------------------------------------------------------|
|
|
226
|
+
| `/projects` | List all projects with their current status and model. |
|
|
227
|
+
| `/run <project> [model]` | Start a project worker; optional `model` overrides the default/current one. |
|
|
228
|
+
| `/stop <project>` | Stop the project worker. |
|
|
229
|
+
| `/switch <project> <model>` | Stop the worker and relaunch it with a new model. |
|
|
230
|
+
| `/start` | Show help and the total number of projects. |
|
|
231
|
+
| `/upgrade` | Run `pipx upgrade vibego && vibego stop && vibego start` to self-upgrade. |
|
|
232
|
+
|
|
233
|
+
- `<project>` accepts either the `project_slug` or `@bot_name`. Responses automatically render clickable `@` links.
|
|
234
|
+
|
|
235
|
+
> The master only interacts with the admin bot. Project bots continue to handle business traffic through the worker (
|
|
236
|
+
`bot.py`) launched by `run_bot.sh`.
|