tsugite-cli 0.23.1__tar.gz → 0.24.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 (184) hide show
  1. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/PKG-INFO +1 -1
  2. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/pyproject.toml +1 -1
  3. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_preparation.py +6 -4
  4. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/history_integration.py +30 -25
  5. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/runner.py +10 -1
  6. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/steps.py +1 -0
  7. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/agent_config.py +14 -14
  8. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/scheduling/SKILL.md +2 -0
  9. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/agents.py +2 -1
  10. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/daemon.py +64 -83
  11. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/history.py +2 -2
  12. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/run.py +2 -1
  13. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/agent.py +20 -6
  14. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/executor.py +9 -0
  15. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/subprocess_executor.py +12 -0
  16. tsugite_cli-0.24.0/tsugite/daemon_client.py +201 -0
  17. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/helpers.py +14 -3
  18. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/models.py +1 -1
  19. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/sqlite_backend.py +17 -7
  20. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/permissions.py +31 -3
  21. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/anthropic.py +6 -3
  22. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/model_registry.py +8 -4
  23. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/openai_compat.py +5 -1
  24. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/skill_discovery.py +6 -0
  25. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/__init__.py +15 -0
  26. tsugite_cli-0.24.0/tsugite/tools/daemon_control.py +107 -0
  27. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/http.py +27 -2
  28. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/jobs.py +21 -2
  29. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/skills.py +19 -13
  30. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/jsonl.py +14 -1
  31. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/usage/quota.py +8 -10
  32. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/utils.py +1 -0
  33. tsugite_cli-0.23.1/tsugite/tools/daemon_control.py +0 -60
  34. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/.gitignore +0 -0
  35. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/AGENTS.md +0 -0
  36. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/CONTRIBUTING.md +0 -0
  37. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/LICENSE +0 -0
  38. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/README.md +0 -0
  39. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/__init__.py +0 -0
  40. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_inheritance.py +0 -0
  41. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/__init__.py +0 -0
  42. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_directives.py +0 -0
  43. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_runner.py +0 -0
  44. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/helpers.py +0 -0
  45. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/metrics.py +0 -0
  46. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/models.py +0 -0
  47. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_runner/validation.py +0 -0
  48. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/agent_utils.py +0 -0
  49. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/approval.py +0 -0
  50. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/__init__.py +0 -0
  51. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/auto_context.py +0 -0
  52. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/base.py +0 -0
  53. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/delegation.py +0 -0
  54. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/file.py +0 -0
  55. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/inline.py +0 -0
  56. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/storage.py +0 -0
  57. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/attachments/url.py +0 -0
  58. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/.gitkeep +0 -0
  59. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/code_searcher.md +0 -0
  60. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/default.md +0 -0
  61. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/file_searcher.md +0 -0
  62. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_verifier.md +0 -0
  63. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_worker.md +0 -0
  64. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_agents/onboard.md +0 -0
  65. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_context.py +0 -0
  66. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/.gitkeep +0 -0
  67. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  68. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  69. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  70. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  71. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  72. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  73. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  74. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  75. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cache.py +0 -0
  76. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cancellation.py +0 -0
  77. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/__init__.py +0 -0
  78. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/attachments.py +0 -0
  79. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/cache.py +0 -0
  80. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/chat.py +0 -0
  81. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/config.py +0 -0
  82. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/exec.py +0 -0
  83. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/helpers.py +0 -0
  84. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/init.py +0 -0
  85. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/models.py +0 -0
  86. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/plugins.py +0 -0
  87. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/render.py +0 -0
  88. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/secrets.py +0 -0
  89. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/skills.py +0 -0
  90. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/tools.py +0 -0
  91. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/usage.py +0 -0
  92. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/validate.py +0 -0
  93. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/cli/workspace.py +0 -0
  94. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/config.py +0 -0
  95. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/console.py +0 -0
  96. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/constants.py +0 -0
  97. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/context.py +0 -0
  98. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/context_block.py +0 -0
  99. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/__init__.py +0 -0
  100. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/content_blocks.py +0 -0
  101. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/executor_registry.py +0 -0
  102. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/memory.py +0 -0
  103. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/proxy.py +0 -0
  104. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/record_store.py +0 -0
  105. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/sandbox.py +0 -0
  106. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/state.py +0 -0
  107. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/core/tools.py +0 -0
  108. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/__init__.py +0 -0
  109. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/base.py +0 -0
  110. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/bus.py +0 -0
  111. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/events/events.py +0 -0
  112. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/exceptions.py +0 -0
  113. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/__init__.py +0 -0
  114. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/base.py +0 -0
  115. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/reconstruction.py +0 -0
  116. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/registry.py +0 -0
  117. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/sqlite_conn.py +0 -0
  118. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/sqlite_schema.py +0 -0
  119. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/history/ui_events.py +0 -0
  120. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/hooks.py +0 -0
  121. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/interaction.py +0 -0
  122. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/md_agents.py +0 -0
  123. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/models.py +0 -0
  124. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/options.py +0 -0
  125. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/plugins.py +0 -0
  126. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/prompt_xml.py +0 -0
  127. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/__init__.py +0 -0
  128. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/base.py +0 -0
  129. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/model_cache.py +0 -0
  130. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/ollama.py +0 -0
  131. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/providers/openrouter.py +0 -0
  132. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/py.typed +0 -0
  133. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/renderer.py +0 -0
  134. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/schemas/__init__.py +0 -0
  135. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/schemas/agent.schema.json +0 -0
  136. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/__init__.py +0 -0
  137. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/backend.py +0 -0
  138. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/env.py +0 -0
  139. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/exec.py +0 -0
  140. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/file.py +0 -0
  141. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/masking.py +0 -0
  142. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/redaction.py +0 -0
  143. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/registry.py +0 -0
  144. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/secrets/sqlite.py +0 -0
  145. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/shell_tool_config.py +0 -0
  146. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/AGENTS.md +0 -0
  147. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/IDENTITY.md +0 -0
  148. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/MEMORY.md +0 -0
  149. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/USER.md +0 -0
  150. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/personas/casual-technical.md +0 -0
  151. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/personas/marvin.md +0 -0
  152. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/templates/personas/minimal.md +0 -0
  153. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/agents.py +0 -0
  154. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/artifacts.py +0 -0
  155. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/fs.py +0 -0
  156. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/history.py +0 -0
  157. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/interactive.py +0 -0
  158. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/notify.py +0 -0
  159. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/schedule.py +0 -0
  160. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/secrets.py +0 -0
  161. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/sessions.py +0 -0
  162. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/shell.py +0 -0
  163. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/shell_tools.py +0 -0
  164. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tools/time.py +0 -0
  165. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/tsugite.py +0 -0
  166. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/__init__.py +0 -0
  167. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/base.py +0 -0
  168. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/chat.py +0 -0
  169. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/dispatch.py +0 -0
  170. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/helpers.py +0 -0
  171. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/plain.py +0 -0
  172. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_chat.py +0 -0
  173. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_commands.py +0 -0
  174. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_completer.py +0 -0
  175. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui/repl_handler.py +0 -0
  176. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui_context.py +0 -0
  177. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/ui_surfaces.py +0 -0
  178. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/usage/__init__.py +0 -0
  179. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/usage/store.py +0 -0
  180. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/user_agent.py +0 -0
  181. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/workspace/__init__.py +0 -0
  182. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/workspace/models.py +0 -0
  183. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/workspace/session.py +0 -0
  184. {tsugite_cli-0.23.1 → tsugite_cli-0.24.0}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tsugite-cli
3
- Version: 0.23.1
3
+ Version: 0.24.0
4
4
  Summary: Micro-agent runner for task automation using markdown definitions
5
5
  Author: Justyn Shull
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.23.1"
3
+ version = "0.24.0"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -152,7 +152,8 @@ class AgentPreparer:
152
152
 
153
153
  A front-matter attachment defines its cache tier and replaces a same-named
154
154
  caller attachment. Legacy `-filename` string entries drop a same-named
155
- entry. Front-matter paths render with `paths` and resolve against its CWD.
155
+ entry. Front-matter paths render with `paths` and resolve against the
156
+ workspace, or the CWD when the run has no workspace.
156
157
  """
157
158
  all_attachments = list(attachments or [])
158
159
 
@@ -160,7 +161,8 @@ class AgentPreparer:
160
161
  if removals:
161
162
  all_attachments = [a for a in all_attachments if a.name not in removals]
162
163
 
163
- loaded, bindings = resolve_agent_config_attachments(keep_items, Path(paths["CWD"]), paths)
164
+ base = paths["WORKSPACE_DIR"] or paths["CWD"]
165
+ loaded, bindings = resolve_agent_config_attachments(keep_items, Path(base), paths)
164
166
  all_attachments = loaded + all_attachments
165
167
 
166
168
  seen_names: set[str] = set()
@@ -240,7 +242,7 @@ class AgentPreparer:
240
242
  auto_load_skills = [s for s in (agent_config.auto_load_skills or []) if s not in suppressed_skills]
241
243
 
242
244
  for skill_name in auto_load_skills:
243
- result = skill_manager.load_skill(skill_name)
245
+ result = skill_manager.load_skill(skill_name, explicit=True)
244
246
  if result.startswith("Failed") or result.startswith("Skill '"):
245
247
  if event_bus:
246
248
  event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
@@ -266,7 +268,7 @@ class AgentPreparer:
266
268
  if name in auto_load_skills:
267
269
  # Already loaded above; no need to double-load but it's still sticky.
268
270
  continue
269
- result = skill_manager.load_skill(name)
271
+ result = skill_manager.load_skill(name, explicit=True)
270
272
  if result.startswith("Failed") or result.startswith("Skill '"):
271
273
  if event_bus:
272
274
  event_bus.emit(SkillLoadFailedEvent(skill_name=name, error_message=result))
@@ -73,7 +73,11 @@ def open_or_create_session(
73
73
  backend = get_history_backend()
74
74
  if continue_conversation_id:
75
75
  if backend.exists(continue_conversation_id):
76
- return backend.load(continue_conversation_id)
76
+ session = backend.load(continue_conversation_id)
77
+ # Daemon-spawned sessions are created without a model.
78
+ if session.summary().model is None:
79
+ session.record("session_start", agent=agent_name, model=model, workspace=workspace)
80
+ return session
77
81
  return backend.create(
78
82
  agent_name=agent_name,
79
83
  model=model,
@@ -92,14 +96,11 @@ def _current_turn_already_has_user_input(storage: Session) -> bool:
92
96
  session_end after it; the post-hoc save_run_to_history on the error path
93
97
  would otherwise record a second, identical one (a duplicate user bubble).
94
98
  A genuinely repeated message opens a new turn *after* the prior session_end,
95
- so this stays False for it.
99
+ so this stays False for it. A final_result also ends a turn copied without
100
+ its session_end (branches, older compacted sessions).
96
101
  """
97
- for event in reversed(list(storage.iter_events(types=("user_input", "session_end")))):
98
- if event.type == "session_end":
99
- return False
100
- if event.type == "user_input":
101
- return True
102
- return False
102
+ events = list(storage.iter_events(types=("user_input", "session_end", "final_result")))
103
+ return bool(events) and events[-1].type == "user_input"
103
104
 
104
105
 
105
106
  def record_user_input(
@@ -132,8 +133,9 @@ def record_user_input(
132
133
  data["attachments"] = [
133
134
  {"name": a.name, "type": a.content_type.value, "source_url": a.source_url} for a in uploads
134
135
  ]
135
- if channel_metadata:
136
- data["channel"] = channel_metadata
136
+ channel = {k: v for k, v in (channel_metadata or {}).items() if k != "tsugite_token"}
137
+ if channel:
138
+ data["channel"] = channel
137
139
  # Structured client context, recorded alongside the folded <client_context> XML
138
140
  # in `text` so the UI reads it back without re-parsing the prompt string.
139
141
  if client_context_items:
@@ -247,8 +249,8 @@ def save_run_to_history(
247
249
  agent_already_recorded = last_model_response > last_session_start
248
250
 
249
251
  if agent_already_recorded:
250
- # A continued conversation pins session_start at index 0, so the current
251
- # turn starts at its own user_input.
252
+ # A continued conversation's session_start can sit at index 0 or come from
253
+ # a backfill. The current turn starts at its own user_input.
252
254
  turn_start = max(last_session_start, last_user_input)
253
255
  if last_session_end < turn_start:
254
256
  record_session_end(storage, status=status, error_message=error_message)
@@ -267,19 +269,20 @@ def save_run_to_history(
267
269
  tools_called=list(getattr(step, "tools_called", []) or []),
268
270
  )
269
271
 
270
- state_delta = provider_state if provider_state else None
271
- # Split as the agent loop records it: bare id in `model`, prefix in `provider`.
272
- model_provider, _, model_id = model.partition(":")
273
- storage.record(
274
- "model_response",
275
- provider=model_provider if model_id else None,
276
- model=model_id or model,
277
- raw_content=result or "",
278
- usage={"total_tokens": token_count} if token_count else None,
279
- cost=cost,
280
- duration_ms=duration_ms,
281
- state_delta=state_delta,
282
- )
272
+ if status == "success":
273
+ state_delta = provider_state if provider_state else None
274
+ # Split as the agent loop records it: bare id in `model`, prefix in `provider`.
275
+ model_provider, _, model_id = model.partition(":")
276
+ storage.record(
277
+ "model_response",
278
+ provider=model_provider if model_id else None,
279
+ model=model_id or model,
280
+ raw_content=result or "",
281
+ usage={"total_tokens": token_count} if token_count else None,
282
+ cost=cost,
283
+ duration_ms=duration_ms,
284
+ state_delta=state_delta,
285
+ )
283
286
 
284
287
  record_session_end(storage, status=status, error_message=error_message)
285
288
  return storage.session_id
@@ -291,6 +294,7 @@ class ResumableSessionState:
291
294
 
292
295
  session_id: str
293
296
  compacted: bool = False
297
+ cumulative_cost: float = 0.0
294
298
 
295
299
 
296
300
  def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessionState]:
@@ -327,6 +331,7 @@ def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessi
327
331
  return ResumableSessionState(
328
332
  session_id=session_id,
329
333
  compacted=bool(state.get("compacted", False)),
334
+ cumulative_cost=state.get("cumulative_cost") or 0.0,
330
335
  )
331
336
  return None
332
337
  except Exception:
@@ -395,6 +395,7 @@ async def _execute_agent_with_prompt(
395
395
  path_context: Optional[Any] = None,
396
396
  resume_session: Optional[str] = None,
397
397
  resume_after_compaction: bool = False,
398
+ resume_cost_baseline: float = 0.0,
398
399
  hook_vars: Optional[Dict[str, str]] = None,
399
400
  continue_conversation_id: Optional[str] = None,
400
401
  user_input_for_history: Optional[str] = None,
@@ -610,9 +611,11 @@ async def _execute_agent_with_prompt(
610
611
  previous_messages=previous_messages,
611
612
  resume_session=resume_session,
612
613
  resume_after_compaction=resume_after_compaction,
614
+ resume_cost_baseline=resume_cost_baseline,
613
615
  hook_vars=hook_vars,
614
616
  storage=session_storage,
615
617
  pre_llm_call=pre_llm_call_cb,
618
+ expects_action=(channel_metadata or {}).get("source") == "scheduler",
616
619
  )
617
620
  # Set event_bus in context so tools can access it during execution
618
621
  _setup_event_context(event_bus)
@@ -867,6 +870,7 @@ class _RunSetup:
867
870
  previous_messages: List[Dict] = field(default_factory=list)
868
871
  resume_session: Optional[str] = None
869
872
  resume_after_compaction: bool = False
873
+ resume_cost_baseline: float = 0.0
870
874
  conversation_id: Optional[str] = None
871
875
 
872
876
 
@@ -958,6 +962,7 @@ async def _run_unit(
958
962
  previous_messages=setup.previous_messages,
959
963
  resume_session=setup.resume_session,
960
964
  resume_after_compaction=setup.resume_after_compaction,
965
+ resume_cost_baseline=setup.resume_cost_baseline,
961
966
  **execute_kwargs,
962
967
  )
963
968
  except (RuntimeError, AgentExecutionError) as e:
@@ -1151,6 +1156,7 @@ async def run_agent_async(
1151
1156
  previous_messages = []
1152
1157
  resume_session = None
1153
1158
  resume_after_compaction = False
1159
+ resume_cost_baseline = 0.0
1154
1160
  if continue_conversation_id:
1155
1161
  from tsugite.agent_runner.history_integration import (
1156
1162
  get_resumable_session_state,
@@ -1162,6 +1168,7 @@ async def run_agent_async(
1162
1168
  if session_info:
1163
1169
  resume_session = session_info.session_id
1164
1170
  resume_after_compaction = session_info.compacted
1171
+ resume_cost_baseline = session_info.cumulative_cost
1165
1172
  logger.info(
1166
1173
  "Resuming provider session %s (compacted=%s)",
1167
1174
  resume_session,
@@ -1210,6 +1217,7 @@ async def run_agent_async(
1210
1217
  previous_messages=previous_messages,
1211
1218
  resume_session=resume_session,
1212
1219
  resume_after_compaction=resume_after_compaction,
1220
+ resume_cost_baseline=resume_cost_baseline,
1213
1221
  conversation_id=continue_conversation_id,
1214
1222
  )
1215
1223
 
@@ -1281,6 +1289,7 @@ def preview_multistep_agent(
1281
1289
 
1282
1290
  # Extract steps
1283
1291
  from tsugite.md_agents import extract_step_directives, has_step_directives
1292
+ from tsugite.tools import format_tool_specs
1284
1293
 
1285
1294
  if not has_step_directives(agent.content):
1286
1295
  output("[yellow]This is a single-step agent (no step directives).[/yellow]", is_warning=True)
@@ -1302,7 +1311,7 @@ def preview_multistep_agent(
1302
1311
  output(f"Prompt: {prompt}")
1303
1312
  output(f"Steps: {len(steps)}")
1304
1313
  output(f"Model: {resolve_effective_model(agent_model=agent.config.model) or 'unknown'}")
1305
- output(f"Tools: {', '.join(agent.config.tools) if agent.config.tools else 'None'}")
1314
+ output(f"Tools: {format_tool_specs(agent.config.tools) if agent.config.tools else 'None'}")
1306
1315
  output("")
1307
1316
 
1308
1317
  # Show steps in table format
@@ -251,6 +251,7 @@ async def _execute_step_with_retries(
251
251
  user_input_for_history=f"[Step {i}/{total_steps}: {step.name}]",
252
252
  resume_session=None,
253
253
  resume_after_compaction=False,
254
+ resume_cost_baseline=0.0,
254
255
  )
255
256
  errors = []
256
257
  step_start_time = time.time()
@@ -17,6 +17,12 @@ from tsugite.utils import has_glob_chars
17
17
  logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
+ def _warn_skipped(subject: str, detail: str) -> None:
21
+ from tsugite.events.helpers import emit_warning_event
22
+
23
+ emit_warning_event(f"Agent attachment skipped: {subject} ({detail})")
24
+
25
+
20
26
  def _render_path(template: str, template_vars: Dict[str, Any]) -> Optional[str]:
21
27
  """Render a Jinja path template; return None on render failure."""
22
28
  from tsugite.renderer import AgentRenderer
@@ -24,7 +30,7 @@ def _render_path(template: str, template_vars: Dict[str, Any]) -> Optional[str]:
24
30
  try:
25
31
  return AgentRenderer().render_string(template, template_vars)
26
32
  except Exception as e:
27
- logger.debug("Failed to render attachment path %r: %s", template, e)
33
+ _warn_skipped(template, f"path template failed to render: {e}")
28
34
  return None
29
35
 
30
36
 
@@ -124,12 +130,10 @@ def _resolve_string_item(
124
130
  return
125
131
  resolved = _resolve_path(rendered, workspace_path)
126
132
  if not resolved.exists():
127
- logger.debug("Agent attachment not found (skipped): %s", resolved)
133
+ _warn_skipped(template, f"not found: {resolved}")
128
134
  return
129
- att = file_handler.fetch(str(resolved))
130
- if att:
131
- attachments.append(att)
132
- logger.debug("Loaded agent attachment: %s", resolved)
135
+ attachments.append(file_handler.fetch(str(resolved)))
136
+ logger.debug("Loaded agent attachment: %s", resolved)
133
137
 
134
138
 
135
139
  def _resolve_spec(
@@ -161,13 +165,12 @@ def _resolve_spec(
161
165
  if not spec.attach and spec.assign:
162
166
  try:
163
167
  content = path.read_text(encoding="utf-8")
164
- except (OSError, UnicodeDecodeError):
168
+ except (OSError, UnicodeDecodeError) as e:
169
+ _warn_skipped(str(path), f"unreadable: {e}")
165
170
  continue
166
171
  bindings[spec.assign].append({"path": str(path), "content": content})
167
172
  continue
168
173
  att = file_handler.fetch(str(path))
169
- if not att:
170
- continue
171
174
  if spec.attach:
172
175
  attachments.append(att)
173
176
  if spec.assign and att.content_type == AttachmentContentType.TEXT:
@@ -178,14 +181,11 @@ def _resolve_spec(
178
181
  if not resolved.exists():
179
182
  if spec.assign:
180
183
  bindings[spec.assign] = None
181
- logger.debug("Agent attachment not found (skipped): %s", resolved)
184
+ else:
185
+ _warn_skipped(spec.path, f"not found: {resolved}")
182
186
  return
183
187
 
184
188
  att = file_handler.fetch(str(resolved))
185
- if not att:
186
- if spec.assign:
187
- bindings[spec.assign] = None
188
- return
189
189
  if spec.attach:
190
190
  attachments.append(att)
191
191
  if spec.assign:
@@ -178,6 +178,8 @@ print(schedules)
178
178
 
179
179
  Each schedule includes: `id`, `execution_type`, `schedule_type`, `enabled`, `next_run`, `last_run`, `last_status`, `last_error`.
180
180
 
181
+ `last_status` is `success`, `error`, `skipped`, or `no_action`.
182
+
181
183
  ## Managing Schedules
182
184
 
183
185
  ```python-exec
@@ -75,6 +75,7 @@ def agents_show(
75
75
  """
76
76
  from tsugite.agent_inheritance import find_agent_file
77
77
  from tsugite.md_agents import parse_agent_file
78
+ from tsugite.tools import format_tool_spec
78
79
 
79
80
  try:
80
81
  agent_file = Path(agent_path)
@@ -113,7 +114,7 @@ def agents_show(
113
114
  if config.tools:
114
115
  console.print(f"\n[bold]Tools ({len(config.tools)}):[/bold]")
115
116
  for tool in config.tools:
116
- console.print(f" • {tool}")
117
+ console.print(f" • {format_tool_spec(tool)}")
117
118
 
118
119
  if config.extends:
119
120
  console.print(f"\n[bold]Extends:[/bold] {config.extends}")
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import re
6
6
  import sys
7
+ from contextlib import contextmanager
7
8
  from pathlib import Path
8
9
  from typing import Optional
9
10
 
@@ -11,15 +12,14 @@ import typer
11
12
  from rich.console import Console
12
13
  from rich.panel import Panel
13
14
 
14
- from tsugite.constants import DEFAULT_DAEMON_HOST, DEFAULT_DAEMON_PORT
15
+ from tsugite.daemon_client import DaemonClient, DaemonError
15
16
 
16
17
  daemon_app = typer.Typer(help="Daemon management commands")
17
18
  console = Console()
18
19
 
19
- # Connection flags shared by every daemon client command. Typer resolves
20
- # flag > env var > default natively, so there is nothing to hand-roll here.
21
- HOST_OPTION = typer.Option(DEFAULT_DAEMON_HOST, "--host", envvar="TSUGITE_DAEMON_HOST", help="Daemon HTTP host")
22
- PORT_OPTION = typer.Option(DEFAULT_DAEMON_PORT, "--port", "-p", envvar="TSUGITE_DAEMON_PORT", help="Daemon HTTP port")
20
+ # Precedence: flag > TSUGITE_DAEMON_HOST/PORT > TSUGITE_DAEMON_URL > default.
21
+ HOST_OPTION = typer.Option(None, "--host", envvar="TSUGITE_DAEMON_HOST", help="Daemon HTTP host")
22
+ PORT_OPTION = typer.Option(None, "--port", "-p", envvar="TSUGITE_DAEMON_PORT", help="Daemon HTTP port")
23
23
  TOKEN_OPTION = typer.Option(None, "--token", "-t", envvar="TSUGITE_DAEMON_TOKEN", help="Auth token")
24
24
 
25
25
 
@@ -363,43 +363,25 @@ def init_daemon(
363
363
  _show_next_steps(token_file, bot_name)
364
364
 
365
365
 
366
- def _daemon_request(method: str, host: str, port: int, path: str, token: Optional[str] = None, **kwargs):
367
- """Make an HTTP request to the daemon, handling connection errors and non-2xx responses.
368
-
369
- Returns the parsed JSON response body on success, or exits on failure.
370
- """
371
- import httpx
372
-
373
- url = f"http://{host}:{port}{path}"
374
- headers = {"Authorization": f"Bearer {token}"} if token else {}
375
-
366
+ @contextmanager
367
+ def _daemon_errors():
368
+ """Print a daemon failure and exit 1."""
376
369
  try:
377
- resp = httpx.request(method, url, headers=headers, timeout=kwargs.pop("timeout", 10), **kwargs)
378
- except httpx.ConnectError:
379
- console.print(f"[red]Could not connect to daemon at {host}:{port}[/red]")
380
- raise typer.Exit(1)
381
-
382
- if resp.status_code >= 400:
383
- try:
384
- msg = resp.json().get("error", resp.text)
385
- except Exception:
386
- msg = resp.text
387
- console.print(f"[red]Error ({resp.status_code}):[/red] {msg}")
370
+ yield
371
+ except DaemonError as e:
372
+ console.print(f"[red]Error ({e.status}):[/red] {e}" if e.status else f"[red]{e}[/red]")
388
373
  raise typer.Exit(1)
389
374
 
390
- return resp.json()
391
-
392
375
 
393
376
  @daemon_app.command("sessions")
394
377
  def list_sessions(
395
- host: str = HOST_OPTION,
396
- port: int = PORT_OPTION,
378
+ host: Optional[str] = HOST_OPTION,
379
+ port: Optional[int] = PORT_OPTION,
397
380
  token: Optional[str] = TOKEN_OPTION,
398
381
  ):
399
382
  """List active daemon sessions."""
400
- data = _daemon_request("GET", host, port, "/api/chat/sessions", token)
401
-
402
- sessions = data.get("sessions", [])
383
+ with _daemon_errors():
384
+ sessions = DaemonClient(host, port, token).list_chat_sessions()
403
385
  if not sessions:
404
386
  console.print("No active sessions")
405
387
  return
@@ -418,20 +400,13 @@ def list_sessions(
418
400
  @daemon_app.command("compact")
419
401
  def compact_session(
420
402
  user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID to compact"),
421
- host: str = HOST_OPTION,
422
- port: int = PORT_OPTION,
403
+ host: Optional[str] = HOST_OPTION,
404
+ port: Optional[int] = PORT_OPTION,
423
405
  token: Optional[str] = TOKEN_OPTION,
424
406
  ):
425
407
  """Force compact a daemon session."""
426
- data = _daemon_request(
427
- "POST",
428
- host,
429
- port,
430
- "/api/chat/compact",
431
- token,
432
- json={"user_id": user_id},
433
- timeout=120,
434
- )
408
+ with _daemon_errors():
409
+ data = DaemonClient(host, port, token).compact_chat(user_id)
435
410
  console.print(f"[green]✓[/green] Session compacted for user [cyan]{user_id}[/cyan]")
436
411
  console.print(f" old: {data['old_conversation_id']}")
437
412
  console.print(f" new: {data['new_conversation_id']}")
@@ -448,41 +423,40 @@ daemon_app.add_typer(session_app, name="session")
448
423
  @session_app.command("set-primary")
449
424
  def session_set_primary(
450
425
  session_id: str = typer.Argument(help="Session ID to mark as primary"),
451
- host: str = HOST_OPTION,
452
- port: int = PORT_OPTION,
426
+ host: Optional[str] = HOST_OPTION,
427
+ port: Optional[int] = PORT_OPTION,
453
428
  token: Optional[str] = TOKEN_OPTION,
454
429
  ):
455
430
  """Mark a session as the user's primary for its agent."""
456
- _daemon_request("POST", host, port, f"/api/sessions/{session_id}/set-primary", token, json={})
431
+ with _daemon_errors():
432
+ DaemonClient(host, port, token).set_primary_session(session_id)
457
433
  console.print(f"[green]✓[/green] Session [cyan]{session_id}[/cyan] is now primary")
458
434
 
459
435
 
460
436
  @session_app.command("clear-primary")
461
437
  def session_clear_primary(
462
438
  user_id: str = typer.Option("web-anonymous", "--user", "-u", help="User ID"),
463
- host: str = HOST_OPTION,
464
- port: int = PORT_OPTION,
439
+ host: Optional[str] = HOST_OPTION,
440
+ port: Optional[int] = PORT_OPTION,
465
441
  token: Optional[str] = TOKEN_OPTION,
466
442
  ):
467
443
  """Remove the primary flag from any of the user's sessions."""
468
- from urllib.parse import urlencode
469
-
470
- path = f"/api/sessions/clear-primary?{urlencode({'user_id': user_id})}"
471
- _daemon_request("POST", host, port, path, token)
444
+ with _daemon_errors():
445
+ DaemonClient(host, port, token).clear_primary_session(user_id)
472
446
  console.print(f"[green]✓[/green] Cleared primary for user [cyan]{user_id}[/cyan]")
473
447
 
474
448
 
475
449
  @session_app.command("list")
476
450
  def session_list(
477
- host: str = HOST_OPTION,
478
- port: int = PORT_OPTION,
451
+ host: Optional[str] = HOST_OPTION,
452
+ port: Optional[int] = PORT_OPTION,
479
453
  token: Optional[str] = TOKEN_OPTION,
480
454
  ):
481
455
  """List all daemon sessions, indicating which one is primary."""
482
456
  from rich.table import Table
483
457
 
484
- data = _daemon_request("GET", host, port, "/api/sessions", token)
485
- sessions = data.get("sessions", [])
458
+ with _daemon_errors():
459
+ sessions = DaemonClient(host, port, token).list_sessions()
486
460
  if not sessions:
487
461
  console.print("No sessions")
488
462
  return
@@ -520,15 +494,15 @@ def _parse_every_to_cron(every: str) -> str:
520
494
 
521
495
  @schedule_app.command("list")
522
496
  def schedule_list(
523
- host: str = HOST_OPTION,
524
- port: int = PORT_OPTION,
497
+ host: Optional[str] = HOST_OPTION,
498
+ port: Optional[int] = PORT_OPTION,
525
499
  token: Optional[str] = TOKEN_OPTION,
526
500
  ):
527
501
  """List all schedules."""
528
502
  from rich.table import Table
529
503
 
530
- data = _daemon_request("GET", host, port, "/api/schedules", token)
531
- schedules = data.get("schedules", [])
504
+ with _daemon_errors():
505
+ schedules = DaemonClient(host, port, token).list_schedules()
532
506
  if not schedules:
533
507
  console.print("No schedules configured")
534
508
  return
@@ -545,7 +519,7 @@ def schedule_list(
545
519
  sched_str = s.get("cron_expr") or s.get("run_at") or ""
546
520
  enabled = "[green]yes[/green]" if s.get("enabled") else "[red]no[/red]"
547
521
  status = s.get("last_status") or "-"
548
- if status == "error":
522
+ if status in ("error", "no_action"):
549
523
  status = f"[red]{status}[/red]"
550
524
  elif status == "success":
551
525
  status = f"[green]{status}[/green]"
@@ -572,8 +546,8 @@ def schedule_add(
572
546
  "--target-session",
573
547
  help="Where the inject_history turn lands: primary, originating, none, name:<n>, or <session_id>",
574
548
  ),
575
- host: str = HOST_OPTION,
576
- port: int = PORT_OPTION,
549
+ host: Optional[str] = HOST_OPTION,
550
+ port: Optional[int] = PORT_OPTION,
577
551
  token: Optional[str] = TOKEN_OPTION,
578
552
  ):
579
553
  """Add a new schedule."""
@@ -601,7 +575,8 @@ def schedule_add(
601
575
  if target_session:
602
576
  body["target_session"] = target_session
603
577
 
604
- data = _daemon_request("POST", host, port, "/api/schedules", token, json=body)
578
+ with _daemon_errors():
579
+ data = DaemonClient(host, port, token).create_schedule(**body)
605
580
  console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] created")
606
581
  if data.get("next_run"):
607
582
  console.print(f" next run: {data['next_run']}")
@@ -609,12 +584,13 @@ def schedule_add(
609
584
 
610
585
  @schedule_app.command("cleanup")
611
586
  def schedule_cleanup(
612
- host: str = HOST_OPTION,
613
- port: int = PORT_OPTION,
587
+ host: Optional[str] = HOST_OPTION,
588
+ port: Optional[int] = PORT_OPTION,
614
589
  token: Optional[str] = TOKEN_OPTION,
615
590
  ):
616
591
  """Remove all orphaned one-off schedules (disabled, already fired)."""
617
- data = _daemon_request("POST", host, port, "/api/schedules/cleanup", token)
592
+ with _daemon_errors():
593
+ data = DaemonClient(host, port, token).cleanup_schedules()
618
594
  removed = data.get("removed", [])
619
595
  if not removed:
620
596
  console.print("Nothing to clean up")
@@ -627,60 +603,65 @@ def schedule_cleanup(
627
603
  @schedule_app.command("remove")
628
604
  def schedule_remove(
629
605
  schedule_id: str = typer.Argument(help="Schedule ID to remove"),
630
- host: str = HOST_OPTION,
631
- port: int = PORT_OPTION,
606
+ host: Optional[str] = HOST_OPTION,
607
+ port: Optional[int] = PORT_OPTION,
632
608
  token: Optional[str] = TOKEN_OPTION,
633
609
  ):
634
610
  """Remove a schedule."""
635
- _daemon_request("DELETE", host, port, f"/api/schedules/{schedule_id}", token)
611
+ with _daemon_errors():
612
+ DaemonClient(host, port, token).delete_schedule(schedule_id)
636
613
  console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] removed")
637
614
 
638
615
 
639
616
  @schedule_app.command("enable")
640
617
  def schedule_enable(
641
618
  schedule_id: str = typer.Argument(help="Schedule ID"),
642
- host: str = HOST_OPTION,
643
- port: int = PORT_OPTION,
619
+ host: Optional[str] = HOST_OPTION,
620
+ port: Optional[int] = PORT_OPTION,
644
621
  token: Optional[str] = TOKEN_OPTION,
645
622
  ):
646
623
  """Enable a schedule."""
647
- _daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/enable", token)
624
+ with _daemon_errors():
625
+ DaemonClient(host, port, token).enable_schedule(schedule_id)
648
626
  console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] enabled")
649
627
 
650
628
 
651
629
  @schedule_app.command("disable")
652
630
  def schedule_disable(
653
631
  schedule_id: str = typer.Argument(help="Schedule ID"),
654
- host: str = HOST_OPTION,
655
- port: int = PORT_OPTION,
632
+ host: Optional[str] = HOST_OPTION,
633
+ port: Optional[int] = PORT_OPTION,
656
634
  token: Optional[str] = TOKEN_OPTION,
657
635
  ):
658
636
  """Disable a schedule."""
659
- _daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/disable", token)
637
+ with _daemon_errors():
638
+ DaemonClient(host, port, token).disable_schedule(schedule_id)
660
639
  console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] disabled")
661
640
 
662
641
 
663
642
  @schedule_app.command("run")
664
643
  def schedule_run(
665
644
  schedule_id: str = typer.Argument(help="Schedule ID to trigger now"),
666
- host: str = HOST_OPTION,
667
- port: int = PORT_OPTION,
645
+ host: Optional[str] = HOST_OPTION,
646
+ port: Optional[int] = PORT_OPTION,
668
647
  token: Optional[str] = TOKEN_OPTION,
669
648
  ):
670
649
  """Trigger a schedule immediately."""
671
- _daemon_request("POST", host, port, f"/api/schedules/{schedule_id}/run", token)
650
+ with _daemon_errors():
651
+ DaemonClient(host, port, token).run_schedule(schedule_id)
672
652
  console.print(f"[green]✓[/green] Schedule [cyan]{schedule_id}[/cyan] triggered")
673
653
 
674
654
 
675
655
  @schedule_app.command("show")
676
656
  def schedule_show(
677
657
  schedule_id: str = typer.Argument(help="Schedule ID"),
678
- host: str = HOST_OPTION,
679
- port: int = PORT_OPTION,
658
+ host: Optional[str] = HOST_OPTION,
659
+ port: Optional[int] = PORT_OPTION,
680
660
  token: Optional[str] = TOKEN_OPTION,
681
661
  ):
682
662
  """Show schedule details."""
683
- data = _daemon_request("GET", host, port, f"/api/schedules/{schedule_id}", token)
663
+ with _daemon_errors():
664
+ data = DaemonClient(host, port, token).get_schedule(schedule_id)
684
665
 
685
666
  console.print(f"[bold]{data['id']}[/bold]")
686
667
  console.print(f" agent: {data['agent']}")