tsugite-cli 0.23.2__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.2 → tsugite_cli-0.24.0}/PKG-INFO +1 -1
  2. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/pyproject.toml +1 -1
  3. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_preparation.py +2 -2
  4. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/history_integration.py +28 -20
  5. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/runner.py +8 -0
  6. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/steps.py +1 -0
  7. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/agent_config.py +14 -14
  8. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/scheduling/SKILL.md +2 -0
  9. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/daemon.py +64 -83
  10. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/history.py +2 -2
  11. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/agent.py +20 -6
  12. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/subprocess_executor.py +12 -0
  13. tsugite_cli-0.24.0/tsugite/daemon_client.py +201 -0
  14. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/helpers.py +14 -3
  15. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/models.py +1 -1
  16. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/sqlite_backend.py +17 -7
  17. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/permissions.py +31 -3
  18. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/anthropic.py +5 -3
  19. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/openai_compat.py +3 -1
  20. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/skill_discovery.py +6 -0
  21. tsugite_cli-0.24.0/tsugite/tools/daemon_control.py +107 -0
  22. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/http.py +27 -2
  23. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/jobs.py +21 -2
  24. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/skills.py +19 -13
  25. tsugite_cli-0.23.2/tsugite/tools/daemon_control.py +0 -60
  26. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/.gitignore +0 -0
  27. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/AGENTS.md +0 -0
  28. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/CONTRIBUTING.md +0 -0
  29. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/LICENSE +0 -0
  30. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/README.md +0 -0
  31. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/__init__.py +0 -0
  32. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_inheritance.py +0 -0
  33. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/__init__.py +0 -0
  34. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_directives.py +0 -0
  35. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/exec_runner.py +0 -0
  36. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/helpers.py +0 -0
  37. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/metrics.py +0 -0
  38. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/models.py +0 -0
  39. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_runner/validation.py +0 -0
  40. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/agent_utils.py +0 -0
  41. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/approval.py +0 -0
  42. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/__init__.py +0 -0
  43. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/auto_context.py +0 -0
  44. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/base.py +0 -0
  45. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/delegation.py +0 -0
  46. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/file.py +0 -0
  47. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/inline.py +0 -0
  48. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/storage.py +0 -0
  49. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/attachments/url.py +0 -0
  50. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/.gitkeep +0 -0
  51. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/code_searcher.md +0 -0
  52. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/default.md +0 -0
  53. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/file_searcher.md +0 -0
  54. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_verifier.md +0 -0
  55. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/job_worker.md +0 -0
  56. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_agents/onboard.md +0 -0
  57. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_context.py +0 -0
  58. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/.gitkeep +0 -0
  59. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  60. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  61. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  62. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  63. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  64. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  65. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  66. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  67. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cache.py +0 -0
  68. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cancellation.py +0 -0
  69. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/__init__.py +0 -0
  70. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/agents.py +0 -0
  71. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/attachments.py +0 -0
  72. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/cache.py +0 -0
  73. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/chat.py +0 -0
  74. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/config.py +0 -0
  75. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/exec.py +0 -0
  76. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/helpers.py +0 -0
  77. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/init.py +0 -0
  78. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/models.py +0 -0
  79. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/plugins.py +0 -0
  80. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/render.py +0 -0
  81. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/run.py +0 -0
  82. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/secrets.py +0 -0
  83. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/skills.py +0 -0
  84. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/tools.py +0 -0
  85. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/usage.py +0 -0
  86. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/validate.py +0 -0
  87. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/cli/workspace.py +0 -0
  88. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/config.py +0 -0
  89. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/console.py +0 -0
  90. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/constants.py +0 -0
  91. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/context.py +0 -0
  92. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/context_block.py +0 -0
  93. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/__init__.py +0 -0
  94. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/content_blocks.py +0 -0
  95. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/executor.py +0 -0
  96. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/executor_registry.py +0 -0
  97. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/memory.py +0 -0
  98. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/proxy.py +0 -0
  99. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/record_store.py +0 -0
  100. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/sandbox.py +0 -0
  101. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/state.py +0 -0
  102. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/core/tools.py +0 -0
  103. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/__init__.py +0 -0
  104. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/base.py +0 -0
  105. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/bus.py +0 -0
  106. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/events/events.py +0 -0
  107. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/exceptions.py +0 -0
  108. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/__init__.py +0 -0
  109. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/base.py +0 -0
  110. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/reconstruction.py +0 -0
  111. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/registry.py +0 -0
  112. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/sqlite_conn.py +0 -0
  113. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/sqlite_schema.py +0 -0
  114. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/history/ui_events.py +0 -0
  115. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/hooks.py +0 -0
  116. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/interaction.py +0 -0
  117. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/md_agents.py +0 -0
  118. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/models.py +0 -0
  119. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/options.py +0 -0
  120. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/plugins.py +0 -0
  121. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/prompt_xml.py +0 -0
  122. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/__init__.py +0 -0
  123. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/base.py +0 -0
  124. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/model_cache.py +0 -0
  125. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/model_registry.py +0 -0
  126. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/ollama.py +0 -0
  127. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/providers/openrouter.py +0 -0
  128. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/py.typed +0 -0
  129. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/renderer.py +0 -0
  130. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/schemas/__init__.py +0 -0
  131. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/schemas/agent.schema.json +0 -0
  132. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/__init__.py +0 -0
  133. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/backend.py +0 -0
  134. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/env.py +0 -0
  135. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/exec.py +0 -0
  136. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/file.py +0 -0
  137. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/masking.py +0 -0
  138. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/redaction.py +0 -0
  139. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/registry.py +0 -0
  140. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/secrets/sqlite.py +0 -0
  141. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/shell_tool_config.py +0 -0
  142. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/AGENTS.md +0 -0
  143. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/IDENTITY.md +0 -0
  144. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/MEMORY.md +0 -0
  145. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/USER.md +0 -0
  146. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/personas/casual-technical.md +0 -0
  147. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/personas/marvin.md +0 -0
  148. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/templates/personas/minimal.md +0 -0
  149. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/__init__.py +0 -0
  150. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/agents.py +0 -0
  151. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/artifacts.py +0 -0
  152. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/fs.py +0 -0
  153. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/history.py +0 -0
  154. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/interactive.py +0 -0
  155. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/notify.py +0 -0
  156. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/schedule.py +0 -0
  157. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/secrets.py +0 -0
  158. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/sessions.py +0 -0
  159. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/shell.py +0 -0
  160. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/shell_tools.py +0 -0
  161. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tools/time.py +0 -0
  162. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/tsugite.py +0 -0
  163. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/__init__.py +0 -0
  164. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/base.py +0 -0
  165. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/chat.py +0 -0
  166. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/dispatch.py +0 -0
  167. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/helpers.py +0 -0
  168. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/jsonl.py +0 -0
  169. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/plain.py +0 -0
  170. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_chat.py +0 -0
  171. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_commands.py +0 -0
  172. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_completer.py +0 -0
  173. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui/repl_handler.py +0 -0
  174. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui_context.py +0 -0
  175. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/ui_surfaces.py +0 -0
  176. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/usage/__init__.py +0 -0
  177. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/usage/quota.py +0 -0
  178. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/usage/store.py +0 -0
  179. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/user_agent.py +0 -0
  180. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/utils.py +0 -0
  181. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/workspace/__init__.py +0 -0
  182. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/workspace/models.py +0 -0
  183. {tsugite_cli-0.23.2 → tsugite_cli-0.24.0}/tsugite/workspace/session.py +0 -0
  184. {tsugite_cli-0.23.2 → 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.2
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.2"
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"
@@ -242,7 +242,7 @@ class AgentPreparer:
242
242
  auto_load_skills = [s for s in (agent_config.auto_load_skills or []) if s not in suppressed_skills]
243
243
 
244
244
  for skill_name in auto_load_skills:
245
- result = skill_manager.load_skill(skill_name)
245
+ result = skill_manager.load_skill(skill_name, explicit=True)
246
246
  if result.startswith("Failed") or result.startswith("Skill '"):
247
247
  if event_bus:
248
248
  event_bus.emit(SkillLoadFailedEvent(skill_name=skill_name, error_message=result))
@@ -268,7 +268,7 @@ class AgentPreparer:
268
268
  if name in auto_load_skills:
269
269
  # Already loaded above; no need to double-load but it's still sticky.
270
270
  continue
271
- result = skill_manager.load_skill(name)
271
+ result = skill_manager.load_skill(name, explicit=True)
272
272
  if result.startswith("Failed") or result.startswith("Skill '"):
273
273
  if event_bus:
274
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,8 +96,8 @@ 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. A final_result also ends a turn: compaction copies
96
- retained turns into the successor without their session_end.
99
+ so this stays False for it. A final_result also ends a turn copied without
100
+ its session_end (branches, older compacted sessions).
97
101
  """
98
102
  events = list(storage.iter_events(types=("user_input", "session_end", "final_result")))
99
103
  return bool(events) and events[-1].type == "user_input"
@@ -129,8 +133,9 @@ def record_user_input(
129
133
  data["attachments"] = [
130
134
  {"name": a.name, "type": a.content_type.value, "source_url": a.source_url} for a in uploads
131
135
  ]
132
- if channel_metadata:
133
- 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
134
139
  # Structured client context, recorded alongside the folded <client_context> XML
135
140
  # in `text` so the UI reads it back without re-parsing the prompt string.
136
141
  if client_context_items:
@@ -244,8 +249,8 @@ def save_run_to_history(
244
249
  agent_already_recorded = last_model_response > last_session_start
245
250
 
246
251
  if agent_already_recorded:
247
- # A continued conversation pins session_start at index 0, so the current
248
- # 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.
249
254
  turn_start = max(last_session_start, last_user_input)
250
255
  if last_session_end < turn_start:
251
256
  record_session_end(storage, status=status, error_message=error_message)
@@ -264,19 +269,20 @@ def save_run_to_history(
264
269
  tools_called=list(getattr(step, "tools_called", []) or []),
265
270
  )
266
271
 
267
- state_delta = provider_state if provider_state else None
268
- # Split as the agent loop records it: bare id in `model`, prefix in `provider`.
269
- model_provider, _, model_id = model.partition(":")
270
- storage.record(
271
- "model_response",
272
- provider=model_provider if model_id else None,
273
- model=model_id or model,
274
- raw_content=result or "",
275
- usage={"total_tokens": token_count} if token_count else None,
276
- cost=cost,
277
- duration_ms=duration_ms,
278
- state_delta=state_delta,
279
- )
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
+ )
280
286
 
281
287
  record_session_end(storage, status=status, error_message=error_message)
282
288
  return storage.session_id
@@ -288,6 +294,7 @@ class ResumableSessionState:
288
294
 
289
295
  session_id: str
290
296
  compacted: bool = False
297
+ cumulative_cost: float = 0.0
291
298
 
292
299
 
293
300
  def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessionState]:
@@ -324,6 +331,7 @@ def get_resumable_session_state(conversation_id: str) -> Optional[ResumableSessi
324
331
  return ResumableSessionState(
325
332
  session_id=session_id,
326
333
  compacted=bool(state.get("compacted", False)),
334
+ cumulative_cost=state.get("cumulative_cost") or 0.0,
327
335
  )
328
336
  return None
329
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
 
@@ -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
@@ -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']}")
@@ -55,7 +55,7 @@ def _format_cost(cost: Optional[float]) -> str:
55
55
 
56
56
 
57
57
  def _status_style(status: Optional[str]) -> str:
58
- return {"success": "green", "error": "red", "interrupted": "yellow"}.get(status or "", "dim")
58
+ return {"success": "green", "error": "red", "no_action": "red", "interrupted": "yellow"}.get(status or "", "dim")
59
59
 
60
60
 
61
61
  def _format_created(dt: Optional[datetime]) -> str:
@@ -77,7 +77,7 @@ def _parse_date(date_str: Optional[str]) -> Optional[datetime]:
77
77
  @history_app.command("list")
78
78
  def history_list(
79
79
  agent: str = typer.Option(None, "--agent", help="Filter by agent name"),
80
- status: str = typer.Option(None, "--status", help="Filter by status (success, error, interrupted)"),
80
+ status: str = typer.Option(None, "--status", help="Filter by status (success, error, interrupted, no_action)"),
81
81
  tool: str = typer.Option(None, "--tool", help="Filter by tool/function name"),
82
82
  since: str = typer.Option(None, "--since", help="Only sessions after this date (YYYY-MM-DD)"),
83
83
  before: str = typer.Option(None, "--before", help="Only sessions before this date (YYYY-MM-DD)"),