yee88 0.6.2__tar.gz → 0.7.0__tar.gz

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 (272) hide show
  1. {yee88-0.6.2 → yee88-0.7.0}/PKG-INFO +1 -1
  2. {yee88-0.6.2 → yee88-0.7.0}/pyproject.toml +1 -1
  3. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/__init__.py +2 -0
  4. yee88-0.7.0/src/yee88/cli/handoff.py +302 -0
  5. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/manager.py +54 -17
  6. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/skills/yee88/SKILL.md +31 -6
  7. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/model.py +42 -1
  8. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/loop.py +2 -1
  9. yee88-0.7.0/tests/test_cron_manager.py +422 -0
  10. yee88-0.7.0/tests/test_cron_scheduler.py +143 -0
  11. {yee88-0.6.2 → yee88-0.7.0}/uv.lock +1 -1
  12. yee88-0.6.2/.sisyphus/plans/yee88-architecture-review.md +0 -581
  13. {yee88-0.6.2 → yee88-0.7.0}/.codex/AGENTS.md +0 -0
  14. {yee88-0.6.2 → yee88-0.7.0}/.github/workflows/ci.yml +0 -0
  15. {yee88-0.6.2 → yee88-0.7.0}/.github/workflows/release.yml +0 -0
  16. {yee88-0.6.2 → yee88-0.7.0}/.gitignore +0 -0
  17. {yee88-0.6.2 → yee88-0.7.0}/Justfile +0 -0
  18. {yee88-0.6.2 → yee88-0.7.0}/LICENSE +0 -0
  19. {yee88-0.6.2 → yee88-0.7.0}/README.md +0 -0
  20. {yee88-0.6.2 → yee88-0.7.0}/changelog.md +0 -0
  21. {yee88-0.6.2 → yee88-0.7.0}/docs/assets/favicon.svg +0 -0
  22. {yee88-0.6.2 → yee88-0.7.0}/docs/assets/logo.svg +0 -0
  23. {yee88-0.6.2 → yee88-0.7.0}/docs/assets/og-image.jpg +0 -0
  24. {yee88-0.6.2 → yee88-0.7.0}/docs/assets/takopi.svg +0 -0
  25. {yee88-0.6.2 → yee88-0.7.0}/docs/developing.md +0 -0
  26. {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/architecture.md +0 -0
  27. {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/index.md +0 -0
  28. {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/module-map.md +0 -0
  29. {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/plugin-system.md +0 -0
  30. {yee88-0.6.2 → yee88-0.7.0}/docs/explanation/routing-and-sessions.md +0 -0
  31. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/add-a-runner.md +0 -0
  32. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/chat-sessions.md +0 -0
  33. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/dev-setup.md +0 -0
  34. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/file-transfer.md +0 -0
  35. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/index.md +0 -0
  36. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/projects.md +0 -0
  37. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/route-by-chat.md +0 -0
  38. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/schedule-tasks.md +0 -0
  39. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/switch-engines.md +0 -0
  40. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/topics.md +0 -0
  41. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/troubleshooting.md +0 -0
  42. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/voice-notes.md +0 -0
  43. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/worktrees.md +0 -0
  44. {yee88-0.6.2 → yee88-0.7.0}/docs/how-to/write-a-plugin.md +0 -0
  45. {yee88-0.6.2 → yee88-0.7.0}/docs/index.md +0 -0
  46. {yee88-0.6.2 → yee88-0.7.0}/docs/javascripts/hero-chat.js +0 -0
  47. {yee88-0.6.2 → yee88-0.7.0}/docs/overrides/.icons/takopi/takopi.svg +0 -0
  48. {yee88-0.6.2 → yee88-0.7.0}/docs/overrides/main.html +0 -0
  49. {yee88-0.6.2 → yee88-0.7.0}/docs/plugins.md +0 -0
  50. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/agents/index.md +0 -0
  51. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/agents/invariants.md +0 -0
  52. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/agents/repo-map.md +0 -0
  53. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/commands-and-directives.md +0 -0
  54. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/config.md +0 -0
  55. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/context-resolution.md +0 -0
  56. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/env-vars.md +0 -0
  57. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/index.md +0 -0
  58. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/plugin-api.md +0 -0
  59. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/plugins.md +0 -0
  60. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/claude/runner.md +0 -0
  61. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/claude/stream-json-cheatsheet.md +0 -0
  62. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/claude/takopi-events.md +0 -0
  63. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/codex/exec-json-cheatsheet.md +0 -0
  64. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/codex/takopi-events.md +0 -0
  65. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/index.md +0 -0
  66. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/opencode/runner.md +0 -0
  67. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/opencode/stream-json-cheatsheet.md +0 -0
  68. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/opencode/takopi-events.md +0 -0
  69. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/pi/runner.md +0 -0
  70. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/pi/stream-json-cheatsheet.md +0 -0
  71. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/runners/pi/takopi-events.md +0 -0
  72. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/specification.md +0 -0
  73. {yee88-0.6.2 → yee88-0.7.0}/docs/reference/transports/telegram.md +0 -0
  74. {yee88-0.6.2 → yee88-0.7.0}/docs/stylesheets/admonitions.css +0 -0
  75. {yee88-0.6.2 → yee88-0.7.0}/docs/stylesheets/hero-chat.css +0 -0
  76. {yee88-0.6.2 → yee88-0.7.0}/docs/stylesheets/workflow-preview.css +0 -0
  77. {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/conversation-modes.md +0 -0
  78. {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/first-run.md +0 -0
  79. {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/index.md +0 -0
  80. {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/install.md +0 -0
  81. {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/multi-engine.md +0 -0
  82. {yee88-0.6.2 → yee88-0.7.0}/docs/tutorials/projects-and-branches.md +0 -0
  83. {yee88-0.6.2 → yee88-0.7.0}/docs/user-guide-zh.md +0 -0
  84. {yee88-0.6.2 → yee88-0.7.0}/docs/user-guide.md +0 -0
  85. {yee88-0.6.2 → yee88-0.7.0}/readme.md +0 -0
  86. {yee88-0.6.2 → yee88-0.7.0}/scripts/commit_notify.py +0 -0
  87. {yee88-0.6.2 → yee88-0.7.0}/scripts/docs_build_cf.sh +0 -0
  88. {yee88-0.6.2 → yee88-0.7.0}/scripts/docs_prebuild.py +0 -0
  89. {yee88-0.6.2 → yee88-0.7.0}/scripts/onboarding_preview.py +0 -0
  90. {yee88-0.6.2 → yee88-0.7.0}/scripts/release_notify.py +0 -0
  91. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/__init__.py +0 -0
  92. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/api.py +0 -0
  93. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/backends.py +0 -0
  94. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/backends_helpers.py +0 -0
  95. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/config.py +0 -0
  96. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/cron.py +0 -0
  97. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/doctor.py +0 -0
  98. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/init.py +0 -0
  99. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/onboarding_cmd.py +0 -0
  100. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/plugins.py +0 -0
  101. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/reload.py +0 -0
  102. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/run.py +0 -0
  103. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cli/topic.py +0 -0
  104. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/commands.py +0 -0
  105. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/config.py +0 -0
  106. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/config_migrations.py +0 -0
  107. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/config_watch.py +0 -0
  108. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/context.py +0 -0
  109. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/__init__.py +0 -0
  110. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/models.py +0 -0
  111. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/cron/scheduler.py +0 -0
  112. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/directives.py +0 -0
  113. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/engines.py +0 -0
  114. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/events.py +0 -0
  115. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/ids.py +0 -0
  116. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/lockfile.py +0 -0
  117. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/logging.py +0 -0
  118. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/markdown.py +0 -0
  119. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/model.py +0 -0
  120. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/plugins.py +0 -0
  121. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/presenter.py +0 -0
  122. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/progress.py +0 -0
  123. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/router.py +0 -0
  124. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runner.py +0 -0
  125. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runner_bridge.py +0 -0
  126. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/__init__.py +0 -0
  127. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/claude.py +0 -0
  128. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/codex.py +0 -0
  129. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/mock.py +0 -0
  130. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/opencode.py +0 -0
  131. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/pi.py +0 -0
  132. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/run_options.py +0 -0
  133. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runners/tool_actions.py +0 -0
  134. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/runtime_loader.py +0 -0
  135. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/scheduler.py +0 -0
  136. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/__init__.py +0 -0
  137. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/claude.py +0 -0
  138. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/codex.py +0 -0
  139. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/opencode.py +0 -0
  140. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/schemas/pi.py +0 -0
  141. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/settings.py +0 -0
  142. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/__init__.py +0 -0
  143. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/api_models.py +0 -0
  144. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/api_schemas.py +0 -0
  145. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/backend.py +0 -0
  146. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/bridge.py +0 -0
  147. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/chat_prefs.py +0 -0
  148. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/chat_sessions.py +0 -0
  149. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/client.py +0 -0
  150. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/client_api.py +0 -0
  151. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/__init__.py +0 -0
  152. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/agent.py +0 -0
  153. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/cancel.py +0 -0
  154. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/dispatch.py +0 -0
  155. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/executor.py +0 -0
  156. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/file_transfer.py +0 -0
  157. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/handlers.py +0 -0
  158. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/media.py +0 -0
  159. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/menu.py +0 -0
  160. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/overrides.py +0 -0
  161. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/parse.py +0 -0
  162. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/plan.py +0 -0
  163. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/reasoning.py +0 -0
  164. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/reply.py +0 -0
  165. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/topics.py +0 -0
  166. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/commands/trigger.py +0 -0
  167. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/context.py +0 -0
  168. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/engine_defaults.py +0 -0
  169. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/engine_overrides.py +0 -0
  170. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/files.py +0 -0
  171. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/onboarding.py +0 -0
  172. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/outbox.py +0 -0
  173. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/parsing.py +0 -0
  174. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/render.py +0 -0
  175. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/state_store.py +0 -0
  176. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/topic_state.py +0 -0
  177. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/topics.py +0 -0
  178. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/trigger_mode.py +0 -0
  179. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/types.py +0 -0
  180. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/telegram/voice.py +0 -0
  181. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/transport.py +0 -0
  182. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/transport_runtime.py +0 -0
  183. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/transports.py +0 -0
  184. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/__init__.py +0 -0
  185. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/git.py +0 -0
  186. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/json_state.py +0 -0
  187. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/paths.py +0 -0
  188. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/streams.py +0 -0
  189. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/utils/subprocess.py +0 -0
  190. {yee88-0.6.2 → yee88-0.7.0}/src/yee88/worktrees.py +0 -0
  191. {yee88-0.6.2 → yee88-0.7.0}/tests/__init__.py +0 -0
  192. {yee88-0.6.2 → yee88-0.7.0}/tests/conftest.py +0 -0
  193. {yee88-0.6.2 → yee88-0.7.0}/tests/factories.py +0 -0
  194. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/claude_stream_json_session.jsonl +0 -0
  195. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/codex_exec_json_all_formats.jsonl +0 -0
  196. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/codex_exec_json_all_formats.txt +0 -0
  197. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_run_json.jsonl +0 -0
  198. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_stream_error.jsonl +0 -0
  199. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_stream_success.jsonl +0 -0
  200. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/opencode_stream_success_no_reason.jsonl +0 -0
  201. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/pi_print_mode_events.jsonl +0 -0
  202. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/pi_stream_error.jsonl +0 -0
  203. {yee88-0.6.2 → yee88-0.7.0}/tests/fixtures/pi_stream_success.jsonl +0 -0
  204. {yee88-0.6.2 → yee88-0.7.0}/tests/plugin_fixtures.py +0 -0
  205. {yee88-0.6.2 → yee88-0.7.0}/tests/telegram_fakes.py +0 -0
  206. {yee88-0.6.2 → yee88-0.7.0}/tests/test_api_exports.py +0 -0
  207. {yee88-0.6.2 → yee88-0.7.0}/tests/test_auto_router.py +0 -0
  208. {yee88-0.6.2 → yee88-0.7.0}/tests/test_claude_runner.py +0 -0
  209. {yee88-0.6.2 → yee88-0.7.0}/tests/test_claude_schema.py +0 -0
  210. {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_auto_router.py +0 -0
  211. {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_chat_id.py +0 -0
  212. {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_commands.py +0 -0
  213. {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_config.py +0 -0
  214. {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_doctor.py +0 -0
  215. {yee88-0.6.2 → yee88-0.7.0}/tests/test_cli_helpers.py +0 -0
  216. {yee88-0.6.2 → yee88-0.7.0}/tests/test_codex_runner_helpers.py +0 -0
  217. {yee88-0.6.2 → yee88-0.7.0}/tests/test_codex_schema.py +0 -0
  218. {yee88-0.6.2 → yee88-0.7.0}/tests/test_codex_tool_result_summary.py +0 -0
  219. {yee88-0.6.2 → yee88-0.7.0}/tests/test_command_registry.py +0 -0
  220. {yee88-0.6.2 → yee88-0.7.0}/tests/test_config_store.py +0 -0
  221. {yee88-0.6.2 → yee88-0.7.0}/tests/test_config_watch.py +0 -0
  222. {yee88-0.6.2 → yee88-0.7.0}/tests/test_engine_discovery.py +0 -0
  223. {yee88-0.6.2 → yee88-0.7.0}/tests/test_exec_bridge.py +0 -0
  224. {yee88-0.6.2 → yee88-0.7.0}/tests/test_exec_render.py +0 -0
  225. {yee88-0.6.2 → yee88-0.7.0}/tests/test_exec_runner.py +0 -0
  226. {yee88-0.6.2 → yee88-0.7.0}/tests/test_git_utils.py +0 -0
  227. {yee88-0.6.2 → yee88-0.7.0}/tests/test_lockfile.py +0 -0
  228. {yee88-0.6.2 → yee88-0.7.0}/tests/test_onboarding.py +0 -0
  229. {yee88-0.6.2 → yee88-0.7.0}/tests/test_onboarding_helpers.py +0 -0
  230. {yee88-0.6.2 → yee88-0.7.0}/tests/test_onboarding_interactive.py +0 -0
  231. {yee88-0.6.2 → yee88-0.7.0}/tests/test_opencode_runner.py +0 -0
  232. {yee88-0.6.2 → yee88-0.7.0}/tests/test_opencode_schema.py +0 -0
  233. {yee88-0.6.2 → yee88-0.7.0}/tests/test_paths.py +0 -0
  234. {yee88-0.6.2 → yee88-0.7.0}/tests/test_pi_runner.py +0 -0
  235. {yee88-0.6.2 → yee88-0.7.0}/tests/test_pi_schema.py +0 -0
  236. {yee88-0.6.2 → yee88-0.7.0}/tests/test_plugins.py +0 -0
  237. {yee88-0.6.2 → yee88-0.7.0}/tests/test_projects_config.py +0 -0
  238. {yee88-0.6.2 → yee88-0.7.0}/tests/test_rendering.py +0 -0
  239. {yee88-0.6.2 → yee88-0.7.0}/tests/test_runner_contract.py +0 -0
  240. {yee88-0.6.2 → yee88-0.7.0}/tests/test_runner_run_options.py +0 -0
  241. {yee88-0.6.2 → yee88-0.7.0}/tests/test_runner_utils.py +0 -0
  242. {yee88-0.6.2 → yee88-0.7.0}/tests/test_runtime_loader.py +0 -0
  243. {yee88-0.6.2 → yee88-0.7.0}/tests/test_settings.py +0 -0
  244. {yee88-0.6.2 → yee88-0.7.0}/tests/test_settings_contract.py +0 -0
  245. {yee88-0.6.2 → yee88-0.7.0}/tests/test_subprocess.py +0 -0
  246. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_agent_trigger_commands.py +0 -0
  247. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_backend.py +0 -0
  248. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_bridge.py +0 -0
  249. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_chat_prefs.py +0 -0
  250. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_chat_sessions.py +0 -0
  251. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_client.py +0 -0
  252. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_client_api.py +0 -0
  253. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_context_helpers.py +0 -0
  254. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_engine_defaults.py +0 -0
  255. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_engine_overrides.py +0 -0
  256. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_file_transfer_helpers.py +0 -0
  257. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_files.py +0 -0
  258. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_incoming.py +0 -0
  259. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_media_command.py +0 -0
  260. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_polling.py +0 -0
  261. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_queue.py +0 -0
  262. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_topic_state.py +0 -0
  263. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_topics_command.py +0 -0
  264. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_topics_helpers.py +0 -0
  265. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_trigger_mode.py +0 -0
  266. {yee88-0.6.2 → yee88-0.7.0}/tests/test_telegram_voice.py +0 -0
  267. {yee88-0.6.2 → yee88-0.7.0}/tests/test_tool_actions.py +0 -0
  268. {yee88-0.6.2 → yee88-0.7.0}/tests/test_transport.py +0 -0
  269. {yee88-0.6.2 → yee88-0.7.0}/tests/test_transport_registry.py +0 -0
  270. {yee88-0.6.2 → yee88-0.7.0}/tests/test_transport_runtime.py +0 -0
  271. {yee88-0.6.2 → yee88-0.7.0}/tests/test_worktrees.py +0 -0
  272. {yee88-0.6.2 → yee88-0.7.0}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yee88
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: Telegram bridge for Codex, Claude Code, and other agent CLIs.
5
5
  Project-URL: Homepage, https://github.com/banteg/yee88
6
6
  Project-URL: Documentation, https://yee88.dev/
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "yee88"
3
3
  authors = [{name = "yee.wang"}]
4
- version = "0.6.2"
4
+ version = "0.7.0"
5
5
  description = "Telegram bridge for Codex, Claude Code, and other agent CLIs."
6
6
  readme = "README.md"
7
7
  license = { file = "LICENSE" }
@@ -92,6 +92,7 @@ from .config import (
92
92
  config_unset,
93
93
  )
94
94
  from .cron import app as cron_app
95
+ from .handoff import app as handoff_app
95
96
  from .reload import reload_command
96
97
 
97
98
 
@@ -215,6 +216,7 @@ def create_app() -> typer.Typer:
215
216
  app.command(name="plugins")(plugins_cmd)
216
217
  app.add_typer(config_app, name="config")
217
218
  app.add_typer(cron_app, name="cron")
219
+ app.add_typer(handoff_app, name="handoff")
218
220
  app.command(name="reload")(reload_command)
219
221
  app.callback()(app_main)
220
222
  for engine_id in _engine_ids_for_cli():
@@ -0,0 +1,302 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ import anyio
10
+ import typer
11
+
12
+ from ..context import RunContext
13
+ from ..model import ResumeToken
14
+ from ..settings import load_settings_if_exists
15
+ from ..telegram.client import TelegramClient
16
+ from ..telegram.topic_state import TopicStateStore, resolve_state_path
17
+
18
+ app = typer.Typer(help="Handoff session context to Telegram")
19
+
20
+ OPENCODE_STORAGE = Path.home() / ".local" / "share" / "opencode" / "storage"
21
+
22
+
23
+ @dataclass
24
+ class SessionInfo:
25
+ id: str
26
+ directory: str
27
+ updated: float
28
+ title: str
29
+
30
+ @property
31
+ def project_name(self) -> str:
32
+ return Path(self.directory).name if self.directory else "unknown"
33
+
34
+ @property
35
+ def updated_str(self) -> str:
36
+ return datetime.fromtimestamp(self.updated / 1000).strftime("%m-%d %H:%M")
37
+
38
+
39
+ def _get_recent_sessions(limit: int = 10) -> list[SessionInfo]:
40
+ try:
41
+ result = subprocess.run(
42
+ ["opencode", "session", "list", "--format", "json", "-n", str(limit)],
43
+ capture_output=True,
44
+ text=True,
45
+ check=True,
46
+ )
47
+ data = json.loads(result.stdout)
48
+ return [
49
+ SessionInfo(
50
+ id=s.get("id", ""),
51
+ directory=s.get("directory", ""),
52
+ updated=s.get("updated", 0),
53
+ title=s.get("title", ""),
54
+ )
55
+ for s in data
56
+ ]
57
+ except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
58
+ return []
59
+
60
+
61
+ def _get_session_messages(session_id: str, limit: int = 5) -> list[dict]:
62
+ message_dir = OPENCODE_STORAGE / "message" / session_id
63
+ if not message_dir.exists():
64
+ return []
65
+
66
+ messages: list[tuple[int, str, str]] = []
67
+ for msg_file in message_dir.glob("msg_*.json"):
68
+ try:
69
+ data = json.loads(msg_file.read_text())
70
+ created = data.get("time", {}).get("created", 0)
71
+ role = data.get("role", "unknown")
72
+ msg_id = data.get("id", "")
73
+ messages.append((created, role, msg_id))
74
+ except (json.JSONDecodeError, OSError):
75
+ continue
76
+
77
+ messages.sort(key=lambda x: x[0], reverse=True)
78
+ messages = messages[:limit]
79
+ messages.reverse()
80
+
81
+ result = []
82
+ for _, role, msg_id in messages:
83
+ part_dir = OPENCODE_STORAGE / "part" / msg_id
84
+ if not part_dir.exists():
85
+ continue
86
+ for part_file in part_dir.glob("prt_*.json"):
87
+ try:
88
+ part_data = json.loads(part_file.read_text())
89
+ if part_data.get("type") == "text":
90
+ text = part_data.get("text", "")
91
+ if text.startswith('"') and text.endswith('"\n'):
92
+ text = json.loads(text.rstrip('\n'))
93
+ result.append({"role": role, "text": text})
94
+ break
95
+ except (json.JSONDecodeError, OSError):
96
+ continue
97
+
98
+ return result
99
+
100
+
101
+ def _format_handoff_message(
102
+ session_id: str,
103
+ messages: list[dict],
104
+ project: str | None = None,
105
+ ) -> str:
106
+ lines = ["📱 **会话接力**", ""]
107
+
108
+ if project:
109
+ lines.append(f"📁 项目: `{project}`")
110
+ lines.append(f"🔗 Session: `{session_id}`")
111
+ lines.append("")
112
+ lines.append("---")
113
+ lines.append("")
114
+
115
+ for msg in messages:
116
+ role = msg.get("role", "unknown")
117
+ text = msg.get("text", "")
118
+ role_label = "👤" if role == "user" else "🤖"
119
+ if len(text) > 500:
120
+ text = text[:500] + "..."
121
+ lines.append(f"{role_label} **{role}**:")
122
+ lines.append(text)
123
+ lines.append("")
124
+
125
+ total_len = sum(len(line) for line in lines)
126
+ if total_len > 3500:
127
+ lines = lines[:20]
128
+ lines.append("... (truncated)")
129
+
130
+ lines.append("---")
131
+ lines.append("")
132
+ lines.append("💡 直接在此 Topic 发消息即可继续对话")
133
+
134
+ return "\n".join(lines)
135
+
136
+
137
+ async def _create_handoff_topic(
138
+ token: str,
139
+ chat_id: int,
140
+ session_id: str,
141
+ project: str,
142
+ config_path: Path,
143
+ ) -> int | None:
144
+ title = f"📱 {project} handoff"
145
+
146
+ client = TelegramClient(token)
147
+ try:
148
+ result = await client.create_forum_topic(chat_id, title)
149
+ if result is None:
150
+ return None
151
+
152
+ thread_id = result.message_thread_id
153
+
154
+ state_path = resolve_state_path(config_path)
155
+ store = TopicStateStore(state_path)
156
+
157
+ context = RunContext(project=project.lower(), branch=None)
158
+ await store.set_context(chat_id, thread_id, context, topic_title=title)
159
+
160
+ resume_token = ResumeToken(engine="opencode", value=session_id)
161
+ await store.set_session_resume(chat_id, thread_id, resume_token)
162
+
163
+ return thread_id
164
+ finally:
165
+ await client.close()
166
+
167
+
168
+ async def _send_to_telegram(
169
+ token: str,
170
+ chat_id: int,
171
+ message: str,
172
+ thread_id: int | None = None,
173
+ ) -> bool:
174
+ client = TelegramClient(token)
175
+ try:
176
+ result = await client.send_message(
177
+ chat_id=chat_id,
178
+ text=message,
179
+ message_thread_id=thread_id,
180
+ parse_mode="Markdown",
181
+ )
182
+ return result is not None
183
+ finally:
184
+ await client.close()
185
+
186
+
187
+ @app.command()
188
+ def send(
189
+ session: str | None = typer.Option(
190
+ None, "--session", "-s", help="Session ID (defaults to latest)"
191
+ ),
192
+ limit: int = typer.Option(
193
+ 3, "--limit", "-n", help="Number of messages to include"
194
+ ),
195
+ project: str | None = typer.Option(
196
+ None, "--project", "-p", help="Project name for context"
197
+ ),
198
+ ) -> None:
199
+ result = load_settings_if_exists()
200
+ if result is None:
201
+ typer.echo("❌ 未找到 yee88 配置文件", err=True)
202
+ raise typer.Exit(1)
203
+
204
+ settings, config_path = result
205
+ telegram_cfg = settings.transports.telegram
206
+
207
+ token = telegram_cfg.bot_token
208
+ chat_id = telegram_cfg.chat_id
209
+
210
+ if not token or not chat_id:
211
+ typer.echo("❌ Telegram 配置不完整 (需要 bot_token 和 chat_id)", err=True)
212
+ raise typer.Exit(1)
213
+
214
+ if not telegram_cfg.topics.enabled:
215
+ typer.echo("❌ Topics 未启用,请先运行: yee88 config set transports.telegram.topics.enabled true", err=True)
216
+ raise typer.Exit(1)
217
+
218
+ session_id = session
219
+ session_project = project
220
+ if session_id is None:
221
+ sessions = _get_recent_sessions(limit=10)
222
+ if not sessions:
223
+ typer.echo("❌ 未找到 OpenCode 会话", err=True)
224
+ raise typer.Exit(1)
225
+
226
+ typer.echo("\n📋 最近的会话:\n")
227
+ for i, s in enumerate(sessions[:10], 1):
228
+ title_display = s.title[:40] if s.title else s.project_name
229
+ typer.echo(f" [{i}] {s.updated_str} {title_display}")
230
+ typer.echo("")
231
+
232
+ choice = typer.prompt("选择会话 (1-10)", default="1")
233
+ try:
234
+ idx = int(choice) - 1
235
+ if idx < 0 or idx >= len(sessions):
236
+ typer.echo("❌ 无效选择", err=True)
237
+ raise typer.Exit(1)
238
+ except ValueError:
239
+ typer.echo("❌ 请输入数字", err=True)
240
+ raise typer.Exit(1)
241
+
242
+ selected = sessions[idx]
243
+ session_id = selected.id
244
+ if session_project is None:
245
+ session_project = selected.project_name
246
+
247
+ if not session_id:
248
+ typer.echo("❌ 会话 ID 为空", err=True)
249
+ raise typer.Exit(1)
250
+
251
+ typer.echo(f"📖 读取会话 {session_id[:20]}...")
252
+
253
+ messages = _get_session_messages(session_id, limit=limit)
254
+ if not messages:
255
+ typer.echo("❌ 无法读取会话消息", err=True)
256
+ raise typer.Exit(1)
257
+
258
+ typer.echo("🆕 创建新 Topic...")
259
+
260
+ async def do_handoff() -> tuple[bool, int | None]:
261
+ thread_id = await _create_handoff_topic(
262
+ token=token,
263
+ chat_id=chat_id,
264
+ session_id=session_id,
265
+ project=session_project or "unknown",
266
+ config_path=config_path,
267
+ )
268
+ if thread_id is None:
269
+ return False, None
270
+
271
+ handoff_msg = _format_handoff_message(
272
+ session_id=session_id,
273
+ messages=messages,
274
+ project=session_project,
275
+ )
276
+
277
+ success = await _send_to_telegram(
278
+ token=token,
279
+ chat_id=chat_id,
280
+ message=handoff_msg,
281
+ thread_id=thread_id,
282
+ )
283
+ return success, thread_id
284
+
285
+ success, thread_id = anyio.run(do_handoff)
286
+
287
+ if success:
288
+ typer.echo("✅ 已发送到 Telegram!")
289
+ typer.echo(f" Session: {session_id}")
290
+ typer.echo(f" Project: {session_project}")
291
+ typer.echo(f" Topic ID: {thread_id}")
292
+ typer.echo(f" 消息数: {limit}")
293
+ else:
294
+ typer.echo("❌ 发送失败", err=True)
295
+ raise typer.Exit(1)
296
+
297
+
298
+ @app.callback(invoke_without_command=True)
299
+ def main(ctx: typer.Context) -> None:
300
+ """Handoff session context to Telegram for mobile continuation."""
301
+ if ctx.invoked_subcommand is None:
302
+ ctx.invoke(send, session=None, limit=3, project=None)
@@ -2,25 +2,54 @@ import tomllib
2
2
  import tomli_w
3
3
  from pathlib import Path
4
4
  from typing import List, Optional
5
+ from zoneinfo import ZoneInfo
5
6
  from croniter import croniter
6
7
  from datetime import datetime
7
8
  from .models import CronJob
8
9
 
10
+ BEIJING_TZ = ZoneInfo("Asia/Shanghai")
11
+
9
12
 
10
13
  class CronManager:
11
- def __init__(self, config_dir: Path):
14
+ def __init__(self, config_dir: Path, timezone: str = "Asia/Shanghai"):
12
15
  self.file = config_dir / "cron.toml"
13
16
  self.jobs: List[CronJob] = []
17
+ self.timezone = ZoneInfo(timezone)
14
18
 
15
19
  def _validate_project(self, project: str) -> None:
16
20
  if not project:
17
21
  return
22
+
18
23
  path = Path(project).expanduser().resolve()
19
24
  if path.exists() and path.is_dir():
20
- git_dir = path / ".git"
21
- if git_dir.exists():
22
- return
23
- raise ValueError(f"不是 git 仓库: {project}")
25
+ return
26
+
27
+ from ..settings import load_settings_if_exists
28
+ from ..engines import list_backend_ids
29
+
30
+ result = load_settings_if_exists()
31
+ if result is None:
32
+ return
33
+
34
+ settings, config_path = result
35
+ engine_ids = list_backend_ids()
36
+ projects_config = settings.to_projects_config(
37
+ config_path=config_path,
38
+ engine_ids=engine_ids
39
+ )
40
+
41
+ if project.lower() in projects_config.projects:
42
+ return
43
+
44
+ available = list(projects_config.projects.keys())
45
+ if available:
46
+ raise ValueError(
47
+ f"未知项目: {project}。可用项目: {', '.join(available)}"
48
+ )
49
+ else:
50
+ raise ValueError(
51
+ f"未知项目: {project}。请先使用 'yee88 init' 注册项目"
52
+ )
24
53
 
25
54
  def load(self):
26
55
  if not self.file.exists():
@@ -99,7 +128,7 @@ class CronManager:
99
128
  return False
100
129
 
101
130
  def get_due_jobs(self) -> List[CronJob]:
102
- now = datetime.now()
131
+ now = datetime.now(self.timezone)
103
132
  due = []
104
133
  one_time_completed = []
105
134
 
@@ -107,30 +136,38 @@ class CronManager:
107
136
  if not job.enabled:
108
137
  continue
109
138
 
110
- # 一次性任务处理
111
139
  if job.one_time:
112
140
  try:
113
141
  exec_time = datetime.fromisoformat(job.schedule)
142
+ if exec_time.tzinfo is None:
143
+ exec_time = exec_time.replace(tzinfo=self.timezone)
114
144
  if exec_time <= now:
115
145
  due.append(job)
116
146
  one_time_completed.append(job.id)
117
147
  except Exception:
118
148
  continue
119
149
  else:
120
- # 周期性任务处理
121
150
  try:
122
- base = datetime.fromisoformat(job.last_run) if job.last_run else now
123
- itr = croniter(job.schedule, base)
124
- next_run = itr.get_next(datetime)
125
-
126
- if next_run <= now:
127
- due.append(job)
128
- job.last_run = now.isoformat()
129
- job.next_run = itr.get_next(datetime).isoformat()
151
+ if job.last_run:
152
+ base = datetime.fromisoformat(job.last_run)
153
+ if base.tzinfo is None:
154
+ base = base.replace(tzinfo=self.timezone)
155
+ itr = croniter(job.schedule, base)
156
+ next_run = itr.get_next(datetime)
157
+ if next_run <= now:
158
+ due.append(job)
159
+ job.last_run = now.isoformat()
160
+ job.next_run = itr.get_next(datetime).isoformat()
161
+ else:
162
+ itr = croniter(job.schedule, now)
163
+ prev_run = itr.get_prev(datetime)
164
+ if prev_run.date() == now.date():
165
+ due.append(job)
166
+ job.last_run = now.isoformat()
167
+ job.next_run = itr.get_next(datetime).isoformat()
130
168
  except Exception:
131
169
  continue
132
170
 
133
- # 删除已完成的一次性任务
134
171
  if one_time_completed:
135
172
  self.jobs = [j for j in self.jobs if j.id not in one_time_completed]
136
173
 
@@ -36,13 +36,38 @@ description: 当用户说"提醒我"、"X分钟/小时后"、"定时"、"每天/
36
36
 
37
37
  ## ⛔ 默认不传 --project!除非用户明确要求
38
38
 
39
- **简单规则:不知道项目别名就不传 `--project`,让 yee88 使用默认上下文。**
39
+ ### 🧠 COT:判断是否需要 --project
40
40
 
41
- | 场景 | 做法 |
42
- |------|------|
43
- | 用户只说"1分钟后提醒我..." | `yee88 cron add reminder "+1m" "..." -o` (不传 --project) |
44
- | 用户说"在 takopi 项目提醒我..." | `yee88 cron add reminder "+1m" "..." --project takopi -o` |
45
- | 不确定项目别名 | **不传 --project** |
41
+ **收到定时任务请求时,先在脑中过一遍这个决策流程:**
42
+
43
+ ```
44
+ 1. 用户有没有提到具体项目名?
45
+ - "在 takopi 项目下..." → 需要 --project
46
+ - "帮我在 work 项目..." → 需要 --project
47
+ - "提醒我喝水" → 不需要 --project
48
+ - "每天9点提醒站会" → 不需要 --project
49
+
50
+ 2. 用户有没有说"在某个项目下运行"?
51
+ - "在 xxx 下执行..." → 需要 --project
52
+ - "切到 xxx 项目..." → 需要 --project
53
+ - 没有提到项目上下文 → 不需要 --project
54
+
55
+ 3. 这个任务是通用提醒还是项目相关?
56
+ - 喝水、休息、开会 → 通用,不需要 --project
57
+ - 代码审查、部署、PR → 可能需要,但除非用户说了项目名,否则不传
58
+ ```
59
+
60
+ **结论:99% 的情况不需要 --project。只有用户明确说了项目名才传。**
61
+
62
+ ### 决策表
63
+
64
+ | 用户说的话 | 需要 --project? | 命令 |
65
+ |-----------|----------------|------|
66
+ | "1分钟后提醒我喝水" | ❌ 不需要 | `yee88 cron add reminder "+1m" "喝水" -o` |
67
+ | "每天9点提醒我站会" | ❌ 不需要 | `yee88 cron add standup "0 9 * * *" "站会时间"` |
68
+ | "30分钟后提醒我休息" | ❌ 不需要 | `yee88 cron add break "+30m" "休息一下" -o` |
69
+ | "在 takopi 项目下每天9点跑测试" | ✅ 需要 | `yee88 cron add test "0 9 * * *" "跑测试" --project takopi` |
70
+ | "帮我在 work 项目设个提醒" | ✅ 需要 | `yee88 cron add reminder "+1h" "..." --project work -o` |
46
71
 
47
72
  **⚠️ --project 只接受项目别名,不是路径!**
48
73
 
@@ -29,7 +29,7 @@ MODEL_SELECT_CALLBACK_PREFIX = "yee88:model_select:"
29
29
 
30
30
  MODEL_USAGE = (
31
31
  "usage: `/model`, `/model status`, `/model set <model>`, "
32
- "`/model set <engine> <model>`, or `/model clear [engine]`"
32
+ "`/model set <engine> <model>`, `/model clear [engine]`, or `/model reset`"
33
33
  )
34
34
 
35
35
 
@@ -339,4 +339,45 @@ async def _handle_model_command(
339
339
  await reply(text="chat model override cleared.")
340
340
  return
341
341
 
342
+ if action == "reset":
343
+ if len(tokens) > 1:
344
+ await reply(text=MODEL_USAGE)
345
+ return
346
+ if not await require_admin_or_private(
347
+ cfg,
348
+ msg,
349
+ missing_sender="cannot verify sender for model overrides.",
350
+ failed_member="failed to verify model override permissions.",
351
+ denied="changing model overrides is restricted to group admins.",
352
+ ):
353
+ return
354
+
355
+ cleared_engines = []
356
+ for engine in engine_ids:
357
+ scope = await apply_engine_override(
358
+ reply=reply,
359
+ tkey=tkey,
360
+ topic_store=topic_store,
361
+ chat_prefs=chat_prefs,
362
+ chat_id=msg.chat_id,
363
+ engine=engine,
364
+ update=lambda current: EngineOverrides(
365
+ model=None,
366
+ reasoning=current.reasoning if current is not None else None,
367
+ ),
368
+ topic_unavailable="topic model overrides are unavailable.",
369
+ chat_unavailable="chat model overrides are unavailable (no config path).",
370
+ )
371
+ if scope is not None:
372
+ cleared_engines.append(engine)
373
+
374
+ if cleared_engines:
375
+ engines_list = ", ".join(cleared_engines)
376
+ await reply(
377
+ text=f"all model overrides reset to default for engines: {engines_list}"
378
+ )
379
+ else:
380
+ await reply(text="no model overrides to reset.")
381
+ return
382
+
342
383
  await reply(text=MODEL_USAGE)
@@ -1077,7 +1077,8 @@ async def run_main_loop(
1077
1077
  tg.start_soon(run_config_watch)
1078
1078
 
1079
1079
  if config_path is not None:
1080
- cron_manager = CronManager(config_path.parent)
1080
+ cron_manager = CronManager(config_path.parent, timezone="Asia/Shanghai")
1081
+ logger.info("cron.manager.initialized", timezone="Asia/Shanghai")
1081
1082
 
1082
1083
  async def _execute_cron_job(job: CronJob) -> None:
1083
1084
  try: