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/utils/git.py ADDED
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from collections.abc import Sequence
5
+ from pathlib import Path
6
+
7
+
8
+ def _run_git(
9
+ args: Sequence[str], *, cwd: Path
10
+ ) -> subprocess.CompletedProcess[str] | None:
11
+ try:
12
+ return subprocess.run(
13
+ ["git", *args],
14
+ cwd=cwd,
15
+ check=False,
16
+ text=True,
17
+ capture_output=True,
18
+ )
19
+ except FileNotFoundError:
20
+ return None
21
+
22
+
23
+ def git_run(
24
+ args: Sequence[str], *, cwd: Path
25
+ ) -> subprocess.CompletedProcess[str] | None:
26
+ return _run_git(args, cwd=cwd)
27
+
28
+
29
+ def git_stdout(args: Sequence[str], *, cwd: Path) -> str | None:
30
+ result = _run_git(args, cwd=cwd)
31
+ if result is None or result.returncode != 0:
32
+ return None
33
+ output = result.stdout.strip()
34
+ return output or None
35
+
36
+
37
+ def git_ok(args: Sequence[str], *, cwd: Path) -> bool:
38
+ result = _run_git(args, cwd=cwd)
39
+ return result is not None and result.returncode == 0
40
+
41
+
42
+ def git_is_worktree(path: Path) -> bool:
43
+ top = git_stdout(
44
+ ["rev-parse", "--path-format=absolute", "--show-toplevel"],
45
+ cwd=path,
46
+ )
47
+ if not top:
48
+ return False
49
+ return Path(top).resolve(strict=False) == path.resolve(strict=False)
50
+
51
+
52
+ def resolve_default_base(root: Path) -> str | None:
53
+ origin_head = git_stdout(
54
+ ["symbolic-ref", "-q", "refs/remotes/origin/HEAD"],
55
+ cwd=root,
56
+ )
57
+ if origin_head:
58
+ prefix = "refs/remotes/origin/"
59
+ if origin_head.startswith(prefix):
60
+ name = origin_head[len(prefix) :].strip()
61
+ if name:
62
+ return name
63
+
64
+ current = git_stdout(["branch", "--show-current"], cwd=root)
65
+ if current:
66
+ return current
67
+
68
+ if git_ok(["show-ref", "--verify", "--quiet", "refs/heads/master"], cwd=root):
69
+ return "master"
70
+ if git_ok(["show-ref", "--verify", "--quiet", "refs/heads/main"], cwd=root):
71
+ return "main"
72
+ return None
73
+
74
+
75
+ def resolve_main_worktree_root(cwd: Path) -> Path | None:
76
+ common_dir = git_stdout(
77
+ ["rev-parse", "--path-format=absolute", "--git-common-dir"],
78
+ cwd=cwd,
79
+ )
80
+ if not common_dir:
81
+ return None
82
+ if git_stdout(["rev-parse", "--is-bare-repository"], cwd=cwd) == "true":
83
+ return cwd
84
+ common_path = Path(common_dir)
85
+ if not common_path.is_absolute():
86
+ common_path = (cwd / common_path).resolve()
87
+ return common_path.parent
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def atomic_write_json(
10
+ path: Path,
11
+ payload: Any,
12
+ *,
13
+ indent: int = 2,
14
+ sort_keys: bool = True,
15
+ ) -> None:
16
+ path.parent.mkdir(parents=True, exist_ok=True)
17
+ tmp_path = path.with_suffix(f"{path.suffix}.tmp")
18
+ with open(tmp_path, "w", encoding="utf-8") as handle:
19
+ json.dump(payload, handle, indent=indent, sort_keys=sort_keys)
20
+ handle.write("\n")
21
+ os.replace(tmp_path, path)
takopi/utils/paths.py ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from contextvars import ContextVar, Token
5
+ from pathlib import Path
6
+
7
+
8
+ _run_base_dir: ContextVar[Path | None] = ContextVar("takopi_run_base_dir", default=None)
9
+
10
+
11
+ def get_run_base_dir() -> Path | None:
12
+ return _run_base_dir.get()
13
+
14
+
15
+ def set_run_base_dir(base_dir: Path | None) -> Token[Path | None]:
16
+ return _run_base_dir.set(base_dir)
17
+
18
+
19
+ def reset_run_base_dir(token: Token[Path | None]) -> None:
20
+ _run_base_dir.reset(token)
21
+
22
+
23
+ def relativize_path(value: str, *, base_dir: Path | None = None) -> str:
24
+ if not value:
25
+ return value
26
+ base = get_run_base_dir() if base_dir is None else base_dir
27
+ if base is None:
28
+ base = Path.cwd()
29
+ base_str = str(base)
30
+ if not base_str:
31
+ return value
32
+ if value == base_str:
33
+ return "."
34
+ for sep in (os.sep, "/"):
35
+ prefix = base_str if base_str.endswith(sep) else f"{base_str}{sep}"
36
+ if value.startswith(prefix):
37
+ suffix = value[len(prefix) :]
38
+ return suffix or "."
39
+ return value
40
+
41
+
42
+ def relativize_command(value: str, *, base_dir: Path | None = None) -> str:
43
+ base = get_run_base_dir() if base_dir is None else base_dir
44
+ if base is None:
45
+ base = Path.cwd()
46
+ base_with_sep = f"{base}{os.sep}"
47
+ return value.replace(base_with_sep, "")
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ import sys
5
+ from typing import Any
6
+
7
+ import anyio
8
+ from anyio.abc import ByteReceiveStream
9
+ from anyio.streams.buffered import BufferedByteReceiveStream
10
+
11
+ from ..logging import log_pipeline
12
+
13
+
14
+ async def iter_bytes_lines(stream: ByteReceiveStream) -> AsyncIterator[bytes]:
15
+ buffered = BufferedByteReceiveStream(stream)
16
+ while True:
17
+ try:
18
+ line = await buffered.receive_until(b"\n", sys.maxsize)
19
+ except anyio.IncompleteRead:
20
+ return
21
+ yield line
22
+
23
+
24
+ async def drain_stderr(
25
+ stream: ByteReceiveStream,
26
+ logger: Any,
27
+ tag: str,
28
+ ) -> None:
29
+ try:
30
+ async for line in iter_bytes_lines(stream):
31
+ text = line.decode("utf-8", errors="replace")
32
+ log_pipeline(
33
+ logger,
34
+ "subprocess.stderr",
35
+ tag=tag,
36
+ line=text,
37
+ )
38
+ except Exception as exc: # noqa: BLE001
39
+ log_pipeline(
40
+ logger,
41
+ "subprocess.stderr.error",
42
+ tag=tag,
43
+ error=str(exc),
44
+ )
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import signal
5
+ from collections.abc import AsyncIterator, Callable, Sequence
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any
8
+
9
+ import anyio
10
+ from anyio.abc import Process
11
+
12
+ from ..logging import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ async def wait_for_process(proc: Process, timeout: float) -> bool:
18
+ with anyio.move_on_after(timeout) as scope:
19
+ await proc.wait()
20
+ return scope.cancel_called
21
+
22
+
23
+ def terminate_process(proc: Process) -> None:
24
+ _signal_process(
25
+ proc,
26
+ signal.SIGTERM,
27
+ fallback=proc.terminate,
28
+ log_event="subprocess.terminate.failed",
29
+ )
30
+
31
+
32
+ def kill_process(proc: Process) -> None:
33
+ _signal_process(
34
+ proc,
35
+ signal.SIGKILL,
36
+ fallback=proc.kill,
37
+ log_event="subprocess.kill.failed",
38
+ )
39
+
40
+
41
+ def _signal_process(
42
+ proc: Process,
43
+ sig: signal.Signals,
44
+ *,
45
+ fallback: Callable[[], None],
46
+ log_event: str,
47
+ ) -> None:
48
+ if proc.returncode is not None:
49
+ return
50
+ if os.name == "posix" and proc.pid is not None:
51
+ try:
52
+ os.killpg(proc.pid, sig)
53
+ return
54
+ except ProcessLookupError:
55
+ return
56
+ except OSError as exc:
57
+ logger.debug(
58
+ log_event,
59
+ error=str(exc),
60
+ error_type=exc.__class__.__name__,
61
+ pid=proc.pid,
62
+ )
63
+ try:
64
+ fallback()
65
+ except ProcessLookupError:
66
+ return
67
+
68
+
69
+ @asynccontextmanager
70
+ async def manage_subprocess(
71
+ cmd: Sequence[str], **kwargs: Any
72
+ ) -> AsyncIterator[Process]:
73
+ """Ensure subprocesses receive SIGTERM, then SIGKILL after a 2s timeout."""
74
+ if os.name == "posix":
75
+ kwargs.setdefault("start_new_session", True)
76
+ proc = await anyio.open_process(cmd, **kwargs)
77
+ try:
78
+ yield proc
79
+ finally:
80
+ if proc.returncode is None:
81
+ with anyio.CancelScope(shield=True):
82
+ terminate_process(proc)
83
+ timed_out = await wait_for_process(proc, timeout=2.0)
84
+ if timed_out:
85
+ kill_process(proc)
86
+ await proc.wait()
takopi/worktrees.py ADDED
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from .config import ProjectConfig, ProjectsConfig
6
+ from .context import RunContext
7
+ from .utils.git import (
8
+ git_is_worktree,
9
+ git_ok,
10
+ git_run,
11
+ git_stdout,
12
+ resolve_default_base,
13
+ )
14
+
15
+
16
+ class WorktreeError(RuntimeError):
17
+ pass
18
+
19
+
20
+ def resolve_run_cwd(
21
+ context: RunContext | None,
22
+ *,
23
+ projects: ProjectsConfig,
24
+ ) -> Path | None:
25
+ if context is None or context.project is None:
26
+ return None
27
+ project = projects.projects.get(context.project)
28
+ if project is None:
29
+ raise WorktreeError(f"unknown project {context.project!r}")
30
+ if context.branch is None:
31
+ return project.path
32
+ branch = _sanitize_branch(context.branch)
33
+ if _matches_project_branch(project.path, branch):
34
+ return project.path
35
+ return ensure_worktree(project, branch)
36
+
37
+
38
+ def ensure_worktree(project: ProjectConfig, branch: str) -> Path:
39
+ root = project.path
40
+ if not root.exists():
41
+ raise WorktreeError(f"project path not found: {root}")
42
+
43
+ branch = _sanitize_branch(branch)
44
+ worktrees_root = project.worktrees_root
45
+ worktree_path = worktrees_root / branch
46
+ _ensure_within_root(worktrees_root, worktree_path)
47
+
48
+ if worktree_path.exists():
49
+ if not git_is_worktree(worktree_path):
50
+ raise WorktreeError(f"{worktree_path} exists but is not a git worktree")
51
+ return worktree_path
52
+
53
+ worktrees_root.mkdir(parents=True, exist_ok=True)
54
+
55
+ if git_ok(
56
+ ["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
57
+ cwd=root,
58
+ ):
59
+ _git_worktree_add(root, worktree_path, branch)
60
+ return worktree_path
61
+
62
+ if git_ok(
63
+ ["show-ref", "--verify", "--quiet", f"refs/remotes/origin/{branch}"],
64
+ cwd=root,
65
+ ):
66
+ _git_worktree_add(
67
+ root,
68
+ worktree_path,
69
+ branch,
70
+ base_ref=f"origin/{branch}",
71
+ create_branch=True,
72
+ )
73
+ return worktree_path
74
+
75
+ base = project.worktree_base or resolve_default_base(root)
76
+ if not base:
77
+ raise WorktreeError("cannot determine base branch for new worktree")
78
+
79
+ _git_worktree_add(
80
+ root,
81
+ worktree_path,
82
+ branch,
83
+ base_ref=base,
84
+ create_branch=True,
85
+ )
86
+ return worktree_path
87
+
88
+
89
+ def _git_worktree_add(
90
+ root: Path,
91
+ worktree_path: Path,
92
+ branch: str,
93
+ *,
94
+ base_ref: str | None = None,
95
+ create_branch: bool = False,
96
+ ) -> None:
97
+ if create_branch:
98
+ if not base_ref:
99
+ raise WorktreeError("missing base ref for worktree creation")
100
+ args = ["worktree", "add", "-b", branch, str(worktree_path), base_ref]
101
+ else:
102
+ args = ["worktree", "add", str(worktree_path), branch]
103
+
104
+ result = git_run(args, cwd=root)
105
+ if result is None:
106
+ raise WorktreeError("git not available on PATH")
107
+ if result.returncode != 0:
108
+ message = result.stderr.strip() or result.stdout.strip()
109
+ raise WorktreeError(message or "git worktree add failed")
110
+
111
+
112
+ def _sanitize_branch(branch: str) -> str:
113
+ cleaned = branch.strip()
114
+ if not cleaned:
115
+ raise WorktreeError("branch name cannot be empty")
116
+ if cleaned.startswith("/"):
117
+ raise WorktreeError("branch name cannot start with '/'")
118
+ for part in Path(cleaned).parts:
119
+ if part == "..":
120
+ raise WorktreeError("branch name cannot contain '..'")
121
+ return cleaned
122
+
123
+
124
+ def _matches_project_branch(root: Path, branch: str) -> bool:
125
+ current = git_stdout(["branch", "--show-current"], cwd=root)
126
+ if not current:
127
+ return False
128
+ return current == branch
129
+
130
+
131
+ def _ensure_within_root(root: Path, path: Path) -> None:
132
+ root_resolved = root.resolve(strict=False)
133
+ path_resolved = path.resolve(strict=False)
134
+ if not path_resolved.is_relative_to(root_resolved):
135
+ raise WorktreeError("branch path escapes the worktrees directory")
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: yee88
3
+ Version: 0.1.0
4
+ Summary: Telegram bridge for Codex, Claude Code, and other agent CLIs.
5
+ Project-URL: Homepage, https://github.com/banteg/takopi
6
+ Project-URL: Documentation, https://takopi.dev/
7
+ Project-URL: Repository, https://github.com/banteg/takopi
8
+ Project-URL: Issues, https://github.com/banteg/takopi/issues
9
+ Author: yee.wang
10
+ License: MIT License
11
+
12
+ Copyright (c) 2025 banteg
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.14
37
+ Requires-Python: >=3.14
38
+ Requires-Dist: anyio>=4.12.0
39
+ Requires-Dist: httpx>=0.28.1
40
+ Requires-Dist: markdown-it-py
41
+ Requires-Dist: msgspec>=0.20.0
42
+ Requires-Dist: openai>=2.15.0
43
+ Requires-Dist: pydantic-settings>=2.12.0
44
+ Requires-Dist: pydantic>=2.12.5
45
+ Requires-Dist: questionary>=2.1.1
46
+ Requires-Dist: rich>=14.2.0
47
+ Requires-Dist: structlog>=25.5.0
48
+ Requires-Dist: sulguk>=0.11.1
49
+ Requires-Dist: tomli-w>=1.2.0
50
+ Requires-Dist: typer>=0.21.0
51
+ Requires-Dist: watchfiles>=0.21.0
52
+ Description-Content-Type: text/markdown
53
+
54
+ # yee88
55
+
56
+ telegram bridge for codex, claude code, opencode, pi.
57
+
58
+ ## quickstart
59
+
60
+ ```sh
61
+ # install
62
+ uv tool install -U yee88
63
+
64
+ # first run - follow the setup wizard
65
+ yee88
66
+
67
+ # in any git repo - create a topic and start
68
+ cd ~/your-project
69
+ yee88 topic init
70
+ yee88
71
+ ```
72
+
73
+ ## features
74
+
75
+ - projects and worktrees: work on multiple repos/branches simultaneously, branches are git worktrees
76
+ - stateless resume: continue in chat or copy the resume line to pick up in terminal
77
+ - progress streaming: commands, tools, file changes, elapsed time
78
+ - parallel runs across agent sessions, per-agent-session queue
79
+ - works with telegram features like voice notes and scheduled messages
80
+ - file transfer: send files to the repo or fetch files/dirs back
81
+ - group chats and topics: map group topics to repo/branch contexts
82
+ - works with existing anthropic and openai subscriptions
83
+
84
+ ## requirements
85
+
86
+ `uv` for installation (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
87
+
88
+ python 3.14+ (`uv python install 3.14`)
89
+
90
+ at least one engine on PATH: `codex`, `claude`, `opencode`, or `pi`
91
+
92
+ ## usage
93
+
94
+ ```sh
95
+ cd ~/dev/happy-gadgets
96
+ yee88
97
+ ```
98
+
99
+ send a message to your bot. prefix with `/codex`, `/claude`, `/opencode`, or `/pi` to pick an engine. reply to continue a thread.
100
+
101
+ register a project with `yee88 init happy-gadgets`, then target it from anywhere with `/happy-gadgets hard reset the timeline`.
102
+
103
+ mention a branch to run an agent in a dedicated worktree `/happy-gadgets @feat/memory-box freeze artifacts forever`.
104
+
105
+ inspect or update settings with `yee88 config list`, `yee88 config get`, and `yee88 config set`.
106
+
107
+ ## plugins
108
+
109
+ takopi supports entrypoint-based plugins for engines, transports, and commands.
110
+
111
+ see [`docs/how-to/write-a-plugin.md`](docs/how-to/write-a-plugin.md) and [`docs/reference/plugin-api.md`](docs/reference/plugin-api.md).
112
+
113
+ ## development
114
+
115
+ see [`docs/reference/specification.md`](docs/reference/specification.md) and [`docs/developing.md`](docs/developing.md).
116
+
@@ -0,0 +1,103 @@
1
+ takopi/__init__.py,sha256=GyaC3hhx285KqPcqukJ07gRSFkuES5T27aUtAwvYYRw,23
2
+ takopi/api.py,sha256=SnUoG0xnjgaSyqM9retJS9wd3axBEI8MCOkrBNz4w8A,2876
3
+ takopi/backends.py,sha256=80ScxPEse-BxVfK0zA6bAaorp2GpTpCWsgX3TTqmumI,543
4
+ takopi/backends_helpers.py,sha256=d84XilyOzVVCq5akygVbH85JbapjTaSUZKeGcAml15I,368
5
+ takopi/commands.py,sha256=DplCjFC8QQM2MYoblZS8HI6OngKmQLO2emxCi8NIOtY,3298
6
+ takopi/config.py,sha256=FqABa_fsTbI4-7rr82c89OhHMNlLYsUbr1mflD4yTXc,4307
7
+ takopi/config_migrations.py,sha256=H-wJZz0HcwmTcNCulXj1nFR8I2ilUQtjo5upMRLrPWc,3501
8
+ takopi/config_watch.py,sha256=GBu4KrlgKACWalPFK-7C3Qo1v1BiOYOiOtK5OUOqz9c,4390
9
+ takopi/context.py,sha256=2GXdWY8KpWbEARJNo34unLN8nqGBtiE72Z_mjwrJTUM,187
10
+ takopi/directives.py,sha256=28XG1f7yQs8GAgvPoy5g0pTe_aaVQeWFSD4-S5mC5oo,4583
11
+ takopi/engines.py,sha256=uXtGie-XkQDB1LROTjPlCkYKbCu7XgyTr0yGn837gN4,1646
12
+ takopi/events.py,sha256=1yo6l5D4xFreyPoU1d4L1ckaplPizxpFeJJevP8JDtk,4225
13
+ takopi/ids.py,sha256=uU2A7x61Xn3_Pj-YgY5-EruoOtwDSXOp2D-XduTqAEo,534
14
+ takopi/lockfile.py,sha256=xCSsxVQMqnCN2RWFkkDOVg1qm5D-Sb4AMKr6FdP0kbY,4162
15
+ takopi/logging.py,sha256=BylZsaQB4pNhAteTdsXGAl0_PUoY8JQ4EFCvOhuB5bo,8228
16
+ takopi/markdown.py,sha256=YCx81lxBp_fchx3SN4Z-0sXNQGY1YjZJWz0m_xtk-uM,9068
17
+ takopi/model.py,sha256=G8Aj2nyTM6_vmHh12vEXlvhNJHR1ZBpsPBKw-DboDbs,1695
18
+ takopi/plugins.py,sha256=ItXdxlbZaikJOmJiUWQLFS9OvfMvJVy4seOnwDoaMHE,9153
19
+ takopi/presenter.py,sha256=rhPIytd2ANoe5S6kGeVvTzsPSQzO5AEMBXK3F_zJEx4,513
20
+ takopi/progress.py,sha256=n5HmZcw1dSuDfhrOXiZhLXdGMMSJNFpfZ4NR_2-wOL4,3155
21
+ takopi/router.py,sha256=pZkFI7OsUYeqjS2JmnL4gmHSPbEg8WF7_mQhzV45BCE,3853
22
+ takopi/runner.py,sha256=kaXscM6WBXa8TwDhzEyf2am4V95hKXTzfb78F1l3how,21476
23
+ takopi/runner_bridge.py,sha256=XGHpxTvmmk-DfiZCgNztXwboJ5m5z9GPKRK_8MxbRWA,19103
24
+ takopi/runtime_loader.py,sha256=i-4IeZqcez1VxWqfXsgmHhakK5g53ecye1kW46Uzc_c,6242
25
+ takopi/scheduler.py,sha256=ss_90rpZ3aKh_GutKhMwn8KWpg5QykkE2r6-ltimExk,5590
26
+ takopi/settings.py,sha256=uZcTzWdbX65Iy7wgLBPHiE_MalpSamWlOfqBJNE6De8,12064
27
+ takopi/transport.py,sha256=hzVJJO4mk1CaXgzsxSiMDqYA8xKMPD-SSQKhHDnwlzE,1330
28
+ takopi/transport_runtime.py,sha256=QdPEJ2rCeCsbRD_-77ZG74sRa2ws3WLAXptPPgssw2c,10260
29
+ takopi/transports.py,sha256=8igVsWgvx4d743SE98LyNuKYaci0-X61IqNiO4U-_Ck,2013
30
+ takopi/worktrees.py,sha256=KDaT1S0-kQH5v0FmiWgG51jgGviyATVEgfoqRrLmh7Y,3907
31
+ takopi/cli/__init__.py,sha256=YcM5Z_Lavn5PbuDH_0y6o6Fc-0Ca56SBec6XwKhJPLI,6442
32
+ takopi/cli/config.py,sha256=KXofbUieznU4OU0O-Atq57lPrigGmwmWGq27DC0EwmA,9563
33
+ takopi/cli/doctor.py,sha256=YrSRvHvI2Vew5Dw33u3D2OX4SIKKhQ1EDyj5cYQfcPc,6003
34
+ takopi/cli/init.py,sha256=y1Z08rh9t4RPhzsgJRtopgd6UvjEPYi9Wfv_t2KlrxI,3761
35
+ takopi/cli/onboarding_cmd.py,sha256=nj7mj2UuYQ5j0ZryFvb8INn4wDcOQU2_tWGZCcOnSKc,4172
36
+ takopi/cli/plugins.py,sha256=Kadu-EeX_UO9hSgloo7od6gmCzJqpyBX4E0skf_qFno,6320
37
+ takopi/cli/run.py,sha256=2KtKoOT1LGEKlizr8MYaCgxX5j3O7jlfPPgnU1UkSCE,13919
38
+ takopi/cli/topic.py,sha256=VPDCc_P5dFUa-FkvwZK7jEVVR9jHqVXmFwzYemTRMW8,11024
39
+ takopi/runners/__init__.py,sha256=McKaMqLXT9dJlgiEwKf6biD0Ns66Fk7SrxwtcP0ZgzI,30
40
+ takopi/runners/claude.py,sha256=-s88BOSTWxp4e7r6QJgUV3RJsEMEfSjk0Z9OmnZQB9Y,15296
41
+ takopi/runners/codex.py,sha256=oen08lhgaDdEn52x-GDcL7m4Hiaq9Vz3kb4b8KXh3yc,20956
42
+ takopi/runners/mock.py,sha256=zKnxNeFkTWyWXe35wUkm3hwf1vOi_eTXfNwIxaVWIjw,6617
43
+ takopi/runners/opencode.py,sha256=QEvpSSRtP5Uv-kzEp6D-85o2II886IHsEV-Us8vqsjA,15018
44
+ takopi/runners/pi.py,sha256=PH8xw_IBZzDo1KQ6AUQEPhqVRo-kb5I2OVKJrIuJq0w,16087
45
+ takopi/runners/run_options.py,sha256=ZjEm2baRwyJjRGCDTegnNreBA1qzQkaBJO_q4ZSnSw8,916
46
+ takopi/runners/tool_actions.py,sha256=Gcfhl_-W8WtIKwM2d1pkTVnVuk3h310eSD-_DvKfqOc,2732
47
+ takopi/schemas/__init__.py,sha256=gt6BRlYw2nKYLKkidkTDdK9RTmDEEDDpSi3LIr_pccw,46
48
+ takopi/schemas/claude.py,sha256=HqOik1O4u3WcMb4RN2cTVJw6VRYn3EaYj8h5Sevs1XY,5702
49
+ takopi/schemas/codex.py,sha256=bgIsh35LuaqoOYdTl6BWR-mn-mvh3ouwes_Jn9JMVXg,3412
50
+ takopi/schemas/opencode.py,sha256=ODhnKXTzxZ_8qaQ7AYqXB7J-XoAjQnXbGMBVTUEM2qY,1175
51
+ takopi/schemas/pi.py,sha256=e5ETawxk8jrdJbEbeBI_pWQKeCFiBBZLEF-Wo5Fx0XY,2680
52
+ takopi/telegram/__init__.py,sha256=hX_Wvu920dNLTDrKlj0fsZFSewOo-_otN26O1UNPNA4,452
53
+ takopi/telegram/api_models.py,sha256=d3H4o2sRZuFgfZfxF1Y1z_xzzCoOy3bKhMeKggYCW3w,538
54
+ takopi/telegram/api_schemas.py,sha256=Hvym_s2iPEaJmviCRGrLy8JarbcOburdE7aga2XnV_8,3824
55
+ takopi/telegram/backend.py,sha256=lpOGSeYSUY_PuUy4LmWcKf4T76m8f_aYYb49GIglPJA,5783
56
+ takopi/telegram/bridge.py,sha256=UQ4tqYdH8yEme5K6VGTOPaMKo2zIP0hAyJ6ev80izAA,13540
57
+ takopi/telegram/chat_prefs.py,sha256=tH7dXIehYHdemN_JOMhB7VuuFOiSBcjKz3FXpfW77Dk,8441
58
+ takopi/telegram/chat_sessions.py,sha256=7uL5bGX5BaQbQYTEpkWZVMh6HIImWdR_pPbqf6C4k0A,3736
59
+ takopi/telegram/client.py,sha256=GqNSCGA0cICaOC3E-qX7E4pcUTeP8edXS9HILYZNZgU,12800
60
+ takopi/telegram/client_api.py,sha256=6gDCrPQSBHHBhyiXX9cCwvNuQChsFXh0z8BvCgGD8Es,17692
61
+ takopi/telegram/context.py,sha256=owY5ZH2iLlS6kGS0DLPLAwNXkbcn34EfdzpWa9eSVR0,4485
62
+ takopi/telegram/engine_defaults.py,sha256=n6ROkTmP_s-H5AhPz_OdT62oZf0QtZJyFEDjp5gfub4,2594
63
+ takopi/telegram/engine_overrides.py,sha256=kv2j102VP-Bqzbutd5ApBkjW3LmVwvCYixsFewVXVeY,3122
64
+ takopi/telegram/files.py,sha256=Rc_wYdKHDC4hawL7pSuKznPbLiNnhPP4Lp0uGNGESv0,5054
65
+ takopi/telegram/loop.py,sha256=ytp6HGoHxnXOhA-yg2bid0wZmxM15cnAK-RSx4qy_Ak,66912
66
+ takopi/telegram/onboarding.py,sha256=YdrjSwZoebwgDPmuEhpqW0dmXJSHbStBOZxnmrVSMzc,34218
67
+ takopi/telegram/outbox.py,sha256=OcoRyQ7zmQCXR8ZXEMK2f_7-UMRVRAbBgmJGS1u_lcU,5939
68
+ takopi/telegram/parsing.py,sha256=5PvIPns1NnKryt3XNxPCp4BpWX1gI8kjKi4VxcQ0W-Q,7429
69
+ takopi/telegram/render.py,sha256=tZuKUeiG6qBb7S_6Y90MBSuSFRUHVbcL-mXWC1r9fHA,6017
70
+ takopi/telegram/state_store.py,sha256=bo0D0dJvdPIbzb7DE7HELbwnJ-VEw2p8AsPgj99an14,2551
71
+ takopi/telegram/topic_state.py,sha256=I8V6p50Hf7fGMKLCVNMXkqaKI46Yvm80Q5l4LaVHrYU,11444
72
+ takopi/telegram/topics.py,sha256=TP9cFkWvbZXaJ_41Gmwv2vO6vmim1c_RkrtMkQ9RiQE,8129
73
+ takopi/telegram/trigger_mode.py,sha256=4wFjNbEANn45lpctO9uBhjXTGh1K3LOUKWlBEA6m6Ps,2070
74
+ takopi/telegram/types.py,sha256=7MQCR0_k1zDtF32h3VoRxPZM7IZe9K6nCcy0xW11Lus,1540
75
+ takopi/telegram/voice.py,sha256=6GsqgVXSCUCXupyS6S235BXXwTp4gSLmYQLBYWAGdfc,3312
76
+ takopi/telegram/commands/__init__.py,sha256=lZfpONZyhgsc7L02B6ZS4TWyhTShWNxIyfRQLIKvvo8,285
77
+ takopi/telegram/commands/agent.py,sha256=Dtd8N0f3JyJM3gTb1CxQ8-0ez_ZR4DtrnrO9SpKBBQU,6969
78
+ takopi/telegram/commands/cancel.py,sha256=jE93VjztNETlmAgb7FJX0sLR3O-NHy5XcraUbK13TLs,3884
79
+ takopi/telegram/commands/dispatch.py,sha256=zcvX0V6_klf5jXqJlBwAK25e83WC9z3-KoRcDbeWre0,3572
80
+ takopi/telegram/commands/executor.py,sha256=um6KbvneUpzGu8GPujtYJB-tFvWmN95xvLHVN-Vuj8g,14678
81
+ takopi/telegram/commands/file_transfer.py,sha256=yfopf_If9f7Scaz4dlUsfcrVvg24QmQdajLzemaENy0,17697
82
+ takopi/telegram/commands/handlers.py,sha256=2zySjRW49e1iv2QJW6xq2m9j0t-uz9_FCJZpE7NhIDA,1834
83
+ takopi/telegram/commands/media.py,sha256=drSKTf_BbyvXOGhS9UKwey_243Ic5XissoaxCykpd-c,5045
84
+ takopi/telegram/commands/menu.py,sha256=AvEgKQUZageBx7TwV2-V_IGhT7AC0qUTtAHjcLIweNY,4466
85
+ takopi/telegram/commands/model.py,sha256=P6rW_N6Q8OtjU3md3QUMKIgLskLuvJXB2uKoyk5vtfk,7512
86
+ takopi/telegram/commands/overrides.py,sha256=lLlIuCWkKwbS5WlQOOr_Ftv_EvfHR9DhjBpWaf6FBng,4853
87
+ takopi/telegram/commands/parse.py,sha256=0QVW1TVdBWadLbpJ9lRY3s7W4Cm62JJa9jfAaFHQmXU,887
88
+ takopi/telegram/commands/plan.py,sha256=iKsaRBS-qIfvAaxik5ZEA_VzAnFwx7aEED8sKXNq1wE,487
89
+ takopi/telegram/commands/reasoning.py,sha256=UFEJOHm4d0v2jFh5HC3oQGS41NYKbmJHRTaAmu_LiGo,8188
90
+ takopi/telegram/commands/reply.py,sha256=a3zkNjKzn3qZXEZFXuflX-tdhQKQyhYDqZskMy1nS3o,580
91
+ takopi/telegram/commands/topics.py,sha256=A_a6XcA2xXzw9aOpS4_OIEp5XWMGwMZfHBXni3XRg6g,10799
92
+ takopi/telegram/commands/trigger.py,sha256=RgB4V7NoFdfDLk83xl3BPTsIIVr5A2zCNstR_5B_mEw,5201
93
+ takopi/utils/__init__.py,sha256=cV9_7v07Y6XkrUju0lHdO1ia0-Q57NqyFVMaFCg--do,34
94
+ takopi/utils/git.py,sha256=SVKcPNl2mUrv-cVHZQ5b8QXWKi33e_Jc4_vfCLwagkg,2413
95
+ takopi/utils/json_state.py,sha256=cnSvGbB9zj90GdYSyigId1M0nEx54T3A3CqqhkAm9kQ,524
96
+ takopi/utils/paths.py,sha256=HQkXwRdh7coKyeb3lUpgEsLvmQEYd-f9aw7WpIfdu3Q,1319
97
+ takopi/utils/streams.py,sha256=TQezA-A5VCNksLOtwsJplfr8vm1xPTXoGxvik8G2NPI,1121
98
+ takopi/utils/subprocess.py,sha256=2if6IxTZVSB1kDa8SXw3igj3E-zhKB8P4z5MVe-odzY,2169
99
+ yee88-0.1.0.dist-info/METADATA,sha256=Tbk1Ofl-v1xuHJNeiT6M1zs8TDy0K8hloOvDvIxzEPo,4314
100
+ yee88-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
101
+ yee88-0.1.0.dist-info/entry_points.txt,sha256=X7Mgvla-mFDUCT_NqPFbuL_82GqVGIMJV2SRLbViBXA,290
102
+ yee88-0.1.0.dist-info/licenses/LICENSE,sha256=poyQ59wnbmL3Ox3TiiephfHvUpLvJl0DwLFFgqBDdHY,1063
103
+ yee88-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,11 @@
1
+ [console_scripts]
2
+ yee88 = takopi.cli:main
3
+
4
+ [takopi.engine_backends]
5
+ claude = takopi.runners.claude:BACKEND
6
+ codex = takopi.runners.codex:BACKEND
7
+ opencode = takopi.runners.opencode:BACKEND
8
+ pi = takopi.runners.pi:BACKEND
9
+
10
+ [takopi.transport_backends]
11
+ telegram = takopi.telegram.backend:BACKEND
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 banteg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.