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.
Files changed (103) hide show
  1. takopi/__init__.py +1 -0
  2. takopi/api.py +116 -0
  3. takopi/backends.py +25 -0
  4. takopi/backends_helpers.py +14 -0
  5. takopi/cli/__init__.py +228 -0
  6. takopi/cli/config.py +320 -0
  7. takopi/cli/doctor.py +173 -0
  8. takopi/cli/init.py +113 -0
  9. takopi/cli/onboarding_cmd.py +126 -0
  10. takopi/cli/plugins.py +196 -0
  11. takopi/cli/run.py +419 -0
  12. takopi/cli/topic.py +355 -0
  13. takopi/commands.py +134 -0
  14. takopi/config.py +142 -0
  15. takopi/config_migrations.py +124 -0
  16. takopi/config_watch.py +146 -0
  17. takopi/context.py +9 -0
  18. takopi/directives.py +146 -0
  19. takopi/engines.py +53 -0
  20. takopi/events.py +170 -0
  21. takopi/ids.py +17 -0
  22. takopi/lockfile.py +158 -0
  23. takopi/logging.py +283 -0
  24. takopi/markdown.py +298 -0
  25. takopi/model.py +77 -0
  26. takopi/plugins.py +312 -0
  27. takopi/presenter.py +25 -0
  28. takopi/progress.py +99 -0
  29. takopi/router.py +113 -0
  30. takopi/runner.py +712 -0
  31. takopi/runner_bridge.py +619 -0
  32. takopi/runners/__init__.py +1 -0
  33. takopi/runners/claude.py +483 -0
  34. takopi/runners/codex.py +656 -0
  35. takopi/runners/mock.py +221 -0
  36. takopi/runners/opencode.py +505 -0
  37. takopi/runners/pi.py +523 -0
  38. takopi/runners/run_options.py +39 -0
  39. takopi/runners/tool_actions.py +90 -0
  40. takopi/runtime_loader.py +207 -0
  41. takopi/scheduler.py +159 -0
  42. takopi/schemas/__init__.py +1 -0
  43. takopi/schemas/claude.py +238 -0
  44. takopi/schemas/codex.py +169 -0
  45. takopi/schemas/opencode.py +51 -0
  46. takopi/schemas/pi.py +117 -0
  47. takopi/settings.py +360 -0
  48. takopi/telegram/__init__.py +20 -0
  49. takopi/telegram/api_models.py +37 -0
  50. takopi/telegram/api_schemas.py +152 -0
  51. takopi/telegram/backend.py +163 -0
  52. takopi/telegram/bridge.py +425 -0
  53. takopi/telegram/chat_prefs.py +242 -0
  54. takopi/telegram/chat_sessions.py +112 -0
  55. takopi/telegram/client.py +409 -0
  56. takopi/telegram/client_api.py +539 -0
  57. takopi/telegram/commands/__init__.py +12 -0
  58. takopi/telegram/commands/agent.py +196 -0
  59. takopi/telegram/commands/cancel.py +116 -0
  60. takopi/telegram/commands/dispatch.py +111 -0
  61. takopi/telegram/commands/executor.py +449 -0
  62. takopi/telegram/commands/file_transfer.py +586 -0
  63. takopi/telegram/commands/handlers.py +45 -0
  64. takopi/telegram/commands/media.py +143 -0
  65. takopi/telegram/commands/menu.py +139 -0
  66. takopi/telegram/commands/model.py +215 -0
  67. takopi/telegram/commands/overrides.py +159 -0
  68. takopi/telegram/commands/parse.py +30 -0
  69. takopi/telegram/commands/plan.py +16 -0
  70. takopi/telegram/commands/reasoning.py +234 -0
  71. takopi/telegram/commands/reply.py +23 -0
  72. takopi/telegram/commands/topics.py +332 -0
  73. takopi/telegram/commands/trigger.py +143 -0
  74. takopi/telegram/context.py +140 -0
  75. takopi/telegram/engine_defaults.py +86 -0
  76. takopi/telegram/engine_overrides.py +105 -0
  77. takopi/telegram/files.py +178 -0
  78. takopi/telegram/loop.py +1822 -0
  79. takopi/telegram/onboarding.py +1088 -0
  80. takopi/telegram/outbox.py +177 -0
  81. takopi/telegram/parsing.py +239 -0
  82. takopi/telegram/render.py +198 -0
  83. takopi/telegram/state_store.py +88 -0
  84. takopi/telegram/topic_state.py +334 -0
  85. takopi/telegram/topics.py +256 -0
  86. takopi/telegram/trigger_mode.py +68 -0
  87. takopi/telegram/types.py +63 -0
  88. takopi/telegram/voice.py +110 -0
  89. takopi/transport.py +53 -0
  90. takopi/transport_runtime.py +323 -0
  91. takopi/transports.py +76 -0
  92. takopi/utils/__init__.py +1 -0
  93. takopi/utils/git.py +87 -0
  94. takopi/utils/json_state.py +21 -0
  95. takopi/utils/paths.py +47 -0
  96. takopi/utils/streams.py +44 -0
  97. takopi/utils/subprocess.py +86 -0
  98. takopi/worktrees.py +135 -0
  99. yee88-0.1.0.dist-info/METADATA +116 -0
  100. yee88-0.1.0.dist-info/RECORD +103 -0
  101. yee88-0.1.0.dist-info/WHEEL +4 -0
  102. yee88-0.1.0.dist-info/entry_points.txt +11 -0
  103. yee88-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,586 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from ...config import ConfigError
9
+ from ...context import RunContext
10
+ from ...directives import DirectiveError
11
+ from ...transport_runtime import ResolvedMessage
12
+ from ..context import _format_context
13
+ from ..files import (
14
+ default_upload_name,
15
+ default_upload_path,
16
+ deny_reason,
17
+ format_bytes,
18
+ normalize_relative_path,
19
+ parse_file_command,
20
+ parse_file_prompt,
21
+ resolve_path_within_root,
22
+ write_bytes_atomic,
23
+ ZipTooLargeError,
24
+ zip_directory,
25
+ )
26
+ from ..topic_state import TopicStateStore
27
+ from ..topics import _maybe_update_topic_context, _topic_key
28
+ from ..types import TelegramDocument, TelegramIncomingMessage
29
+ from .reply import make_reply
30
+
31
+ if TYPE_CHECKING:
32
+ from ..bridge import TelegramBridgeConfig
33
+
34
+ FILE_PUT_USAGE = "usage: `/file put <path>`"
35
+ FILE_GET_USAGE = "usage: `/file get <path>`"
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class _FilePutPlan:
40
+ resolved: ResolvedMessage
41
+ run_root: Path
42
+ path_value: str | None
43
+ force: bool
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class _FilePutResult:
48
+ name: str
49
+ rel_path: Path | None
50
+ size: int | None
51
+ error: str | None
52
+
53
+
54
+ @dataclass(slots=True)
55
+ class _SavedFilePut:
56
+ context: RunContext | None
57
+ rel_path: Path
58
+ size: int
59
+
60
+
61
+ @dataclass(slots=True)
62
+ class _SavedFilePutGroup:
63
+ context: RunContext | None
64
+ base_dir: Path | None
65
+ saved: list[_FilePutResult]
66
+ failed: list[_FilePutResult]
67
+
68
+
69
+ def resolve_file_put_paths(
70
+ plan: _FilePutPlan,
71
+ *,
72
+ cfg: TelegramBridgeConfig,
73
+ require_dir: bool,
74
+ ) -> tuple[Path | None, Path | None, str | None]:
75
+ path_value = plan.path_value
76
+ if not path_value:
77
+ return None, None, None
78
+ if require_dir or path_value.endswith("/"):
79
+ base_dir = normalize_relative_path(path_value)
80
+ if base_dir is None:
81
+ return None, None, "invalid upload path."
82
+ deny_rule = deny_reason(base_dir, cfg.files.deny_globs)
83
+ if deny_rule is not None:
84
+ return None, None, f"path denied by rule: {deny_rule}"
85
+ base_target = resolve_path_within_root(plan.run_root, base_dir)
86
+ if base_target is None:
87
+ return None, None, "upload path escapes the repo root."
88
+ if base_target.exists() and not base_target.is_dir():
89
+ return None, None, "upload path is a file."
90
+ return base_dir, None, None
91
+ rel_path = normalize_relative_path(path_value)
92
+ if rel_path is None:
93
+ return None, None, "invalid upload path."
94
+ return None, rel_path, None
95
+
96
+
97
+ async def _check_file_permissions(
98
+ cfg: TelegramBridgeConfig, msg: TelegramIncomingMessage
99
+ ) -> bool:
100
+ reply = make_reply(cfg, msg)
101
+ sender_id = msg.sender_id
102
+ if sender_id is None:
103
+ await reply(text="cannot verify sender for file transfer.")
104
+ return False
105
+ if cfg.files.allowed_user_ids:
106
+ if sender_id not in cfg.files.allowed_user_ids:
107
+ await reply(text="file transfer is not allowed for this user.")
108
+ return False
109
+ return True
110
+ if msg.is_private:
111
+ return True
112
+ member = await cfg.bot.get_chat_member(msg.chat_id, sender_id)
113
+ if member is None:
114
+ await reply(text="failed to verify file transfer permissions.")
115
+ return False
116
+ if member.status in {"creator", "administrator"}:
117
+ return True
118
+ await reply(text="file transfer is restricted to group admins.")
119
+ return False
120
+
121
+
122
+ async def _prepare_file_put_plan(
123
+ cfg: TelegramBridgeConfig,
124
+ msg: TelegramIncomingMessage,
125
+ args_text: str,
126
+ ambient_context: RunContext | None,
127
+ topic_store: TopicStateStore | None,
128
+ ) -> _FilePutPlan | None:
129
+ reply = make_reply(cfg, msg)
130
+ if not await _check_file_permissions(cfg, msg):
131
+ return None
132
+ try:
133
+ resolved = cfg.runtime.resolve_message(
134
+ text=args_text,
135
+ reply_text=msg.reply_to_text,
136
+ ambient_context=ambient_context,
137
+ chat_id=msg.chat_id,
138
+ )
139
+ except DirectiveError as exc:
140
+ await reply(text=f"error:\n{exc}")
141
+ return None
142
+ topic_key = _topic_key(msg, cfg) if topic_store is not None else None
143
+ await _maybe_update_topic_context(
144
+ cfg=cfg,
145
+ topic_store=topic_store,
146
+ topic_key=topic_key,
147
+ context=resolved.context,
148
+ context_source=resolved.context_source,
149
+ )
150
+ if resolved.context is None or resolved.context.project is None:
151
+ await reply(text="no project context available for file upload.")
152
+ return None
153
+ try:
154
+ run_root = cfg.runtime.resolve_run_cwd(resolved.context)
155
+ except ConfigError as exc:
156
+ await reply(text=f"error:\n{exc}")
157
+ return None
158
+ if run_root is None:
159
+ await reply(text="no project context available for file upload.")
160
+ return None
161
+ path_value, force, error = parse_file_prompt(resolved.prompt, allow_empty=True)
162
+ if error is not None:
163
+ await reply(text=error)
164
+ return None
165
+ return _FilePutPlan(
166
+ resolved=resolved,
167
+ run_root=run_root,
168
+ path_value=path_value,
169
+ force=force,
170
+ )
171
+
172
+
173
+ def _format_file_put_failures(failed: Sequence[_FilePutResult]) -> str | None:
174
+ if not failed:
175
+ return None
176
+ errors = ", ".join(
177
+ f"`{item.name}` ({item.error})" for item in failed if item.error is not None
178
+ )
179
+ if not errors:
180
+ return None
181
+ return f"failed: {errors}"
182
+
183
+
184
+ async def _save_document_payload(
185
+ cfg: TelegramBridgeConfig,
186
+ *,
187
+ document: TelegramDocument,
188
+ run_root: Path,
189
+ rel_path: Path | None,
190
+ base_dir: Path | None,
191
+ force: bool,
192
+ ) -> _FilePutResult:
193
+ name = default_upload_name(document.file_name, None)
194
+ if (
195
+ document.file_size is not None
196
+ and document.file_size > cfg.files.max_upload_bytes
197
+ ):
198
+ return _FilePutResult(
199
+ name=name,
200
+ rel_path=None,
201
+ size=None,
202
+ error="file is too large to upload.",
203
+ )
204
+ file_info = await cfg.bot.get_file(document.file_id)
205
+ if file_info is None:
206
+ return _FilePutResult(
207
+ name=name,
208
+ rel_path=None,
209
+ size=None,
210
+ error="failed to fetch file metadata.",
211
+ )
212
+ file_path = file_info.file_path
213
+ name = default_upload_name(document.file_name, file_path)
214
+ resolved_path = rel_path
215
+ if resolved_path is None:
216
+ if base_dir is None:
217
+ resolved_path = default_upload_path(
218
+ cfg.files.uploads_dir, document.file_name, file_path
219
+ )
220
+ else:
221
+ resolved_path = base_dir / name
222
+ deny_rule = deny_reason(resolved_path, cfg.files.deny_globs)
223
+ if deny_rule is not None:
224
+ return _FilePutResult(
225
+ name=name,
226
+ rel_path=None,
227
+ size=None,
228
+ error=f"path denied by rule: {deny_rule}",
229
+ )
230
+ target = resolve_path_within_root(run_root, resolved_path)
231
+ if target is None:
232
+ return _FilePutResult(
233
+ name=name,
234
+ rel_path=None,
235
+ size=None,
236
+ error="upload path escapes the repo root.",
237
+ )
238
+ if target.exists():
239
+ if target.is_dir():
240
+ return _FilePutResult(
241
+ name=name,
242
+ rel_path=None,
243
+ size=None,
244
+ error="upload target is a directory.",
245
+ )
246
+ if not force:
247
+ return _FilePutResult(
248
+ name=name,
249
+ rel_path=None,
250
+ size=None,
251
+ error="file already exists; use --force to overwrite.",
252
+ )
253
+ payload = await cfg.bot.download_file(file_path)
254
+ if payload is None:
255
+ return _FilePutResult(
256
+ name=name,
257
+ rel_path=None,
258
+ size=None,
259
+ error="failed to download file.",
260
+ )
261
+ if len(payload) > cfg.files.max_upload_bytes:
262
+ return _FilePutResult(
263
+ name=name,
264
+ rel_path=None,
265
+ size=None,
266
+ error="file is too large to upload.",
267
+ )
268
+ try:
269
+ write_bytes_atomic(target, payload)
270
+ except OSError as exc:
271
+ return _FilePutResult(
272
+ name=name,
273
+ rel_path=None,
274
+ size=None,
275
+ error=f"failed to write file: {exc}",
276
+ )
277
+ return _FilePutResult(
278
+ name=name,
279
+ rel_path=resolved_path,
280
+ size=len(payload),
281
+ error=None,
282
+ )
283
+
284
+
285
+ async def _handle_file_command(
286
+ cfg: TelegramBridgeConfig,
287
+ msg: TelegramIncomingMessage,
288
+ args_text: str,
289
+ ambient_context: RunContext | None,
290
+ topic_store: TopicStateStore | None,
291
+ ) -> None:
292
+ reply = make_reply(cfg, msg)
293
+ command, rest, error = parse_file_command(args_text)
294
+ if error is not None:
295
+ await reply(text=error)
296
+ return
297
+ if command == "put":
298
+ await _handle_file_put(cfg, msg, rest, ambient_context, topic_store)
299
+ else:
300
+ await _handle_file_get(cfg, msg, rest, ambient_context, topic_store)
301
+
302
+
303
+ async def _handle_file_put_default(
304
+ cfg: TelegramBridgeConfig,
305
+ msg: TelegramIncomingMessage,
306
+ ambient_context: RunContext | None,
307
+ topic_store: TopicStateStore | None,
308
+ ) -> None:
309
+ await _handle_file_put(cfg, msg, "", ambient_context, topic_store)
310
+
311
+
312
+ async def _save_file_put(
313
+ cfg: TelegramBridgeConfig,
314
+ msg: TelegramIncomingMessage,
315
+ args_text: str,
316
+ ambient_context: RunContext | None,
317
+ topic_store: TopicStateStore | None,
318
+ ) -> _SavedFilePut | None:
319
+ reply = make_reply(cfg, msg)
320
+ document = msg.document
321
+ if document is None:
322
+ await reply(text=FILE_PUT_USAGE)
323
+ return None
324
+ plan = await _prepare_file_put_plan(
325
+ cfg,
326
+ msg,
327
+ args_text,
328
+ ambient_context,
329
+ topic_store,
330
+ )
331
+ if plan is None:
332
+ return None
333
+ base_dir, rel_path, error = resolve_file_put_paths(
334
+ plan,
335
+ cfg=cfg,
336
+ require_dir=False,
337
+ )
338
+ if error is not None:
339
+ await reply(text=error)
340
+ return None
341
+ result = await _save_document_payload(
342
+ cfg,
343
+ document=document,
344
+ run_root=plan.run_root,
345
+ rel_path=rel_path,
346
+ base_dir=base_dir,
347
+ force=plan.force,
348
+ )
349
+ if result.error is not None:
350
+ await reply(text=result.error)
351
+ return None
352
+ if result.rel_path is None or result.size is None:
353
+ await reply(text="failed to save file.")
354
+ return None
355
+ return _SavedFilePut(
356
+ context=plan.resolved.context,
357
+ rel_path=result.rel_path,
358
+ size=result.size,
359
+ )
360
+
361
+
362
+ async def _handle_file_put(
363
+ cfg: TelegramBridgeConfig,
364
+ msg: TelegramIncomingMessage,
365
+ args_text: str,
366
+ ambient_context: RunContext | None,
367
+ topic_store: TopicStateStore | None,
368
+ ) -> None:
369
+ reply = make_reply(cfg, msg)
370
+ saved = await _save_file_put(
371
+ cfg,
372
+ msg,
373
+ args_text,
374
+ ambient_context,
375
+ topic_store,
376
+ )
377
+ if saved is None:
378
+ return
379
+ context_label = _format_context(cfg.runtime, saved.context)
380
+ await reply(
381
+ text=(
382
+ f"saved `{saved.rel_path.as_posix()}` "
383
+ f"in `{context_label}` ({format_bytes(saved.size)})"
384
+ ),
385
+ )
386
+
387
+
388
+ async def _handle_file_put_group(
389
+ cfg: TelegramBridgeConfig,
390
+ msg: TelegramIncomingMessage,
391
+ args_text: str,
392
+ messages: Sequence[TelegramIncomingMessage],
393
+ ambient_context: RunContext | None,
394
+ topic_store: TopicStateStore | None,
395
+ ) -> None:
396
+ reply = make_reply(cfg, msg)
397
+ saved_group = await _save_file_put_group(
398
+ cfg,
399
+ msg,
400
+ args_text,
401
+ messages,
402
+ ambient_context,
403
+ topic_store,
404
+ )
405
+ if saved_group is None:
406
+ return
407
+ context_label = _format_context(cfg.runtime, saved_group.context)
408
+ total_bytes = sum(item.size or 0 for item in saved_group.saved)
409
+ dir_label: Path | None = saved_group.base_dir
410
+ if dir_label is None and saved_group.saved:
411
+ first_path = saved_group.saved[0].rel_path
412
+ if first_path is not None:
413
+ dir_label = first_path.parent
414
+ if saved_group.saved:
415
+ saved_names = ", ".join(f"`{item.name}`" for item in saved_group.saved)
416
+ if dir_label is not None:
417
+ dir_text = dir_label.as_posix()
418
+ if not dir_text.endswith("/"):
419
+ dir_text = f"{dir_text}/"
420
+ text = (
421
+ f"saved {saved_names} to `{dir_text}` "
422
+ f"in `{context_label}` ({format_bytes(total_bytes)})"
423
+ )
424
+ else:
425
+ text = (
426
+ f"saved {saved_names} in `{context_label}` "
427
+ f"({format_bytes(total_bytes)})"
428
+ )
429
+ else:
430
+ text = "failed to upload files."
431
+ failure_text = _format_file_put_failures(saved_group.failed)
432
+ if failure_text is not None:
433
+ text = f"{text}\n\n{failure_text}"
434
+ await reply(text=text)
435
+
436
+
437
+ async def _save_file_put_group(
438
+ cfg: TelegramBridgeConfig,
439
+ msg: TelegramIncomingMessage,
440
+ args_text: str,
441
+ messages: Sequence[TelegramIncomingMessage],
442
+ ambient_context: RunContext | None,
443
+ topic_store: TopicStateStore | None,
444
+ ) -> _SavedFilePutGroup | None:
445
+ reply = make_reply(cfg, msg)
446
+ documents = [item.document for item in messages if item.document is not None]
447
+ if not documents:
448
+ await reply(text=FILE_PUT_USAGE)
449
+ return None
450
+ plan = await _prepare_file_put_plan(
451
+ cfg,
452
+ msg,
453
+ args_text,
454
+ ambient_context,
455
+ topic_store,
456
+ )
457
+ if plan is None:
458
+ return None
459
+ base_dir, _, error = resolve_file_put_paths(
460
+ plan,
461
+ cfg=cfg,
462
+ require_dir=True,
463
+ )
464
+ if error is not None:
465
+ await reply(text=error)
466
+ return None
467
+ saved: list[_FilePutResult] = []
468
+ failed: list[_FilePutResult] = []
469
+ for document in documents:
470
+ result = await _save_document_payload(
471
+ cfg,
472
+ document=document,
473
+ run_root=plan.run_root,
474
+ rel_path=None,
475
+ base_dir=base_dir,
476
+ force=plan.force,
477
+ )
478
+ if result.error is None:
479
+ saved.append(result)
480
+ else:
481
+ failed.append(result)
482
+ return _SavedFilePutGroup(
483
+ context=plan.resolved.context,
484
+ base_dir=base_dir,
485
+ saved=saved,
486
+ failed=failed,
487
+ )
488
+
489
+
490
+ async def _handle_file_get(
491
+ cfg: TelegramBridgeConfig,
492
+ msg: TelegramIncomingMessage,
493
+ args_text: str,
494
+ ambient_context: RunContext | None,
495
+ topic_store: TopicStateStore | None,
496
+ ) -> None:
497
+ reply = make_reply(cfg, msg)
498
+ if not await _check_file_permissions(cfg, msg):
499
+ return
500
+ try:
501
+ resolved = cfg.runtime.resolve_message(
502
+ text=args_text,
503
+ reply_text=msg.reply_to_text,
504
+ ambient_context=ambient_context,
505
+ chat_id=msg.chat_id,
506
+ )
507
+ except DirectiveError as exc:
508
+ await reply(text=f"error:\n{exc}")
509
+ return
510
+ topic_key = _topic_key(msg, cfg) if topic_store is not None else None
511
+ await _maybe_update_topic_context(
512
+ cfg=cfg,
513
+ topic_store=topic_store,
514
+ topic_key=topic_key,
515
+ context=resolved.context,
516
+ context_source=resolved.context_source,
517
+ )
518
+ if resolved.context is None or resolved.context.project is None:
519
+ await reply(text="no project context available for file download.")
520
+ return
521
+ try:
522
+ run_root = cfg.runtime.resolve_run_cwd(resolved.context)
523
+ except ConfigError as exc:
524
+ await reply(text=f"error:\n{exc}")
525
+ return
526
+ if run_root is None:
527
+ await reply(text="no project context available for file download.")
528
+ return
529
+ path_value = resolved.prompt
530
+ if not path_value.strip():
531
+ await reply(text=FILE_GET_USAGE)
532
+ return
533
+ rel_path = normalize_relative_path(path_value)
534
+ if rel_path is None:
535
+ await reply(text="invalid download path.")
536
+ return
537
+ deny_rule = deny_reason(rel_path, cfg.files.deny_globs)
538
+ if deny_rule is not None:
539
+ await reply(text=f"path denied by rule: {deny_rule}")
540
+ return
541
+ target = resolve_path_within_root(run_root, rel_path)
542
+ if target is None:
543
+ await reply(text="download path escapes the repo root.")
544
+ return
545
+ if not target.exists():
546
+ await reply(text="file does not exist.")
547
+ return
548
+ if target.is_dir():
549
+ try:
550
+ payload = zip_directory(
551
+ run_root,
552
+ rel_path,
553
+ cfg.files.deny_globs,
554
+ max_bytes=cfg.files.max_download_bytes,
555
+ )
556
+ except ZipTooLargeError:
557
+ await reply(text="file is too large to send.")
558
+ return
559
+ except OSError as exc:
560
+ await reply(text=f"failed to read directory: {exc}")
561
+ return
562
+ filename = f"{rel_path.name or 'archive'}.zip"
563
+ else:
564
+ try:
565
+ size = target.stat().st_size
566
+ if size > cfg.files.max_download_bytes:
567
+ await reply(text="file is too large to send.")
568
+ return
569
+ payload = target.read_bytes()
570
+ except OSError as exc:
571
+ await reply(text=f"failed to read file: {exc}")
572
+ return
573
+ filename = target.name
574
+ if len(payload) > cfg.files.max_download_bytes:
575
+ await reply(text="file is too large to send.")
576
+ return
577
+ sent = await cfg.bot.send_document(
578
+ chat_id=msg.chat_id,
579
+ filename=filename,
580
+ content=payload,
581
+ reply_to_message_id=msg.message_id,
582
+ message_thread_id=msg.thread_id,
583
+ )
584
+ if sent is None:
585
+ await reply(text="failed to send file.")
586
+ return
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ # ruff: noqa: F401
4
+
5
+ from .agent import _handle_agent_command as handle_agent_command
6
+ from .dispatch import _dispatch_command as dispatch_command
7
+ from .executor import _run_engine as run_engine
8
+ from .executor import _should_show_resume_line as should_show_resume_line
9
+ from .file_transfer import _handle_file_command as handle_file_command
10
+ from .file_transfer import _handle_file_put_default as handle_file_put_default
11
+ from .file_transfer import _save_file_put as save_file_put
12
+ from .media import _handle_media_group as handle_media_group
13
+ from .menu import _reserved_commands as get_reserved_commands
14
+ from .menu import _set_command_menu as set_command_menu
15
+ from .model import _handle_model_command as handle_model_command
16
+ from .parse import _parse_slash_command as parse_slash_command
17
+ from .reasoning import _handle_reasoning_command as handle_reasoning_command
18
+ from .topics import _handle_chat_new_command as handle_chat_new_command
19
+ from .topics import _handle_chat_ctx_command as handle_chat_ctx_command
20
+ from .topics import _handle_ctx_command as handle_ctx_command
21
+ from .topics import _handle_new_command as handle_new_command
22
+ from .topics import _handle_topic_command as handle_topic_command
23
+ from .trigger import _handle_trigger_command as handle_trigger_command
24
+
25
+ __all__ = [
26
+ "dispatch_command",
27
+ "get_reserved_commands",
28
+ "handle_agent_command",
29
+ "handle_chat_ctx_command",
30
+ "handle_chat_new_command",
31
+ "handle_ctx_command",
32
+ "handle_file_command",
33
+ "handle_file_put_default",
34
+ "handle_media_group",
35
+ "handle_model_command",
36
+ "handle_new_command",
37
+ "handle_reasoning_command",
38
+ "handle_topic_command",
39
+ "handle_trigger_command",
40
+ "parse_slash_command",
41
+ "run_engine",
42
+ "save_file_put",
43
+ "set_command_menu",
44
+ "should_show_resume_line",
45
+ ]