yee88 0.3.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.
Files changed (103) hide show
  1. yee88/__init__.py +1 -0
  2. yee88/api.py +116 -0
  3. yee88/backends.py +25 -0
  4. yee88/backends_helpers.py +14 -0
  5. yee88/cli/__init__.py +228 -0
  6. yee88/cli/config.py +320 -0
  7. yee88/cli/doctor.py +173 -0
  8. yee88/cli/init.py +113 -0
  9. yee88/cli/onboarding_cmd.py +126 -0
  10. yee88/cli/plugins.py +196 -0
  11. yee88/cli/run.py +419 -0
  12. yee88/cli/topic.py +355 -0
  13. yee88/commands.py +134 -0
  14. yee88/config.py +142 -0
  15. yee88/config_migrations.py +124 -0
  16. yee88/config_watch.py +146 -0
  17. yee88/context.py +9 -0
  18. yee88/directives.py +146 -0
  19. yee88/engines.py +53 -0
  20. yee88/events.py +170 -0
  21. yee88/ids.py +17 -0
  22. yee88/lockfile.py +158 -0
  23. yee88/logging.py +283 -0
  24. yee88/markdown.py +298 -0
  25. yee88/model.py +77 -0
  26. yee88/plugins.py +312 -0
  27. yee88/presenter.py +25 -0
  28. yee88/progress.py +99 -0
  29. yee88/router.py +113 -0
  30. yee88/runner.py +712 -0
  31. yee88/runner_bridge.py +619 -0
  32. yee88/runners/__init__.py +1 -0
  33. yee88/runners/claude.py +483 -0
  34. yee88/runners/codex.py +656 -0
  35. yee88/runners/mock.py +221 -0
  36. yee88/runners/opencode.py +505 -0
  37. yee88/runners/pi.py +523 -0
  38. yee88/runners/run_options.py +39 -0
  39. yee88/runners/tool_actions.py +90 -0
  40. yee88/runtime_loader.py +207 -0
  41. yee88/scheduler.py +159 -0
  42. yee88/schemas/__init__.py +1 -0
  43. yee88/schemas/claude.py +238 -0
  44. yee88/schemas/codex.py +169 -0
  45. yee88/schemas/opencode.py +51 -0
  46. yee88/schemas/pi.py +117 -0
  47. yee88/settings.py +360 -0
  48. yee88/telegram/__init__.py +20 -0
  49. yee88/telegram/api_models.py +37 -0
  50. yee88/telegram/api_schemas.py +152 -0
  51. yee88/telegram/backend.py +163 -0
  52. yee88/telegram/bridge.py +425 -0
  53. yee88/telegram/chat_prefs.py +242 -0
  54. yee88/telegram/chat_sessions.py +112 -0
  55. yee88/telegram/client.py +409 -0
  56. yee88/telegram/client_api.py +539 -0
  57. yee88/telegram/commands/__init__.py +12 -0
  58. yee88/telegram/commands/agent.py +196 -0
  59. yee88/telegram/commands/cancel.py +116 -0
  60. yee88/telegram/commands/dispatch.py +111 -0
  61. yee88/telegram/commands/executor.py +449 -0
  62. yee88/telegram/commands/file_transfer.py +586 -0
  63. yee88/telegram/commands/handlers.py +45 -0
  64. yee88/telegram/commands/media.py +143 -0
  65. yee88/telegram/commands/menu.py +139 -0
  66. yee88/telegram/commands/model.py +215 -0
  67. yee88/telegram/commands/overrides.py +159 -0
  68. yee88/telegram/commands/parse.py +30 -0
  69. yee88/telegram/commands/plan.py +16 -0
  70. yee88/telegram/commands/reasoning.py +234 -0
  71. yee88/telegram/commands/reply.py +23 -0
  72. yee88/telegram/commands/topics.py +332 -0
  73. yee88/telegram/commands/trigger.py +143 -0
  74. yee88/telegram/context.py +140 -0
  75. yee88/telegram/engine_defaults.py +86 -0
  76. yee88/telegram/engine_overrides.py +105 -0
  77. yee88/telegram/files.py +178 -0
  78. yee88/telegram/loop.py +1822 -0
  79. yee88/telegram/onboarding.py +1088 -0
  80. yee88/telegram/outbox.py +177 -0
  81. yee88/telegram/parsing.py +239 -0
  82. yee88/telegram/render.py +198 -0
  83. yee88/telegram/state_store.py +88 -0
  84. yee88/telegram/topic_state.py +334 -0
  85. yee88/telegram/topics.py +256 -0
  86. yee88/telegram/trigger_mode.py +68 -0
  87. yee88/telegram/types.py +63 -0
  88. yee88/telegram/voice.py +110 -0
  89. yee88/transport.py +53 -0
  90. yee88/transport_runtime.py +323 -0
  91. yee88/transports.py +76 -0
  92. yee88/utils/__init__.py +1 -0
  93. yee88/utils/git.py +87 -0
  94. yee88/utils/json_state.py +21 -0
  95. yee88/utils/paths.py +47 -0
  96. yee88/utils/streams.py +44 -0
  97. yee88/utils/subprocess.py +86 -0
  98. yee88/worktrees.py +135 -0
  99. yee88-0.3.0.dist-info/METADATA +116 -0
  100. yee88-0.3.0.dist-info/RECORD +103 -0
  101. yee88-0.3.0.dist-info/WHEEL +4 -0
  102. yee88-0.3.0.dist-info/entry_points.txt +11 -0
  103. yee88-0.3.0.dist-info/licenses/LICENSE +21 -0
yee88/progress.py ADDED
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from collections.abc import Callable
5
+
6
+ from .model import Action, ActionEvent, ResumeToken, StartedEvent, TakopiEvent
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class ActionState:
11
+ action: Action
12
+ phase: str
13
+ ok: bool | None
14
+ display_phase: str
15
+ completed: bool
16
+ first_seen: int
17
+ last_update: int
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class ProgressState:
22
+ engine: str
23
+ action_count: int
24
+ actions: tuple[ActionState, ...]
25
+ resume: ResumeToken | None
26
+ resume_line: str | None
27
+ context_line: str | None
28
+
29
+
30
+ class ProgressTracker:
31
+ def __init__(self, *, engine: str) -> None:
32
+ self.engine = engine
33
+ self.resume: ResumeToken | None = None
34
+ self.action_count = 0
35
+ self._actions: dict[str, ActionState] = {}
36
+ self._seq = 0
37
+
38
+ def note_event(self, event: TakopiEvent) -> bool:
39
+ match event:
40
+ case StartedEvent(resume=resume):
41
+ self.resume = resume
42
+ return True
43
+ case ActionEvent(action=action, phase=phase, ok=ok):
44
+ if action.kind == "turn":
45
+ return False
46
+ action_id = str(action.id or "")
47
+ if not action_id:
48
+ return False
49
+ completed = phase == "completed"
50
+ existing = self._actions.get(action_id)
51
+ has_open = existing is not None and not existing.completed
52
+ is_update = phase == "updated" or (phase == "started" and has_open)
53
+ display_phase = "updated" if is_update and not completed else phase
54
+
55
+ self._seq += 1
56
+ seq = self._seq
57
+
58
+ if existing is None:
59
+ self.action_count += 1
60
+ first_seen = seq
61
+ else:
62
+ first_seen = existing.first_seen
63
+ self._actions[action_id] = ActionState(
64
+ action=action,
65
+ phase=phase,
66
+ ok=ok,
67
+ display_phase=display_phase,
68
+ completed=completed,
69
+ first_seen=first_seen,
70
+ last_update=seq,
71
+ )
72
+ return True
73
+ case _:
74
+ return False
75
+
76
+ def set_resume(self, resume: ResumeToken | None) -> None:
77
+ if resume is not None:
78
+ self.resume = resume
79
+
80
+ def snapshot(
81
+ self,
82
+ *,
83
+ resume_formatter: Callable[[ResumeToken], str] | None = None,
84
+ context_line: str | None = None,
85
+ ) -> ProgressState:
86
+ resume_line: str | None = None
87
+ if self.resume is not None and resume_formatter is not None:
88
+ resume_line = resume_formatter(self.resume)
89
+ actions = tuple(
90
+ sorted(self._actions.values(), key=lambda item: item.first_seen)
91
+ )
92
+ return ProgressState(
93
+ engine=self.engine,
94
+ action_count=self.action_count,
95
+ actions=actions,
96
+ resume=self.resume,
97
+ resume_line=resume_line,
98
+ context_line=context_line,
99
+ )
yee88/router.py ADDED
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+ from collections.abc import Iterable
6
+
7
+ from .model import EngineId, ResumeToken
8
+ from .runner import Runner
9
+
10
+
11
+ class RunnerUnavailableError(RuntimeError):
12
+ def __init__(self, engine: EngineId, issue: str | None = None) -> None:
13
+ message = f"engine {engine!r} is unavailable"
14
+ if issue:
15
+ message = f"{message}: {issue}"
16
+ super().__init__(message)
17
+ self.engine = engine
18
+ self.issue = issue
19
+
20
+
21
+ type EngineStatus = Literal["ok", "missing_cli", "bad_config", "load_error"]
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class RunnerEntry:
26
+ engine: EngineId
27
+ runner: Runner
28
+ status: EngineStatus = "ok"
29
+ issue: str | None = None
30
+
31
+ @property
32
+ def available(self) -> bool:
33
+ # "bad_config" means we ignored user config and built the runner with defaults.
34
+ # The engine is still runnable, but a warning should be surfaced to the user.
35
+ return self.status in {"ok", "bad_config"}
36
+
37
+
38
+ class AutoRouter:
39
+ def __init__(
40
+ self, entries: Iterable[RunnerEntry], default_engine: EngineId
41
+ ) -> None:
42
+ self._entries = tuple(entries)
43
+ if not self._entries:
44
+ raise ValueError("AutoRouter requires at least one runner.")
45
+ by_engine: dict[EngineId, RunnerEntry] = {}
46
+ for entry in self._entries:
47
+ if entry.engine in by_engine:
48
+ raise ValueError(f"duplicate runner engine: {entry.engine}")
49
+ by_engine[entry.engine] = entry
50
+ if default_engine not in by_engine:
51
+ raise ValueError(f"default engine {default_engine!r} is not configured")
52
+ self._by_engine = by_engine
53
+ self.default_engine = default_engine
54
+
55
+ @property
56
+ def entries(self) -> tuple[RunnerEntry, ...]:
57
+ return self._entries
58
+
59
+ @property
60
+ def available_entries(self) -> tuple[RunnerEntry, ...]:
61
+ return tuple(entry for entry in self._entries if entry.available)
62
+
63
+ @property
64
+ def engine_ids(self) -> tuple[EngineId, ...]:
65
+ return tuple(entry.engine for entry in self._entries)
66
+
67
+ @property
68
+ def default_entry(self) -> RunnerEntry:
69
+ return self._by_engine[self.default_engine]
70
+
71
+ def entry_for_engine(self, engine: EngineId | None) -> RunnerEntry:
72
+ engine = self.default_engine if engine is None else engine
73
+ entry = self._by_engine.get(engine)
74
+ if entry is None:
75
+ raise RunnerUnavailableError(engine, "engine not configured")
76
+ return entry
77
+
78
+ def entry_for(self, resume: ResumeToken | None) -> RunnerEntry:
79
+ if resume is None:
80
+ return self.entry_for_engine(None)
81
+ return self.entry_for_engine(resume.engine)
82
+
83
+ def runner_for(self, resume: ResumeToken | None) -> Runner:
84
+ entry = self.entry_for(resume)
85
+ if not entry.available:
86
+ raise RunnerUnavailableError(entry.engine, entry.issue)
87
+ return entry.runner
88
+
89
+ def format_resume(self, token: ResumeToken) -> str:
90
+ entry = self.entry_for(token)
91
+ return entry.runner.format_resume(token)
92
+
93
+ def extract_resume(self, text: str | None) -> ResumeToken | None:
94
+ if not text:
95
+ return None
96
+ for entry in self._entries:
97
+ token = entry.runner.extract_resume(text)
98
+ if token is not None:
99
+ return token
100
+ return None
101
+
102
+ def resolve_resume(
103
+ self, text: str | None, reply_text: str | None
104
+ ) -> ResumeToken | None:
105
+ token = self.extract_resume(text)
106
+ if token is not None:
107
+ return token
108
+ if reply_text is None:
109
+ return None
110
+ return self.extract_resume(reply_text)
111
+
112
+ def is_resume_line(self, line: str) -> bool:
113
+ return any(entry.runner.is_resume_line(line) for entry in self._entries)