yee88 0.1.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.
- takopi/__init__.py +1 -0
- takopi/api.py +116 -0
- takopi/backends.py +25 -0
- takopi/backends_helpers.py +14 -0
- takopi/cli/__init__.py +228 -0
- takopi/cli/config.py +320 -0
- takopi/cli/doctor.py +173 -0
- takopi/cli/init.py +113 -0
- takopi/cli/onboarding_cmd.py +126 -0
- takopi/cli/plugins.py +196 -0
- takopi/cli/run.py +419 -0
- takopi/cli/topic.py +355 -0
- takopi/commands.py +134 -0
- takopi/config.py +142 -0
- takopi/config_migrations.py +124 -0
- takopi/config_watch.py +146 -0
- takopi/context.py +9 -0
- takopi/directives.py +146 -0
- takopi/engines.py +53 -0
- takopi/events.py +170 -0
- takopi/ids.py +17 -0
- takopi/lockfile.py +158 -0
- takopi/logging.py +283 -0
- takopi/markdown.py +298 -0
- takopi/model.py +77 -0
- takopi/plugins.py +312 -0
- takopi/presenter.py +25 -0
- takopi/progress.py +99 -0
- takopi/router.py +113 -0
- takopi/runner.py +712 -0
- takopi/runner_bridge.py +619 -0
- takopi/runners/__init__.py +1 -0
- takopi/runners/claude.py +483 -0
- takopi/runners/codex.py +656 -0
- takopi/runners/mock.py +221 -0
- takopi/runners/opencode.py +505 -0
- takopi/runners/pi.py +523 -0
- takopi/runners/run_options.py +39 -0
- takopi/runners/tool_actions.py +90 -0
- takopi/runtime_loader.py +207 -0
- takopi/scheduler.py +159 -0
- takopi/schemas/__init__.py +1 -0
- takopi/schemas/claude.py +238 -0
- takopi/schemas/codex.py +169 -0
- takopi/schemas/opencode.py +51 -0
- takopi/schemas/pi.py +117 -0
- takopi/settings.py +360 -0
- takopi/telegram/__init__.py +20 -0
- takopi/telegram/api_models.py +37 -0
- takopi/telegram/api_schemas.py +152 -0
- takopi/telegram/backend.py +163 -0
- takopi/telegram/bridge.py +425 -0
- takopi/telegram/chat_prefs.py +242 -0
- takopi/telegram/chat_sessions.py +112 -0
- takopi/telegram/client.py +409 -0
- takopi/telegram/client_api.py +539 -0
- takopi/telegram/commands/__init__.py +12 -0
- takopi/telegram/commands/agent.py +196 -0
- takopi/telegram/commands/cancel.py +116 -0
- takopi/telegram/commands/dispatch.py +111 -0
- takopi/telegram/commands/executor.py +449 -0
- takopi/telegram/commands/file_transfer.py +586 -0
- takopi/telegram/commands/handlers.py +45 -0
- takopi/telegram/commands/media.py +143 -0
- takopi/telegram/commands/menu.py +139 -0
- takopi/telegram/commands/model.py +215 -0
- takopi/telegram/commands/overrides.py +159 -0
- takopi/telegram/commands/parse.py +30 -0
- takopi/telegram/commands/plan.py +16 -0
- takopi/telegram/commands/reasoning.py +234 -0
- takopi/telegram/commands/reply.py +23 -0
- takopi/telegram/commands/topics.py +332 -0
- takopi/telegram/commands/trigger.py +143 -0
- takopi/telegram/context.py +140 -0
- takopi/telegram/engine_defaults.py +86 -0
- takopi/telegram/engine_overrides.py +105 -0
- takopi/telegram/files.py +178 -0
- takopi/telegram/loop.py +1822 -0
- takopi/telegram/onboarding.py +1088 -0
- takopi/telegram/outbox.py +177 -0
- takopi/telegram/parsing.py +239 -0
- takopi/telegram/render.py +198 -0
- takopi/telegram/state_store.py +88 -0
- takopi/telegram/topic_state.py +334 -0
- takopi/telegram/topics.py +256 -0
- takopi/telegram/trigger_mode.py +68 -0
- takopi/telegram/types.py +63 -0
- takopi/telegram/voice.py +110 -0
- takopi/transport.py +53 -0
- takopi/transport_runtime.py +323 -0
- takopi/transports.py +76 -0
- takopi/utils/__init__.py +1 -0
- takopi/utils/git.py +87 -0
- takopi/utils/json_state.py +21 -0
- takopi/utils/paths.py +47 -0
- takopi/utils/streams.py +44 -0
- takopi/utils/subprocess.py +86 -0
- takopi/worktrees.py +135 -0
- yee88-0.1.0.dist-info/METADATA +116 -0
- yee88-0.1.0.dist-info/RECORD +103 -0
- yee88-0.1.0.dist-info/WHEEL +4 -0
- yee88-0.1.0.dist-info/entry_points.txt +11 -0
- yee88-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ...context import RunContext
|
|
6
|
+
from ..chat_prefs import ChatPrefsStore
|
|
7
|
+
from ..engine_overrides import (
|
|
8
|
+
EngineOverrides,
|
|
9
|
+
allowed_reasoning_levels,
|
|
10
|
+
resolve_override_value,
|
|
11
|
+
)
|
|
12
|
+
from ..files import split_command_args
|
|
13
|
+
from ..topic_state import TopicStateStore
|
|
14
|
+
from ..topics import _topic_key
|
|
15
|
+
from ..types import TelegramIncomingMessage
|
|
16
|
+
from .overrides import (
|
|
17
|
+
ENGINE_SOURCE_LABELS,
|
|
18
|
+
OVERRIDE_SOURCE_LABELS,
|
|
19
|
+
apply_engine_override,
|
|
20
|
+
parse_set_args,
|
|
21
|
+
require_admin_or_private,
|
|
22
|
+
resolve_engine_selection,
|
|
23
|
+
)
|
|
24
|
+
from .reply import make_reply
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from ..bridge import TelegramBridgeConfig
|
|
28
|
+
|
|
29
|
+
REASONING_USAGE = (
|
|
30
|
+
"usage: `/reasoning`, `/reasoning set <level>`, "
|
|
31
|
+
"`/reasoning set <engine> <level>`, or `/reasoning clear [engine]`"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _handle_reasoning_command(
|
|
36
|
+
cfg: TelegramBridgeConfig,
|
|
37
|
+
msg: TelegramIncomingMessage,
|
|
38
|
+
args_text: str,
|
|
39
|
+
ambient_context: RunContext | None,
|
|
40
|
+
topic_store: TopicStateStore | None,
|
|
41
|
+
chat_prefs: ChatPrefsStore | None,
|
|
42
|
+
*,
|
|
43
|
+
resolved_scope: str | None = None,
|
|
44
|
+
scope_chat_ids: frozenset[int] | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
reply = make_reply(cfg, msg)
|
|
47
|
+
tkey = (
|
|
48
|
+
_topic_key(msg, cfg, scope_chat_ids=scope_chat_ids)
|
|
49
|
+
if topic_store is not None
|
|
50
|
+
else None
|
|
51
|
+
)
|
|
52
|
+
tokens = split_command_args(args_text)
|
|
53
|
+
action = tokens[0].lower() if tokens else "show"
|
|
54
|
+
engine_ids = {engine.lower() for engine in cfg.runtime.engine_ids}
|
|
55
|
+
|
|
56
|
+
if action in {"show", ""}:
|
|
57
|
+
selection = await resolve_engine_selection(
|
|
58
|
+
cfg,
|
|
59
|
+
msg,
|
|
60
|
+
ambient_context=ambient_context,
|
|
61
|
+
topic_store=topic_store,
|
|
62
|
+
chat_prefs=chat_prefs,
|
|
63
|
+
topic_key=tkey,
|
|
64
|
+
)
|
|
65
|
+
if selection is None:
|
|
66
|
+
return
|
|
67
|
+
engine, engine_source = selection
|
|
68
|
+
topic_override = None
|
|
69
|
+
if tkey is not None and topic_store is not None:
|
|
70
|
+
topic_override = await topic_store.get_engine_override(
|
|
71
|
+
tkey[0], tkey[1], engine
|
|
72
|
+
)
|
|
73
|
+
chat_override = None
|
|
74
|
+
if chat_prefs is not None:
|
|
75
|
+
chat_override = await chat_prefs.get_engine_override(msg.chat_id, engine)
|
|
76
|
+
resolution = resolve_override_value(
|
|
77
|
+
topic_override=topic_override,
|
|
78
|
+
chat_override=chat_override,
|
|
79
|
+
field="reasoning",
|
|
80
|
+
)
|
|
81
|
+
engine_line = f"engine: {engine} ({ENGINE_SOURCE_LABELS[engine_source]})"
|
|
82
|
+
reasoning_value = resolution.value or "default"
|
|
83
|
+
reasoning_line = (
|
|
84
|
+
f"reasoning: {reasoning_value} "
|
|
85
|
+
f"({OVERRIDE_SOURCE_LABELS[resolution.source]})"
|
|
86
|
+
)
|
|
87
|
+
topic_label = resolution.topic_value or "none"
|
|
88
|
+
if tkey is None:
|
|
89
|
+
topic_label = "none"
|
|
90
|
+
chat_label = (
|
|
91
|
+
"unavailable" if chat_prefs is None else resolution.chat_value or "none"
|
|
92
|
+
)
|
|
93
|
+
defaults_line = f"defaults: topic: {topic_label}, chat: {chat_label}"
|
|
94
|
+
available_levels = ", ".join(allowed_reasoning_levels(engine))
|
|
95
|
+
available_line = f"available levels: {available_levels}"
|
|
96
|
+
await reply(
|
|
97
|
+
text="\n\n".join(
|
|
98
|
+
[engine_line, reasoning_line, defaults_line, available_line]
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if action == "set":
|
|
104
|
+
engine_arg, level = parse_set_args(tokens, engine_ids=engine_ids)
|
|
105
|
+
if level is None:
|
|
106
|
+
await reply(text=REASONING_USAGE)
|
|
107
|
+
return
|
|
108
|
+
if not await require_admin_or_private(
|
|
109
|
+
cfg,
|
|
110
|
+
msg,
|
|
111
|
+
missing_sender="cannot verify sender for reasoning overrides.",
|
|
112
|
+
failed_member="failed to verify reasoning override permissions.",
|
|
113
|
+
denied="changing reasoning overrides is restricted to group admins.",
|
|
114
|
+
):
|
|
115
|
+
return
|
|
116
|
+
if engine_arg is None:
|
|
117
|
+
selection = await resolve_engine_selection(
|
|
118
|
+
cfg,
|
|
119
|
+
msg,
|
|
120
|
+
ambient_context=ambient_context,
|
|
121
|
+
topic_store=topic_store,
|
|
122
|
+
chat_prefs=chat_prefs,
|
|
123
|
+
topic_key=tkey,
|
|
124
|
+
)
|
|
125
|
+
if selection is None:
|
|
126
|
+
return
|
|
127
|
+
engine, _ = selection
|
|
128
|
+
else:
|
|
129
|
+
engine = engine_arg
|
|
130
|
+
if engine not in engine_ids:
|
|
131
|
+
available = ", ".join(cfg.runtime.engine_ids)
|
|
132
|
+
await reply(
|
|
133
|
+
text=f"unknown engine `{engine}`.\navailable engines: `{available}`"
|
|
134
|
+
)
|
|
135
|
+
return
|
|
136
|
+
normalized_level = level.strip().lower()
|
|
137
|
+
allowed = allowed_reasoning_levels(engine)
|
|
138
|
+
if normalized_level not in allowed:
|
|
139
|
+
await reply(
|
|
140
|
+
text=(
|
|
141
|
+
f"unknown reasoning level `{level}`.\n"
|
|
142
|
+
f"available levels: {', '.join(allowed)}"
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
scope = await apply_engine_override(
|
|
147
|
+
reply=reply,
|
|
148
|
+
tkey=tkey,
|
|
149
|
+
topic_store=topic_store,
|
|
150
|
+
chat_prefs=chat_prefs,
|
|
151
|
+
chat_id=msg.chat_id,
|
|
152
|
+
engine=engine,
|
|
153
|
+
update=lambda current: EngineOverrides(
|
|
154
|
+
model=current.model if current is not None else None,
|
|
155
|
+
reasoning=normalized_level,
|
|
156
|
+
),
|
|
157
|
+
topic_unavailable="topic reasoning overrides are unavailable.",
|
|
158
|
+
chat_unavailable="chat reasoning overrides are unavailable (no config path).",
|
|
159
|
+
)
|
|
160
|
+
if scope is None:
|
|
161
|
+
return
|
|
162
|
+
if scope == "topic":
|
|
163
|
+
await reply(
|
|
164
|
+
text=(
|
|
165
|
+
f"topic reasoning override set to `{normalized_level}` "
|
|
166
|
+
f"for `{engine}`.\n"
|
|
167
|
+
"If you want a clean start on the new setting, run `/new`."
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
await reply(
|
|
172
|
+
text=(
|
|
173
|
+
f"chat reasoning override set to `{normalized_level}` for `{engine}`.\n"
|
|
174
|
+
"If you want a clean start on the new setting, run `/new`."
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if action == "clear":
|
|
180
|
+
engine = None
|
|
181
|
+
if len(tokens) > 2:
|
|
182
|
+
await reply(text=REASONING_USAGE)
|
|
183
|
+
return
|
|
184
|
+
if len(tokens) == 2:
|
|
185
|
+
engine = tokens[1].strip().lower() or None
|
|
186
|
+
if not await require_admin_or_private(
|
|
187
|
+
cfg,
|
|
188
|
+
msg,
|
|
189
|
+
missing_sender="cannot verify sender for reasoning overrides.",
|
|
190
|
+
failed_member="failed to verify reasoning override permissions.",
|
|
191
|
+
denied="changing reasoning overrides is restricted to group admins.",
|
|
192
|
+
):
|
|
193
|
+
return
|
|
194
|
+
if engine is None:
|
|
195
|
+
selection = await resolve_engine_selection(
|
|
196
|
+
cfg,
|
|
197
|
+
msg,
|
|
198
|
+
ambient_context=ambient_context,
|
|
199
|
+
topic_store=topic_store,
|
|
200
|
+
chat_prefs=chat_prefs,
|
|
201
|
+
topic_key=tkey,
|
|
202
|
+
)
|
|
203
|
+
if selection is None:
|
|
204
|
+
return
|
|
205
|
+
engine, _ = selection
|
|
206
|
+
if engine not in engine_ids:
|
|
207
|
+
available = ", ".join(cfg.runtime.engine_ids)
|
|
208
|
+
await reply(
|
|
209
|
+
text=f"unknown engine `{engine}`.\navailable engines: `{available}`"
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
scope = await apply_engine_override(
|
|
213
|
+
reply=reply,
|
|
214
|
+
tkey=tkey,
|
|
215
|
+
topic_store=topic_store,
|
|
216
|
+
chat_prefs=chat_prefs,
|
|
217
|
+
chat_id=msg.chat_id,
|
|
218
|
+
engine=engine,
|
|
219
|
+
update=lambda current: EngineOverrides(
|
|
220
|
+
model=current.model if current is not None else None,
|
|
221
|
+
reasoning=None,
|
|
222
|
+
),
|
|
223
|
+
topic_unavailable="topic reasoning overrides are unavailable.",
|
|
224
|
+
chat_unavailable="chat reasoning overrides are unavailable (no config path).",
|
|
225
|
+
)
|
|
226
|
+
if scope is None:
|
|
227
|
+
return
|
|
228
|
+
if scope == "topic":
|
|
229
|
+
await reply(text="topic reasoning override cleared (using chat default).")
|
|
230
|
+
return
|
|
231
|
+
await reply(text="chat reasoning override cleared.")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
await reply(text=REASONING_USAGE)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..bridge import send_plain
|
|
8
|
+
from ..types import TelegramIncomingMessage
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..bridge import TelegramBridgeConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_reply(
|
|
15
|
+
cfg: TelegramBridgeConfig, msg: TelegramIncomingMessage
|
|
16
|
+
) -> Callable[..., Awaitable[None]]:
|
|
17
|
+
return partial(
|
|
18
|
+
send_plain,
|
|
19
|
+
cfg.exec_cfg.transport,
|
|
20
|
+
chat_id=msg.chat_id,
|
|
21
|
+
user_msg_id=msg.message_id,
|
|
22
|
+
thread_id=msg.thread_id,
|
|
23
|
+
)
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ...context import RunContext
|
|
6
|
+
from ...markdown import MarkdownParts
|
|
7
|
+
from ...transport_runtime import TransportRuntime
|
|
8
|
+
from ...transport import RenderedMessage, SendOptions
|
|
9
|
+
from ..chat_prefs import ChatPrefsStore
|
|
10
|
+
from ..chat_sessions import ChatSessionStore
|
|
11
|
+
from ..context import (
|
|
12
|
+
_format_context,
|
|
13
|
+
_format_ctx_status,
|
|
14
|
+
_merge_topic_context,
|
|
15
|
+
_parse_project_branch_args,
|
|
16
|
+
_usage_ctx_set,
|
|
17
|
+
_usage_topic,
|
|
18
|
+
)
|
|
19
|
+
from ..files import split_command_args
|
|
20
|
+
from ..render import prepare_telegram
|
|
21
|
+
from ..topic_state import TopicStateStore
|
|
22
|
+
from ..topics import (
|
|
23
|
+
_maybe_rename_topic,
|
|
24
|
+
_topic_key,
|
|
25
|
+
_topic_title,
|
|
26
|
+
_topics_chat_project,
|
|
27
|
+
_topics_command_error,
|
|
28
|
+
)
|
|
29
|
+
from ..types import TelegramIncomingMessage
|
|
30
|
+
from .reply import make_reply
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from ..bridge import TelegramBridgeConfig
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _handle_ctx_command(
|
|
37
|
+
cfg: TelegramBridgeConfig,
|
|
38
|
+
msg: TelegramIncomingMessage,
|
|
39
|
+
args_text: str,
|
|
40
|
+
store: TopicStateStore,
|
|
41
|
+
*,
|
|
42
|
+
resolved_scope: str | None = None,
|
|
43
|
+
scope_chat_ids: frozenset[int] | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
reply = make_reply(cfg, msg)
|
|
46
|
+
error = _topics_command_error(
|
|
47
|
+
cfg,
|
|
48
|
+
msg.chat_id,
|
|
49
|
+
resolved_scope=resolved_scope,
|
|
50
|
+
scope_chat_ids=scope_chat_ids,
|
|
51
|
+
)
|
|
52
|
+
if error is not None:
|
|
53
|
+
await reply(text=error)
|
|
54
|
+
return
|
|
55
|
+
chat_project = _topics_chat_project(cfg, msg.chat_id)
|
|
56
|
+
tkey = _topic_key(msg, cfg, scope_chat_ids=scope_chat_ids)
|
|
57
|
+
if tkey is None:
|
|
58
|
+
await reply(text="this command only works inside a topic.")
|
|
59
|
+
return
|
|
60
|
+
tokens = split_command_args(args_text)
|
|
61
|
+
action = tokens[0].lower() if tokens else "show"
|
|
62
|
+
if action in {"show", ""}:
|
|
63
|
+
snapshot = await store.get_thread(*tkey)
|
|
64
|
+
bound = snapshot.context if snapshot is not None else None
|
|
65
|
+
ambient = _merge_topic_context(chat_project=chat_project, bound=bound)
|
|
66
|
+
resolved = cfg.runtime.resolve_message(
|
|
67
|
+
text="",
|
|
68
|
+
reply_text=msg.reply_to_text,
|
|
69
|
+
chat_id=msg.chat_id,
|
|
70
|
+
ambient_context=ambient,
|
|
71
|
+
)
|
|
72
|
+
text = _format_ctx_status(
|
|
73
|
+
cfg=cfg,
|
|
74
|
+
runtime=cfg.runtime,
|
|
75
|
+
bound=bound,
|
|
76
|
+
resolved=resolved.context,
|
|
77
|
+
context_source=resolved.context_source,
|
|
78
|
+
snapshot=snapshot,
|
|
79
|
+
chat_project=chat_project,
|
|
80
|
+
)
|
|
81
|
+
await reply(text=text)
|
|
82
|
+
return
|
|
83
|
+
if action == "set":
|
|
84
|
+
rest = " ".join(tokens[1:])
|
|
85
|
+
context, error = _parse_project_branch_args(
|
|
86
|
+
rest,
|
|
87
|
+
runtime=cfg.runtime,
|
|
88
|
+
require_branch=False,
|
|
89
|
+
chat_project=chat_project,
|
|
90
|
+
)
|
|
91
|
+
if error is not None:
|
|
92
|
+
await reply(
|
|
93
|
+
text=f"error:\n{error}\n{_usage_ctx_set(chat_project=chat_project)}",
|
|
94
|
+
)
|
|
95
|
+
return
|
|
96
|
+
if context is None:
|
|
97
|
+
await reply(
|
|
98
|
+
text=f"error:\n{_usage_ctx_set(chat_project=chat_project)}",
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
await store.set_context(*tkey, context)
|
|
102
|
+
await _maybe_rename_topic(
|
|
103
|
+
cfg,
|
|
104
|
+
store,
|
|
105
|
+
chat_id=tkey[0],
|
|
106
|
+
thread_id=tkey[1],
|
|
107
|
+
context=context,
|
|
108
|
+
)
|
|
109
|
+
await reply(
|
|
110
|
+
text=f"topic bound to `{_format_context(cfg.runtime, context)}`",
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
if action == "clear":
|
|
114
|
+
await store.clear_context(*tkey)
|
|
115
|
+
await reply(text="topic binding cleared.")
|
|
116
|
+
return
|
|
117
|
+
await reply(
|
|
118
|
+
text="unknown `/ctx` command. use `/ctx`, `/ctx set`, or `/ctx clear`.",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_chat_ctx_args(
|
|
123
|
+
args_text: str,
|
|
124
|
+
*,
|
|
125
|
+
runtime: TransportRuntime,
|
|
126
|
+
default_project: str | None,
|
|
127
|
+
) -> tuple[RunContext | None, str | None]:
|
|
128
|
+
tokens = split_command_args(args_text)
|
|
129
|
+
if not tokens:
|
|
130
|
+
return None, _usage_ctx_set(chat_project=None)
|
|
131
|
+
if len(tokens) > 2:
|
|
132
|
+
return None, "too many arguments"
|
|
133
|
+
project_token: str | None = None
|
|
134
|
+
branch: str | None = None
|
|
135
|
+
first = tokens[0]
|
|
136
|
+
if first.startswith("@"):
|
|
137
|
+
branch = first[1:] or None
|
|
138
|
+
else:
|
|
139
|
+
project_token = first
|
|
140
|
+
if len(tokens) == 2:
|
|
141
|
+
second = tokens[1]
|
|
142
|
+
if not second.startswith("@"):
|
|
143
|
+
return None, "branch must be prefixed with @"
|
|
144
|
+
branch = second[1:] or None
|
|
145
|
+
project_key: str | None = None
|
|
146
|
+
if project_token is None:
|
|
147
|
+
if default_project is None:
|
|
148
|
+
return None, "project is required"
|
|
149
|
+
project_key = default_project
|
|
150
|
+
else:
|
|
151
|
+
project_key = runtime.normalize_project_key(project_token)
|
|
152
|
+
if project_key is None:
|
|
153
|
+
return None, f"unknown project {project_token!r}"
|
|
154
|
+
return RunContext(project=project_key, branch=branch), None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def _handle_chat_ctx_command(
|
|
158
|
+
cfg: TelegramBridgeConfig,
|
|
159
|
+
msg: TelegramIncomingMessage,
|
|
160
|
+
args_text: str,
|
|
161
|
+
chat_prefs: ChatPrefsStore | None,
|
|
162
|
+
) -> None:
|
|
163
|
+
reply = make_reply(cfg, msg)
|
|
164
|
+
if chat_prefs is None:
|
|
165
|
+
await reply(text="chat context unavailable; config path is not set.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
tokens = split_command_args(args_text)
|
|
169
|
+
action = tokens[0].lower() if tokens else "show"
|
|
170
|
+
if action in {"show", ""}:
|
|
171
|
+
bound = await chat_prefs.get_context(msg.chat_id)
|
|
172
|
+
resolved = cfg.runtime.resolve_message(
|
|
173
|
+
text="",
|
|
174
|
+
reply_text=msg.reply_to_text,
|
|
175
|
+
chat_id=msg.chat_id,
|
|
176
|
+
ambient_context=bound,
|
|
177
|
+
)
|
|
178
|
+
source = resolved.context_source
|
|
179
|
+
if bound is not None and resolved.context_source == "ambient":
|
|
180
|
+
source = "bound"
|
|
181
|
+
lines = [
|
|
182
|
+
f"bound ctx: {_format_context(cfg.runtime, bound)}",
|
|
183
|
+
f"resolved ctx: {_format_context(cfg.runtime, resolved.context)} (source: {source})",
|
|
184
|
+
]
|
|
185
|
+
if bound is None:
|
|
186
|
+
ctx_usage = (
|
|
187
|
+
_usage_ctx_set(chat_project=None).removeprefix("usage: ").strip()
|
|
188
|
+
)
|
|
189
|
+
lines.append(f"note: no bound context — bind with {ctx_usage}")
|
|
190
|
+
await reply(text="\n".join(lines))
|
|
191
|
+
return
|
|
192
|
+
if action == "set":
|
|
193
|
+
rest = " ".join(tokens[1:])
|
|
194
|
+
context, error = _parse_chat_ctx_args(
|
|
195
|
+
rest,
|
|
196
|
+
runtime=cfg.runtime,
|
|
197
|
+
default_project=cfg.runtime.default_project,
|
|
198
|
+
)
|
|
199
|
+
if error is not None:
|
|
200
|
+
await reply(
|
|
201
|
+
text=f"error:\n{error}\n{_usage_ctx_set(chat_project=None)}",
|
|
202
|
+
)
|
|
203
|
+
return
|
|
204
|
+
if context is None:
|
|
205
|
+
await reply(text=f"error:\n{_usage_ctx_set(chat_project=None)}")
|
|
206
|
+
return
|
|
207
|
+
await chat_prefs.set_context(msg.chat_id, context)
|
|
208
|
+
await reply(
|
|
209
|
+
text=f"chat bound to `{_format_context(cfg.runtime, context)}`",
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
if action == "clear":
|
|
213
|
+
await chat_prefs.clear_context(msg.chat_id)
|
|
214
|
+
await reply(text="chat context cleared.")
|
|
215
|
+
return
|
|
216
|
+
await reply(
|
|
217
|
+
text="unknown `/ctx` command. use `/ctx`, `/ctx set`, or `/ctx clear`.",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _handle_new_command(
|
|
222
|
+
cfg: TelegramBridgeConfig,
|
|
223
|
+
msg: TelegramIncomingMessage,
|
|
224
|
+
store: TopicStateStore,
|
|
225
|
+
*,
|
|
226
|
+
resolved_scope: str | None = None,
|
|
227
|
+
scope_chat_ids: frozenset[int] | None = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
reply = make_reply(cfg, msg)
|
|
230
|
+
error = _topics_command_error(
|
|
231
|
+
cfg,
|
|
232
|
+
msg.chat_id,
|
|
233
|
+
resolved_scope=resolved_scope,
|
|
234
|
+
scope_chat_ids=scope_chat_ids,
|
|
235
|
+
)
|
|
236
|
+
if error is not None:
|
|
237
|
+
await reply(text=error)
|
|
238
|
+
return
|
|
239
|
+
tkey = _topic_key(msg, cfg, scope_chat_ids=scope_chat_ids)
|
|
240
|
+
if tkey is None:
|
|
241
|
+
await reply(text="this command only works inside a topic.")
|
|
242
|
+
return
|
|
243
|
+
await store.clear_sessions(*tkey)
|
|
244
|
+
await reply(text="cleared stored sessions for this topic.")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _handle_chat_new_command(
|
|
248
|
+
cfg: TelegramBridgeConfig,
|
|
249
|
+
msg: TelegramIncomingMessage,
|
|
250
|
+
store: ChatSessionStore,
|
|
251
|
+
session_key: tuple[int, int | None] | None,
|
|
252
|
+
) -> None:
|
|
253
|
+
reply = make_reply(cfg, msg)
|
|
254
|
+
if session_key is None:
|
|
255
|
+
await reply(text="no stored sessions to clear for this chat.")
|
|
256
|
+
return
|
|
257
|
+
await store.clear_sessions(session_key[0], session_key[1])
|
|
258
|
+
if msg.chat_type == "private":
|
|
259
|
+
text = "cleared stored sessions for this chat."
|
|
260
|
+
else:
|
|
261
|
+
text = "cleared stored sessions for you in this chat."
|
|
262
|
+
await reply(text=text)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def _handle_topic_command(
|
|
266
|
+
cfg: TelegramBridgeConfig,
|
|
267
|
+
msg: TelegramIncomingMessage,
|
|
268
|
+
args_text: str,
|
|
269
|
+
store: TopicStateStore,
|
|
270
|
+
*,
|
|
271
|
+
resolved_scope: str | None = None,
|
|
272
|
+
scope_chat_ids: frozenset[int] | None = None,
|
|
273
|
+
) -> None:
|
|
274
|
+
reply = make_reply(cfg, msg)
|
|
275
|
+
error = _topics_command_error(
|
|
276
|
+
cfg,
|
|
277
|
+
msg.chat_id,
|
|
278
|
+
resolved_scope=resolved_scope,
|
|
279
|
+
scope_chat_ids=scope_chat_ids,
|
|
280
|
+
)
|
|
281
|
+
if error is not None:
|
|
282
|
+
await reply(text=error)
|
|
283
|
+
return
|
|
284
|
+
chat_project = _topics_chat_project(cfg, msg.chat_id)
|
|
285
|
+
context, error = _parse_project_branch_args(
|
|
286
|
+
args_text,
|
|
287
|
+
runtime=cfg.runtime,
|
|
288
|
+
require_branch=True,
|
|
289
|
+
chat_project=chat_project,
|
|
290
|
+
)
|
|
291
|
+
if error is not None or context is None:
|
|
292
|
+
usage = _usage_topic(chat_project=chat_project)
|
|
293
|
+
text = f"error:\n{error}\n{usage}" if error else usage
|
|
294
|
+
await reply(text=text)
|
|
295
|
+
return
|
|
296
|
+
title = _topic_title(runtime=cfg.runtime, context=context)
|
|
297
|
+
existing = await store.find_thread_for_context(msg.chat_id, context)
|
|
298
|
+
stale_thread_id: int | None = None
|
|
299
|
+
if existing is not None:
|
|
300
|
+
updated = await cfg.bot.edit_forum_topic(
|
|
301
|
+
chat_id=msg.chat_id,
|
|
302
|
+
message_thread_id=existing,
|
|
303
|
+
name=title,
|
|
304
|
+
)
|
|
305
|
+
if updated:
|
|
306
|
+
await reply(
|
|
307
|
+
text=f"topic already exists for {_format_context(cfg.runtime, context)} "
|
|
308
|
+
"in this chat.",
|
|
309
|
+
)
|
|
310
|
+
return
|
|
311
|
+
stale_thread_id = existing
|
|
312
|
+
created = await cfg.bot.create_forum_topic(msg.chat_id, title)
|
|
313
|
+
if created is None:
|
|
314
|
+
await reply(text="failed to create topic.")
|
|
315
|
+
return
|
|
316
|
+
thread_id = created.message_thread_id
|
|
317
|
+
if stale_thread_id is not None:
|
|
318
|
+
await store.delete_thread(msg.chat_id, stale_thread_id)
|
|
319
|
+
await store.set_context(
|
|
320
|
+
msg.chat_id,
|
|
321
|
+
thread_id,
|
|
322
|
+
context,
|
|
323
|
+
topic_title=title,
|
|
324
|
+
)
|
|
325
|
+
await reply(text=f"created topic `{title}`.")
|
|
326
|
+
bound_text = f"topic bound to `{_format_context(cfg.runtime, context)}`"
|
|
327
|
+
rendered_text, entities = prepare_telegram(MarkdownParts(header=bound_text))
|
|
328
|
+
await cfg.exec_cfg.transport.send(
|
|
329
|
+
channel_id=msg.chat_id,
|
|
330
|
+
message=RenderedMessage(text=rendered_text, extra={"entities": entities}),
|
|
331
|
+
options=SendOptions(thread_id=thread_id),
|
|
332
|
+
)
|