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
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ...context import RunContext
6
+ from ...directives import DirectiveError
7
+ from ..chat_prefs import ChatPrefsStore
8
+ from ..engine_defaults import resolve_engine_for_message
9
+ from ..engine_overrides import resolve_override_value
10
+ from ..files import split_command_args
11
+ from ..topic_state import TopicStateStore
12
+ from ..topics import _topic_key
13
+ from ..types import TelegramIncomingMessage
14
+ from .reply import make_reply
15
+
16
+ if TYPE_CHECKING:
17
+ from ..bridge import TelegramBridgeConfig
18
+
19
+ AGENT_USAGE = "usage: `/agent`, `/agent set <engine>`, or `/agent clear`"
20
+
21
+
22
+ async def _check_agent_permissions(
23
+ cfg: TelegramBridgeConfig, msg: TelegramIncomingMessage
24
+ ) -> bool:
25
+ reply = make_reply(cfg, msg)
26
+ sender_id = msg.sender_id
27
+ if sender_id is None:
28
+ await reply(text="cannot verify sender for engine defaults.")
29
+ return False
30
+ if msg.is_private:
31
+ return True
32
+ member = await cfg.bot.get_chat_member(msg.chat_id, sender_id)
33
+ if member is None:
34
+ await reply(text="failed to verify engine permissions.")
35
+ return False
36
+ if member.status in {"creator", "administrator"}:
37
+ return True
38
+ await reply(text="changing default engines is restricted to group admins.")
39
+ return False
40
+
41
+
42
+ async def _handle_agent_command(
43
+ cfg: TelegramBridgeConfig,
44
+ msg: TelegramIncomingMessage,
45
+ args_text: str,
46
+ ambient_context: RunContext | None,
47
+ topic_store: TopicStateStore | None,
48
+ chat_prefs: ChatPrefsStore | None,
49
+ *,
50
+ resolved_scope: str | None = None,
51
+ scope_chat_ids: frozenset[int] | None = None,
52
+ ) -> None:
53
+ reply = make_reply(cfg, msg)
54
+ tkey = (
55
+ _topic_key(msg, cfg, scope_chat_ids=scope_chat_ids)
56
+ if topic_store is not None
57
+ else None
58
+ )
59
+ tokens = split_command_args(args_text)
60
+ action = tokens[0].lower() if tokens else "show"
61
+
62
+ if action in {"show", ""}:
63
+ try:
64
+ resolved = cfg.runtime.resolve_message(
65
+ text="",
66
+ reply_text=msg.reply_to_text,
67
+ ambient_context=ambient_context,
68
+ chat_id=msg.chat_id,
69
+ )
70
+ except DirectiveError as exc:
71
+ await reply(text=f"error:\n{exc}")
72
+ return
73
+ selection = await resolve_engine_for_message(
74
+ runtime=cfg.runtime,
75
+ context=resolved.context,
76
+ explicit_engine=None,
77
+ chat_id=msg.chat_id,
78
+ topic_key=tkey,
79
+ topic_store=topic_store,
80
+ chat_prefs=chat_prefs,
81
+ )
82
+ source_labels = {
83
+ "directive": "directive",
84
+ "topic_default": "topic default",
85
+ "chat_default": "chat default",
86
+ "project_default": "project default",
87
+ "global_default": "global default",
88
+ }
89
+ agent_line = f"engine: {selection.engine} ({source_labels[selection.source]})"
90
+ topic_override = None
91
+ if tkey is not None and topic_store is not None:
92
+ topic_override = await topic_store.get_engine_override(
93
+ tkey[0], tkey[1], selection.engine
94
+ )
95
+ chat_override = None
96
+ if chat_prefs is not None:
97
+ chat_override = await chat_prefs.get_engine_override(
98
+ msg.chat_id, selection.engine
99
+ )
100
+ override_labels = {
101
+ "topic_override": "topic override",
102
+ "chat_default": "chat default",
103
+ "default": "no override",
104
+ }
105
+ model_resolution = resolve_override_value(
106
+ topic_override=topic_override,
107
+ chat_override=chat_override,
108
+ field="model",
109
+ )
110
+ reasoning_resolution = resolve_override_value(
111
+ topic_override=topic_override,
112
+ chat_override=chat_override,
113
+ field="reasoning",
114
+ )
115
+ model_value = model_resolution.value or "default"
116
+ model_line = (
117
+ f"model: {model_value} ({override_labels[model_resolution.source]})"
118
+ )
119
+ reasoning_value = reasoning_resolution.value or "default"
120
+ reasoning_line = (
121
+ "reasoning: "
122
+ f"{reasoning_value} ({override_labels[reasoning_resolution.source]})"
123
+ )
124
+ topic_default = selection.topic_default or "none"
125
+ if tkey is None:
126
+ topic_default = "none"
127
+ if chat_prefs is None:
128
+ chat_default = "unavailable"
129
+ else:
130
+ chat_default = selection.chat_default or "none"
131
+ project_default = (
132
+ selection.project_default
133
+ if selection.project_default is not None
134
+ else "none"
135
+ )
136
+ defaults_line = (
137
+ "defaults: "
138
+ f"topic: {topic_default}, "
139
+ f"chat: {chat_default}, "
140
+ f"project: {project_default}, "
141
+ f"global: {cfg.runtime.default_engine}"
142
+ )
143
+ available = ", ".join(cfg.runtime.engine_ids)
144
+ available_line = f"available: {available}"
145
+ await reply(
146
+ text="\n\n".join(
147
+ [agent_line, model_line, reasoning_line, defaults_line, available_line]
148
+ )
149
+ )
150
+ return
151
+
152
+ if action == "set":
153
+ if len(tokens) < 2:
154
+ await reply(text=AGENT_USAGE)
155
+ return
156
+ if not await _check_agent_permissions(cfg, msg):
157
+ return
158
+ engine = tokens[1].strip().lower()
159
+ if engine not in cfg.runtime.engine_ids:
160
+ available = ", ".join(cfg.runtime.engine_ids)
161
+ await reply(
162
+ text=f"unknown engine `{engine}`.\navailable engines: `{available}`",
163
+ )
164
+ return
165
+ if tkey is not None:
166
+ if topic_store is None:
167
+ await reply(text="topic defaults are unavailable.")
168
+ return
169
+ await topic_store.set_default_engine(tkey[0], tkey[1], engine)
170
+ await reply(text=f"topic default engine set to `{engine}`")
171
+ return
172
+ if chat_prefs is None:
173
+ await reply(text="chat defaults are unavailable (no config path).")
174
+ return
175
+ await chat_prefs.set_default_engine(msg.chat_id, engine)
176
+ await reply(text=f"chat default engine set to `{engine}`")
177
+ return
178
+
179
+ if action == "clear":
180
+ if not await _check_agent_permissions(cfg, msg):
181
+ return
182
+ if tkey is not None:
183
+ if topic_store is None:
184
+ await reply(text="topic defaults are unavailable.")
185
+ return
186
+ await topic_store.clear_default_engine(tkey[0], tkey[1])
187
+ await reply(text="topic default engine cleared.")
188
+ return
189
+ if chat_prefs is None:
190
+ await reply(text="chat defaults are unavailable (no config path).")
191
+ return
192
+ await chat_prefs.clear_default_engine(msg.chat_id)
193
+ await reply(text="chat default engine cleared.")
194
+ return
195
+
196
+ await reply(text=AGENT_USAGE)
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ...logging import get_logger
6
+ from ...progress import ProgressTracker
7
+ from ...runner_bridge import RunningTasks
8
+ from ...scheduler import ThreadJob, ThreadScheduler
9
+ from ...transport import MessageRef
10
+ from ..types import TelegramCallbackQuery, TelegramIncomingMessage
11
+ from .reply import make_reply
12
+
13
+ if TYPE_CHECKING:
14
+ from ..bridge import TelegramBridgeConfig
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ async def handle_cancel(
20
+ cfg: TelegramBridgeConfig,
21
+ msg: TelegramIncomingMessage,
22
+ running_tasks: RunningTasks,
23
+ scheduler: ThreadScheduler | None = None,
24
+ ) -> None:
25
+ reply = make_reply(cfg, msg)
26
+ chat_id = msg.chat_id
27
+ reply_id = msg.reply_to_message_id
28
+
29
+ if reply_id is None:
30
+ if msg.reply_to_text:
31
+ await reply(text="nothing is currently running for that message.")
32
+ return
33
+ await reply(text="reply to the progress message to cancel.")
34
+ return
35
+
36
+ progress_ref = MessageRef(channel_id=chat_id, message_id=reply_id)
37
+ running_task = running_tasks.get(progress_ref)
38
+ if running_task is None:
39
+ if scheduler is not None:
40
+ job = await scheduler.cancel_queued(chat_id, reply_id)
41
+ if job is not None:
42
+ logger.info(
43
+ "cancel.queued",
44
+ chat_id=chat_id,
45
+ progress_message_id=reply_id,
46
+ resume=job.resume_token.value,
47
+ )
48
+ await _edit_cancelled_message(cfg, progress_ref, job)
49
+ return
50
+ await reply(text="nothing is currently running for that message.")
51
+ return
52
+
53
+ logger.info(
54
+ "cancel.requested",
55
+ chat_id=chat_id,
56
+ progress_message_id=reply_id,
57
+ )
58
+ running_task.cancel_requested.set()
59
+
60
+
61
+ async def handle_callback_cancel(
62
+ cfg: TelegramBridgeConfig,
63
+ query: TelegramCallbackQuery,
64
+ running_tasks: RunningTasks,
65
+ scheduler: ThreadScheduler | None = None,
66
+ ) -> None:
67
+ progress_ref = MessageRef(channel_id=query.chat_id, message_id=query.message_id)
68
+ running_task = running_tasks.get(progress_ref)
69
+ if running_task is None:
70
+ if scheduler is not None:
71
+ job = await scheduler.cancel_queued(query.chat_id, query.message_id)
72
+ if job is not None:
73
+ logger.info(
74
+ "cancel.queued",
75
+ chat_id=query.chat_id,
76
+ progress_message_id=query.message_id,
77
+ resume=job.resume_token.value,
78
+ )
79
+ await _edit_cancelled_message(cfg, progress_ref, job)
80
+ await cfg.bot.answer_callback_query(
81
+ callback_query_id=query.callback_query_id,
82
+ text="dropped from queue.",
83
+ )
84
+ return
85
+ await cfg.bot.answer_callback_query(
86
+ callback_query_id=query.callback_query_id,
87
+ text="nothing is currently running for that message.",
88
+ )
89
+ return
90
+ logger.info(
91
+ "cancel.requested",
92
+ chat_id=query.chat_id,
93
+ progress_message_id=query.message_id,
94
+ )
95
+ running_task.cancel_requested.set()
96
+ await cfg.bot.answer_callback_query(
97
+ callback_query_id=query.callback_query_id,
98
+ text="cancelling...",
99
+ )
100
+
101
+
102
+ async def _edit_cancelled_message(
103
+ cfg: TelegramBridgeConfig,
104
+ progress_ref: MessageRef,
105
+ job: ThreadJob,
106
+ ) -> None:
107
+ tracker = ProgressTracker(engine=job.resume_token.engine)
108
+ tracker.set_resume(job.resume_token)
109
+ context_line = cfg.runtime.format_context_line(job.context)
110
+ state = tracker.snapshot(context_line=context_line)
111
+ message = cfg.exec_cfg.presenter.render_progress(
112
+ state,
113
+ elapsed_s=0.0,
114
+ label="`cancelled`",
115
+ )
116
+ await cfg.exec_cfg.transport.edit(ref=progress_ref, message=message)
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import TYPE_CHECKING
5
+
6
+ import anyio
7
+
8
+ from ...commands import CommandContext, get_command
9
+ from ...config import ConfigError
10
+ from ...logging import get_logger
11
+ from ...model import EngineId, ResumeToken
12
+ from ...runners.run_options import EngineRunOptions
13
+ from ...runner_bridge import RunningTasks
14
+ from ...scheduler import ThreadScheduler
15
+ from ...transport import MessageRef
16
+ from ..files import split_command_args
17
+ from ..types import TelegramIncomingMessage
18
+ from .executor import _TelegramCommandExecutor
19
+
20
+ if TYPE_CHECKING:
21
+ from ..bridge import TelegramBridgeConfig
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ async def _dispatch_command(
27
+ cfg: TelegramBridgeConfig,
28
+ msg: TelegramIncomingMessage,
29
+ text: str,
30
+ command_id: str,
31
+ args_text: str,
32
+ running_tasks: RunningTasks,
33
+ scheduler: ThreadScheduler,
34
+ on_thread_known: Callable[[ResumeToken, anyio.Event], Awaitable[None]] | None,
35
+ stateful_mode: bool,
36
+ default_engine_override: EngineId | None,
37
+ engine_overrides_resolver: Callable[[EngineId], Awaitable[EngineRunOptions | None]]
38
+ | None,
39
+ ) -> None:
40
+ allowlist = cfg.runtime.allowlist
41
+ chat_id = msg.chat_id
42
+ user_msg_id = msg.message_id
43
+ reply_ref = (
44
+ MessageRef(
45
+ channel_id=chat_id,
46
+ message_id=msg.reply_to_message_id,
47
+ thread_id=msg.thread_id,
48
+ )
49
+ if msg.reply_to_message_id is not None
50
+ else None
51
+ )
52
+ executor = _TelegramCommandExecutor(
53
+ exec_cfg=cfg.exec_cfg,
54
+ runtime=cfg.runtime,
55
+ running_tasks=running_tasks,
56
+ scheduler=scheduler,
57
+ on_thread_known=on_thread_known,
58
+ engine_overrides_resolver=engine_overrides_resolver,
59
+ chat_id=chat_id,
60
+ user_msg_id=user_msg_id,
61
+ thread_id=msg.thread_id,
62
+ show_resume_line=cfg.show_resume_line,
63
+ stateful_mode=stateful_mode,
64
+ default_engine_override=default_engine_override,
65
+ )
66
+ message_ref = MessageRef(
67
+ channel_id=chat_id,
68
+ message_id=user_msg_id,
69
+ thread_id=msg.thread_id,
70
+ sender_id=msg.sender_id,
71
+ raw=msg.raw,
72
+ )
73
+ try:
74
+ backend = get_command(command_id, allowlist=allowlist, required=False)
75
+ except ConfigError as exc:
76
+ await executor.send(f"error:\n{exc}", reply_to=message_ref, notify=True)
77
+ return
78
+ if backend is None:
79
+ return
80
+ try:
81
+ plugin_config = cfg.runtime.plugin_config(command_id)
82
+ except ConfigError as exc:
83
+ await executor.send(f"error:\n{exc}", reply_to=message_ref, notify=True)
84
+ return
85
+ ctx = CommandContext(
86
+ command=command_id,
87
+ text=text,
88
+ args_text=args_text,
89
+ args=split_command_args(args_text),
90
+ message=message_ref,
91
+ reply_to=reply_ref,
92
+ reply_text=msg.reply_to_text,
93
+ config_path=cfg.runtime.config_path,
94
+ plugin_config=plugin_config,
95
+ runtime=cfg.runtime,
96
+ executor=executor,
97
+ )
98
+ try:
99
+ result = await backend.handle(ctx)
100
+ except Exception as exc:
101
+ logger.exception(
102
+ "command.failed",
103
+ command=command_id,
104
+ error=str(exc),
105
+ error_type=exc.__class__.__name__,
106
+ )
107
+ await executor.send(f"error:\n{exc}", reply_to=message_ref, notify=True)
108
+ return
109
+ if result is not None:
110
+ reply_to = message_ref if result.reply_to is None else result.reply_to
111
+ await executor.send(result.text, reply_to=reply_to, notify=result.notify)