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
takopi/cli/run.py ADDED
@@ -0,0 +1,419 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from collections.abc import Callable
6
+ from functools import partial
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+
10
+ import anyio
11
+ import typer
12
+
13
+ from .. import __version__
14
+ from ..backends import EngineBackend
15
+ from ..config import ConfigError, load_or_init_config
16
+ from ..engines import get_backend
17
+ from ..ids import RESERVED_CHAT_COMMANDS
18
+ from ..lockfile import LockError, LockHandle, acquire_lock, token_fingerprint
19
+ from ..logging import get_logger, setup_logging
20
+ from ..runtime_loader import build_runtime_spec, resolve_plugins_allowlist
21
+ from ..settings import TakopiSettings, load_settings, load_settings_if_exists
22
+ from ..transports import SetupResult, get_transport
23
+ from .config import _config_path_display, _fail_missing_config
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ def _load_settings_optional() -> tuple[TakopiSettings | None, Path | None]:
29
+ try:
30
+ loaded = load_settings_if_exists()
31
+ except ConfigError:
32
+ return None, None
33
+ if loaded is None:
34
+ return None, None
35
+ return loaded
36
+
37
+
38
+ def _resolve_transport_id(override: str | None) -> str:
39
+ if override is not None:
40
+ value = override.strip()
41
+ if not value:
42
+ raise ConfigError("Invalid `--transport`; expected a non-empty string.")
43
+ return value
44
+ load_or_init_config_fn = cast(
45
+ Callable[[], tuple[dict, Path]],
46
+ _resolve_cli_attr("load_or_init_config") or load_or_init_config,
47
+ )
48
+ try:
49
+ config, _ = load_or_init_config_fn()
50
+ except ConfigError:
51
+ return "telegram"
52
+ raw = config.get("transport")
53
+ if not isinstance(raw, str) or not raw.strip():
54
+ return "telegram"
55
+ return raw.strip()
56
+
57
+
58
+ def acquire_config_lock(config_path: Path, token: str | None) -> LockHandle:
59
+ fingerprint = token_fingerprint(token) if token else None
60
+ acquire_lock_fn = cast(
61
+ Callable[..., LockHandle],
62
+ _resolve_cli_attr("acquire_lock") or acquire_lock,
63
+ )
64
+ try:
65
+ return acquire_lock_fn(
66
+ config_path=config_path,
67
+ token_fingerprint=fingerprint,
68
+ )
69
+ except LockError as exc:
70
+ lines = str(exc).splitlines()
71
+ if lines:
72
+ typer.echo(lines[0], err=True)
73
+ if len(lines) > 1:
74
+ typer.echo("\n".join(lines[1:]), err=True)
75
+ else:
76
+ typer.echo("error: unknown error", err=True)
77
+ raise typer.Exit(code=1) from exc
78
+
79
+
80
+ def _default_engine_for_setup(
81
+ override: str | None,
82
+ *,
83
+ settings: TakopiSettings | None,
84
+ config_path: Path | None,
85
+ ) -> str:
86
+ if override:
87
+ return override
88
+ if settings is None or config_path is None:
89
+ return "codex"
90
+ value = settings.default_engine
91
+ return value
92
+
93
+
94
+ def _resolve_setup_engine(
95
+ default_engine_override: str | None,
96
+ ) -> tuple[
97
+ TakopiSettings | None,
98
+ Path | None,
99
+ list[str] | None,
100
+ str,
101
+ EngineBackend,
102
+ ]:
103
+ load_settings_optional_fn = cast(
104
+ Callable[[], tuple[TakopiSettings | None, Path | None]],
105
+ _resolve_cli_attr("_load_settings_optional") or _load_settings_optional,
106
+ )
107
+ resolve_plugins_allowlist_fn = cast(
108
+ Callable[[TakopiSettings | None], list[str] | None],
109
+ _resolve_cli_attr("resolve_plugins_allowlist") or resolve_plugins_allowlist,
110
+ )
111
+ default_engine_for_setup_fn = cast(
112
+ Callable[..., str],
113
+ _resolve_cli_attr("_default_engine_for_setup") or _default_engine_for_setup,
114
+ )
115
+ get_backend_fn = cast(
116
+ Callable[..., EngineBackend],
117
+ _resolve_cli_attr("get_backend") or get_backend,
118
+ )
119
+
120
+ settings_hint, config_hint = load_settings_optional_fn()
121
+ allowlist = resolve_plugins_allowlist_fn(settings_hint)
122
+ default_engine = default_engine_for_setup_fn(
123
+ default_engine_override,
124
+ settings=settings_hint,
125
+ config_path=config_hint,
126
+ )
127
+ engine_backend = get_backend_fn(default_engine, allowlist=allowlist)
128
+ return settings_hint, config_hint, allowlist, default_engine, engine_backend
129
+
130
+
131
+ def _should_run_interactive() -> bool:
132
+ if os.environ.get("TAKOPI_NO_INTERACTIVE"):
133
+ return False
134
+ return sys.stdin.isatty() and sys.stdout.isatty()
135
+
136
+
137
+ def _setup_needs_config(setup: SetupResult) -> bool:
138
+ config_titles = {"create a config", "configure telegram"}
139
+ return any(issue.title in config_titles for issue in setup.issues)
140
+
141
+
142
+ def _run_auto_router(
143
+ *,
144
+ default_engine_override: str | None,
145
+ transport_override: str | None,
146
+ final_notify: bool,
147
+ debug: bool,
148
+ onboard: bool,
149
+ ) -> None:
150
+ setup_logging_fn = cast(
151
+ Callable[..., None],
152
+ _resolve_cli_attr("setup_logging") or setup_logging,
153
+ )
154
+ resolve_setup_engine_fn = cast(
155
+ Callable[
156
+ [str | None],
157
+ tuple[
158
+ TakopiSettings | None,
159
+ Path | None,
160
+ list[str] | None,
161
+ str,
162
+ EngineBackend,
163
+ ],
164
+ ],
165
+ _resolve_cli_attr("_resolve_setup_engine") or _resolve_setup_engine,
166
+ )
167
+ resolve_transport_id_fn = cast(
168
+ Callable[[str | None], str],
169
+ _resolve_cli_attr("_resolve_transport_id") or _resolve_transport_id,
170
+ )
171
+ get_transport_fn = cast(
172
+ Callable[..., Any],
173
+ _resolve_cli_attr("get_transport") or get_transport,
174
+ )
175
+ should_run_interactive_fn = cast(
176
+ Callable[[], bool],
177
+ _resolve_cli_attr("_should_run_interactive") or _should_run_interactive,
178
+ )
179
+ setup_needs_config_fn = cast(
180
+ Callable[[SetupResult], bool],
181
+ _resolve_cli_attr("_setup_needs_config") or _setup_needs_config,
182
+ )
183
+ config_path_display_fn = cast(
184
+ Callable[[Path], str],
185
+ _resolve_cli_attr("_config_path_display") or _config_path_display,
186
+ )
187
+ fail_missing_config_fn = cast(
188
+ Callable[[Path], None],
189
+ _resolve_cli_attr("_fail_missing_config") or _fail_missing_config,
190
+ )
191
+ load_settings_fn = cast(
192
+ Callable[[], tuple[TakopiSettings, Path]],
193
+ _resolve_cli_attr("load_settings") or load_settings,
194
+ )
195
+ build_runtime_spec_fn = cast(
196
+ Callable[..., Any],
197
+ _resolve_cli_attr("build_runtime_spec") or build_runtime_spec,
198
+ )
199
+ acquire_config_lock_fn = cast(
200
+ Callable[[Path, str | None], LockHandle],
201
+ _resolve_cli_attr("acquire_config_lock") or acquire_config_lock,
202
+ )
203
+
204
+ if debug:
205
+ os.environ.setdefault("TAKOPI_LOG_FILE", "debug.log")
206
+ setup_logging_fn(debug=debug)
207
+ lock_handle: LockHandle | None = None
208
+ try:
209
+ (
210
+ settings_hint,
211
+ config_hint,
212
+ allowlist,
213
+ default_engine,
214
+ engine_backend,
215
+ ) = resolve_setup_engine_fn(default_engine_override)
216
+ transport_id = resolve_transport_id_fn(transport_override)
217
+ transport_backend = get_transport_fn(transport_id, allowlist=allowlist)
218
+ except ConfigError as exc:
219
+ typer.echo(f"error: {exc}", err=True)
220
+ raise typer.Exit(code=1) from exc
221
+ if onboard:
222
+ if not should_run_interactive_fn():
223
+ typer.echo("error: --onboard requires a TTY", err=True)
224
+ raise typer.Exit(code=1)
225
+ if not anyio.run(partial(transport_backend.interactive_setup, force=True)):
226
+ raise typer.Exit(code=1)
227
+ (
228
+ settings_hint,
229
+ config_hint,
230
+ allowlist,
231
+ default_engine,
232
+ engine_backend,
233
+ ) = resolve_setup_engine_fn(default_engine_override)
234
+ setup = transport_backend.check_setup(
235
+ engine_backend,
236
+ transport_override=transport_override,
237
+ )
238
+ if not setup.ok:
239
+ if setup_needs_config_fn(setup) and should_run_interactive_fn():
240
+ if setup.config_path.exists():
241
+ display = config_path_display_fn(setup.config_path)
242
+ run_onboard = typer.confirm(
243
+ f"config at {display} is missing/invalid for "
244
+ f"{transport_backend.id}, run onboarding now?",
245
+ default=False,
246
+ )
247
+ if run_onboard and anyio.run(
248
+ partial(transport_backend.interactive_setup, force=True)
249
+ ):
250
+ (
251
+ settings_hint,
252
+ config_hint,
253
+ allowlist,
254
+ default_engine,
255
+ engine_backend,
256
+ ) = resolve_setup_engine_fn(default_engine_override)
257
+ setup = transport_backend.check_setup(
258
+ engine_backend,
259
+ transport_override=transport_override,
260
+ )
261
+ elif anyio.run(partial(transport_backend.interactive_setup, force=False)):
262
+ (
263
+ settings_hint,
264
+ config_hint,
265
+ allowlist,
266
+ default_engine,
267
+ engine_backend,
268
+ ) = resolve_setup_engine_fn(default_engine_override)
269
+ setup = transport_backend.check_setup(
270
+ engine_backend,
271
+ transport_override=transport_override,
272
+ )
273
+ if not setup.ok:
274
+ if setup_needs_config_fn(setup):
275
+ fail_missing_config_fn(setup.config_path)
276
+ else:
277
+ first = setup.issues[0]
278
+ typer.echo(f"error: {first.title}", err=True)
279
+ raise typer.Exit(code=1)
280
+ try:
281
+ settings, config_path = load_settings_fn()
282
+ if transport_override and transport_override != settings.transport:
283
+ settings = settings.model_copy(update={"transport": transport_override})
284
+ spec = build_runtime_spec_fn(
285
+ settings=settings,
286
+ config_path=config_path,
287
+ default_engine_override=default_engine_override,
288
+ reserved=RESERVED_CHAT_COMMANDS,
289
+ )
290
+ if settings.transport == "telegram":
291
+ transport_config = settings.transports.telegram
292
+ else:
293
+ transport_config = settings.transport_config(
294
+ settings.transport, config_path=config_path
295
+ )
296
+ lock_token = transport_backend.lock_token(
297
+ transport_config=transport_config,
298
+ _config_path=config_path,
299
+ )
300
+ lock_handle = acquire_config_lock_fn(config_path, lock_token)
301
+ runtime = spec.to_runtime(config_path=config_path)
302
+ transport_backend.build_and_run(
303
+ final_notify=final_notify,
304
+ default_engine_override=default_engine_override,
305
+ config_path=config_path,
306
+ transport_config=transport_config,
307
+ runtime=runtime,
308
+ )
309
+ except ConfigError as exc:
310
+ typer.echo(f"error: {exc}", err=True)
311
+ raise typer.Exit(code=1) from exc
312
+ except KeyboardInterrupt:
313
+ logger.info("shutdown.interrupted")
314
+ raise typer.Exit(code=130) from None
315
+ finally:
316
+ if lock_handle is not None:
317
+ lock_handle.release()
318
+
319
+
320
+ def _print_version_and_exit() -> None:
321
+ typer.echo(__version__)
322
+ raise typer.Exit()
323
+
324
+
325
+ def _version_callback(value: bool) -> None:
326
+ if value:
327
+ _print_version_and_exit()
328
+
329
+
330
+ def app_main(
331
+ ctx: typer.Context,
332
+ version: bool = typer.Option(
333
+ False,
334
+ "--version",
335
+ help="Show the version and exit.",
336
+ callback=_version_callback,
337
+ is_eager=True,
338
+ ),
339
+ final_notify: bool = typer.Option(
340
+ True,
341
+ "--final-notify/--no-final-notify",
342
+ help="Send the final response as a new message (not an edit).",
343
+ ),
344
+ onboard: bool = typer.Option(
345
+ False,
346
+ "--onboard/--no-onboard",
347
+ help="Run the interactive setup wizard before starting.",
348
+ ),
349
+ transport: str | None = typer.Option(
350
+ None,
351
+ "--transport",
352
+ help="Override the transport backend id.",
353
+ ),
354
+ debug: bool = typer.Option(
355
+ False,
356
+ "--debug/--no-debug",
357
+ help="Log engine JSONL, Telegram requests, and rendered messages.",
358
+ ),
359
+ ) -> None:
360
+ """Takopi CLI."""
361
+ if ctx.invoked_subcommand is None:
362
+ run_auto_router = cast(
363
+ Callable[..., None],
364
+ _resolve_cli_attr("_run_auto_router") or _run_auto_router,
365
+ )
366
+ run_auto_router(
367
+ default_engine_override=None,
368
+ transport_override=transport,
369
+ final_notify=final_notify,
370
+ debug=debug,
371
+ onboard=onboard,
372
+ )
373
+ raise typer.Exit()
374
+
375
+
376
+ def make_engine_cmd(engine_id: str) -> Callable[..., None]:
377
+ def _cmd(
378
+ final_notify: bool = typer.Option(
379
+ True,
380
+ "--final-notify/--no-final-notify",
381
+ help="Send the final response as a new message (not an edit).",
382
+ ),
383
+ onboard: bool = typer.Option(
384
+ False,
385
+ "--onboard/--no-onboard",
386
+ help="Run the interactive setup wizard before starting.",
387
+ ),
388
+ transport: str | None = typer.Option(
389
+ None,
390
+ "--transport",
391
+ help="Override the transport backend id.",
392
+ ),
393
+ debug: bool = typer.Option(
394
+ False,
395
+ "--debug/--no-debug",
396
+ help="Log engine JSONL, Telegram requests, and rendered messages.",
397
+ ),
398
+ ) -> None:
399
+ run_auto_router = cast(
400
+ Callable[..., None],
401
+ _resolve_cli_attr("_run_auto_router") or _run_auto_router,
402
+ )
403
+ run_auto_router(
404
+ default_engine_override=engine_id,
405
+ transport_override=transport,
406
+ final_notify=final_notify,
407
+ debug=debug,
408
+ onboard=onboard,
409
+ )
410
+
411
+ _cmd.__name__ = f"run_{engine_id}"
412
+ return _cmd
413
+
414
+
415
+ def _resolve_cli_attr(name: str) -> object | None:
416
+ cli_module = sys.modules.get("takopi.cli")
417
+ if cli_module is None:
418
+ return None
419
+ return getattr(cli_module, name, None)