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.

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
- """将遗留的 UTC 字符串转换为上海时区表示。"""
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
- """将历史任务 ID 的连字符/点号改写为下划线格式,保障 Telegram 命令可点击。"""
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("检测到旧版任务 ID,开始迁移: project=%s", self.project_slug)
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
- "任务 ID 迁移检测到无法规范化的值: project=%s value=%s",
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("任务 ID 迁移失败:存在无法规范化的 ID")
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
- "任务 ID 迁移检测到潜在冲突: project=%s old=%s new=%s",
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("任务 ID 迁移冲突:目标 ID 已存在")
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
- "任务 ID 迁移检测到冲突: project=%s old=%s new=%s",
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("任务 ID 迁移冲突")
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
- "任务 ID 迁移完成: project=%s changed=%s",
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("已归档遗留子任务: project=%s count=%s", self.project_slug, changed)
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
- "任务状态检查发现 NULL 值: project=%s",
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
- "任务状态检查发现无法识别的值: project=%s value=%s",
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
- """根据任务 ID 返回任务详情,不存在时返回 None"""
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
- "任务状态入参已自动修正: task_id=%s raw=%s normalized=%s",
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("事件 payload 序列化失败: task_id=%s error=%s", task_id, exc)
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
- """统一任务 ID 的分隔符,兼容历史格式。"""
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
- # 旧格式 TASK0001/TASK0001_1 需要补下划线
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
- """将外部传入的任务 ID 规范化为统一格式。"""
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
- """将任务 ID 迁移结果记录为 JSON 报告,便于排查。"""
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
- "写入任务 ID 迁移报告失败: project=%s error=%s",
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
- """自增并返回 root 任务序列号。"""
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
- """将状态字符串标准化,兼容遗留 design 并记录异常数据。"""
1047
+ """Normalise status strings, providing compatibility with legacy aliases."""
1048
1048
 
1049
1049
  if not value:
1050
- logger.warning("检测到空任务状态,已回退默认: context=%s", context)
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
- "检测到未知任务状态: value=%s mapped=%s context=%s",
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
- "任务状态已根据别名转换: raw=%s normalized=%s context=%s",
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
- """ sqlite row 转换为 TaskRecord 实例。"""
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`.