devops-bot-sdk 1.6.10__tar.gz → 1.6.14__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 (120) hide show
  1. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agentd.py +5 -1
  6. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/chat.py +6 -2
  7. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/ticket_run.py +38 -7
  8. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/cli.py +6 -0
  9. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/platform_compat.py +36 -0
  10. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/run_auto.py +7 -1
  11. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/api.py +35 -0
  12. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/runner.py +18 -0
  13. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/server.py +19 -1
  14. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/app.js +94 -32
  15. devops_bot_sdk-1.6.14/sdk/ui/static/schedules.html +102 -0
  16. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/updater.py +32 -11
  17. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_run_ticket.py +64 -0
  18. devops_bot_sdk-1.6.14/tests/test_console_encoding.py +170 -0
  19. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_schedule_api.py +57 -0
  20. devops_bot_sdk-1.6.10/sdk/ui/static/schedules.html +0 -58
  21. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/README.md +0 -0
  22. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  23. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  24. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/requires.txt +0 -0
  25. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  26. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/pyproject.toml +0 -0
  27. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/__init__.py +0 -0
  28. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/activity.py +0 -0
  29. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/builder.py +0 -0
  30. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/connectors.py +0 -0
  31. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/__init__.py +0 -0
  32. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/claude_code.py +0 -0
  33. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/http.py +0 -0
  34. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/mcp.py +0 -0
  35. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/n8n.py +0 -0
  36. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/shell.py +0 -0
  37. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/integrations.py +0 -0
  38. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/inventory.py +0 -0
  39. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/library/__init__.py +0 -0
  40. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/manifest.py +0 -0
  41. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/prompt_studio.py +0 -0
  42. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/prompt_template.py +0 -0
  43. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/prompt_vars.py +0 -0
  44. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/run_log.py +0 -0
  45. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/schedules.py +0 -0
  46. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/secrets.py +0 -0
  47. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/usage.py +0 -0
  48. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/varstore.py +0 -0
  49. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/client.py +0 -0
  50. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/__init__.py +0 -0
  51. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/files.py +0 -0
  52. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/process.py +0 -0
  53. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/screenshot.py +0 -0
  54. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/config.py +0 -0
  55. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/crucial.py +0 -0
  56. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/__init__.py +0 -0
  57. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/assets.py +0 -0
  58. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/bootstrap.py +0 -0
  59. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/browser.py +0 -0
  60. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/compare.py +0 -0
  61. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/loop.py +0 -0
  62. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/exceptions.py +0 -0
  63. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/git_ops.py +0 -0
  64. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/github_ingest.py +0 -0
  65. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/graphify.py +0 -0
  66. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/hooks/__init__.py +0 -0
  67. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/hooks/crucial_guard.py +0 -0
  68. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ipc/__init__.py +0 -0
  69. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ipc/electron_bridge.py +0 -0
  70. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ipc/handlers.py +0 -0
  71. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/local_exec.py +0 -0
  72. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/__init__.py +0 -0
  73. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/envelope.py +0 -0
  74. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/requests.py +0 -0
  75. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/responses.py +0 -0
  76. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/snapshots.py +0 -0
  77. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/py.typed +0 -0
  78. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/scheduler.py +0 -0
  79. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/sse.py +0 -0
  80. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/test.py +0 -0
  81. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/test_pipeline.py +0 -0
  82. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/__init__.py +0 -0
  83. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/dashboard.py +0 -0
  84. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/health.py +0 -0
  85. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/ip_allowlist.py +0 -0
  86. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/jobs.py +0 -0
  87. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/models.py +0 -0
  88. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/security.py +0 -0
  89. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/service.py +0 -0
  90. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/session.py +0 -0
  91. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/setup_manager.py +0 -0
  92. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/agents.html +0 -0
  93. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/custom-agents.html +0 -0
  94. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/default-agents.html +0 -0
  95. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/index.html +0 -0
  96. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/login.html +0 -0
  97. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/runs.html +0 -0
  98. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/setup.html +0 -0
  99. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/styles.css +0 -0
  100. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/tickets.html +0 -0
  101. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/vps_health.py +0 -0
  102. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/setup.cfg +0 -0
  103. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_activity.py +0 -0
  104. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_chat_features.py +0 -0
  105. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_listing.py +0 -0
  106. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_prompt_async.py +0 -0
  107. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_prompt_models.py +0 -0
  108. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_schedules.py +0 -0
  109. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_workspace_settings.py +0 -0
  110. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agents_connectors_builder.py +0 -0
  111. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_allowed_tools_config.py +0 -0
  112. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_git_action.py +0 -0
  113. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_github_ingest.py +0 -0
  114. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_health.py +0 -0
  115. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_otp_login.py +0 -0
  116. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_run_outputs.py +0 -0
  117. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_scheduler.py +0 -0
  118. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_update_timer.py +0 -0
  119. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_updater_extras.py +0 -0
  120. {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_vps_health.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.10
3
+ Version: 1.6.14
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.10
3
+ Version: 1.6.14
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -104,6 +104,7 @@ tests/test_agent_schedules.py
104
104
  tests/test_agent_workspace_settings.py
105
105
  tests/test_agents_connectors_builder.py
106
106
  tests/test_allowed_tools_config.py
107
+ tests/test_console_encoding.py
107
108
  tests/test_git_action.py
108
109
  tests/test_github_ingest.py
109
110
  tests/test_health.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.10
3
+ Version: 1.6.14
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.6.10" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.6.14" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
34
34
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
35
35
 
36
36
  from sdk.client import BackendClient
@@ -669,10 +669,14 @@ def status() -> None:
669
669
 
670
670
 
671
671
  def main() -> None:
672
+ # Windows: stdout is cp1252 whenever it is not a console (pipe / redirected
673
+ # log), which cannot encode the glyphs we print. Must run before any output.
674
+ from sdk.platform_compat import ensure_macos_fork_safety, ensure_utf8_stdio
675
+ ensure_utf8_stdio()
676
+
672
677
  # macOS: keep the background daemon from aborting ("Python quit unexpectedly")
673
678
  # when it does networking after the double-fork. May re-exec once; must run
674
679
  # before any fork or objc-linked import.
675
- from sdk.platform_compat import ensure_macos_fork_safety
676
680
  ensure_macos_fork_safety()
677
681
 
678
682
  argv = sys.argv[1:]
@@ -106,8 +106,12 @@ async def resolve_agent(name: str, client=None) -> AgentManifest:
106
106
  detail = "; ".join(reasons) if reasons else (
107
107
  "not in ~/.agentos/agents/, your backend agents, or your department defaults"
108
108
  )
109
- message = f"no agent named '{name}' — {detail}"
110
- raise AgentLookupUnavailable(message) if unreadable else LookupError(message)
109
+ if unreadable:
110
+ # Do NOT say "no agent named …" here: a source went unread, so absence was
111
+ # never established. Claiming it sends the user hunting for a typo when the
112
+ # fix is a token or a network.
113
+ raise AgentLookupUnavailable(f"'{name}' could not be looked up — {detail}")
114
+ raise LookupError(f"no agent named '{name}' — {detail}")
111
115
 
112
116
 
113
117
  def _render_conversation(history: list[dict] | None, message: str) -> str:
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
22
22
 
23
23
  async def resolve_manifest(
24
24
  client: "BackendClient", name: str, scope: str = "custom", department_id: str = "",
25
+ reasons: list[str] | None = None,
25
26
  ) -> "AgentManifest | None":
26
27
  """Find the agent manifest to run, by ``scope``.
27
28
 
@@ -34,25 +35,45 @@ async def resolve_manifest(
34
35
  the ``manifest`` column this used to read no longer exists on the table, so
35
36
  reading it raised ``KeyError`` and made every department agent unrunnable.
36
37
 
37
- Returns None when no matching agent is found, so the caller reports it.
38
+ Returns None when no matching agent is found, so the caller reports it. Pass
39
+ ``reasons`` — a list this appends to — to learn WHY: "not found" and "your
40
+ token is not valid so the agent list could not be read" and "the agent exists
41
+ but has no prompt" are three very different problems that all end in None, and
42
+ a bare "was not found" sends the user hunting for a typo that isn't there.
38
43
  """
39
44
  from sdk.agents.chat import resolve_agent
40
45
  from sdk.agents.manifest import manifest_from_backend_row
41
46
 
47
+ def _note(reason: str) -> None:
48
+ if reasons is not None:
49
+ reasons.append(reason)
50
+
42
51
  name = (name or "").strip()
43
52
  if scope == "department":
44
53
  try:
45
54
  rows = await client.list_department_agents()
46
- except Exception: # noqa: BLE001 — treat a backend hiccup as "not found"
55
+ except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
56
+ # str() is EMPTY on some httpx errors (ReadTimeout) — name the class then.
57
+ _note(f"department defaults could not be fetched "
58
+ f"({str(exc) or type(exc).__name__})")
47
59
  return None
48
60
  for row in rows:
49
61
  if row.get("name") == name and row.get("status") == "active":
50
- return manifest_from_backend_row(row)
62
+ manifest = manifest_from_backend_row(row)
63
+ if manifest is None:
64
+ _note(f"'{name}' is a department default but its prompt is empty, "
65
+ f"so there is no instruction to run")
66
+ return manifest
67
+ _note(f"no ACTIVE department default named '{name}' "
68
+ f"({len(rows)} department agent(s) visible to you)")
51
69
  return None
52
70
 
53
71
  try:
54
72
  return await resolve_agent(name, client)
55
- except LookupError:
73
+ except LookupError as exc:
74
+ # resolve_agent names every source it checked and why each one missed —
75
+ # keep that, it is the whole diagnosis.
76
+ _note(str(exc))
56
77
  return None
57
78
 
58
79
 
@@ -76,7 +97,10 @@ async def run_on_ticket(
76
97
  (``{status, task_id, result, ok, error}``).
77
98
 
78
99
  Raises ``ValueError`` when the agent — or an explicitly-named ticket — cannot
79
- be found, so the caller reports a clear message.
100
+ be found, so the caller reports a clear message. For the agent, that message
101
+ carries the resolver's REASON (unreadable agent list vs. genuinely absent vs.
102
+ present-but-promptless): they demand different fixes and used to be
103
+ indistinguishable behind one "was not found".
80
104
  """
81
105
  from sdk.agentd import _dispatch_agent_run
82
106
 
@@ -88,9 +112,16 @@ async def run_on_ticket(
88
112
  raise ValueError(f"Jira ticket '{key}' was not found.")
89
113
  task = records[0]
90
114
 
91
- manifest = await resolve_manifest(client, name, scope=scope, department_id=department_id)
115
+ reasons: list[str] = []
116
+ manifest = await resolve_manifest(
117
+ client, name, scope=scope, department_id=department_id, reasons=reasons,
118
+ )
92
119
  if manifest is None:
93
- raise ValueError(f"Agent '{name}' was not found (scope={scope}).")
120
+ detail = " — ".join(reasons)
121
+ raise ValueError(
122
+ f"Agent '{name}' could not be run (scope={scope})"
123
+ + (f" — {detail}." if detail else ".")
124
+ )
94
125
 
95
126
  return await _dispatch_agent_run(
96
127
  manifest, client, task, auto_approve=True,
@@ -68,6 +68,12 @@ import yaml
68
68
 
69
69
 
70
70
  def main() -> None:
71
+ # Windows: stdout falls back to cp1252 the moment it is not a console (piped,
72
+ # or `agentos … > out.txt`), and cp1252 cannot encode the ✓/→/⚠ this CLI and
73
+ # the updater print. Must run before any output.
74
+ from sdk.platform_compat import ensure_utf8_stdio
75
+ ensure_utf8_stdio()
76
+
71
77
  argv = sys.argv[1:]
72
78
  if not argv:
73
79
  _print_usage()
@@ -10,6 +10,42 @@ import os
10
10
  import sys
11
11
 
12
12
 
13
+ def ensure_utf8_stdio() -> None:
14
+ """Make stdout/stderr able to carry the SDK's ``✓ ↻ → ⚠`` glyphs ANYWHERE.
15
+
16
+ Windows only picks UTF-8 for a stream attached to a real console. Anything
17
+ else — a pipe, or a redirect into a file — falls back to the process locale
18
+ encoding, which on most Windows boxes is cp1252. cp1252 has no ``✓`` (U+2713),
19
+ ``→`` (U+2192), ``↻`` or ``⚠``, so the FIRST such print raises
20
+ ``UnicodeEncodeError`` and takes the whole command down.
21
+
22
+ That is not hypothetical: the control panel spawns ``agentos-auto`` with its
23
+ output redirected into ``auto.log`` (``sdk.ui.runner.spawn_auto``) or onto a
24
+ pipe (``run_auto_capture``), so every UI-driven run reached a non-console
25
+ stdout and died inside the update banner — before the run itself had begun.
26
+ The same crash hits a plain ``agentos … > out.txt`` in cmd.
27
+
28
+ Both streams are reconfigured to UTF-8 with ``errors="replace"``: the encoding
29
+ covers our glyphs, and the error handler guarantees that an odd byte echoed
30
+ from engine output can still never kill the process. ``PYTHONIOENCODING`` is
31
+ exported too, so children we spawn — including the interpreter that
32
+ ``updater.auto_update_and_reexec`` re-execs — inherit the same guarantee even
33
+ though they never call this function themselves.
34
+
35
+ Safe to call more than once, and a no-op when a stream has already been
36
+ swapped for a plain file object (the daemons redirect stdout to their log).
37
+ """
38
+ os.environ.setdefault("PYTHONIOENCODING", "utf-8")
39
+ for stream in (sys.stdout, sys.stderr):
40
+ reconfigure = getattr(stream, "reconfigure", None)
41
+ if reconfigure is None:
42
+ continue # None under pythonw, or already a non-TextIOWrapper
43
+ try:
44
+ reconfigure(encoding="utf-8", errors="replace")
45
+ except Exception: # noqa: BLE001 — never let the shim break startup
46
+ pass
47
+
48
+
13
49
  def ensure_macos_fork_safety() -> None:
14
50
  """Stop macOS from killing our background daemons with a native abort.
15
51
 
@@ -544,10 +544,16 @@ def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None,
544
544
 
545
545
 
546
546
  def main() -> None:
547
+ # Windows: this command is usually spawned by the panel with its output on a
548
+ # pipe or redirected into auto.log, so stdout is NOT a console and defaults to
549
+ # cp1252 — which cannot encode the ✓/→ in the update banner below. Must run
550
+ # before the first print.
551
+ from sdk.platform_compat import ensure_macos_fork_safety, ensure_utf8_stdio
552
+ ensure_utf8_stdio()
553
+
547
554
  # macOS: keep the background daemon from aborting ("Python quit unexpectedly")
548
555
  # when it does networking after the double-fork. May re-exec once; must run
549
556
  # before any fork or objc-linked import.
550
- from sdk.platform_compat import ensure_macos_fork_safety
551
557
  ensure_macos_fork_safety()
552
558
 
553
559
  argv = sys.argv[1:]
@@ -742,6 +742,41 @@ async def list_schedules() -> dict:
742
742
  return {"schedules": rows, "scheduler": scheduler.status()}
743
743
 
744
744
 
745
+ @router.get("/agents/schedules/preview", dependencies=[_auth])
746
+ async def preview_schedule(cron: str = Query(...), timezone: str = Query("Asia/Karachi"),
747
+ count: int = Query(3)) -> dict:
748
+ """Validate a cron expression and show the next few times it would fire.
749
+
750
+ A cron expression is easy to get subtly wrong (``0 9 * * 1`` vs ``0 9 1 * *``),
751
+ and a schedule that is wrong by a factor of thirty is not obvious until a month
752
+ of nothing. The page shows these times as you build the schedule, in the
753
+ schedule's own timezone, so the answer is visible before saving.
754
+ """
755
+ from datetime import datetime, timezone as _tz
756
+ from zoneinfo import ZoneInfo
757
+
758
+ from sdk.agents import schedules as sched_store
759
+
760
+ ok, err = sched_store.validate(cron, timezone)
761
+ if not ok:
762
+ return {"ok": False, "error": err, "next": []}
763
+
764
+ probe = {"cron": cron, "timezone": timezone}
765
+ cursor = datetime.now(_tz.utc)
766
+ out: list[str] = []
767
+ for _ in range(max(1, min(10, count))):
768
+ nxt = sched_store.next_fire(probe, cursor)
769
+ if nxt is None:
770
+ break
771
+ try:
772
+ local = nxt.astimezone(ZoneInfo(timezone))
773
+ except Exception: # noqa: BLE001 — a box without tzdata still gets a time
774
+ local = nxt
775
+ out.append(f"{local.strftime('%a')} {local.day} {local.strftime('%b %Y, %I:%M %p %Z')}")
776
+ cursor = nxt
777
+ return {"ok": True, "error": "", "next": out}
778
+
779
+
745
780
  @router.post("/agents/schedules", dependencies=[_auth])
746
781
  async def create_schedule(body: ScheduleCreateRequest) -> dict:
747
782
  """Put an agent on a cron.
@@ -52,6 +52,22 @@ def _auto_argv(*args: str) -> list[str]:
52
52
  return [sys.executable, "-m", "sdk.run_auto", *args]
53
53
 
54
54
 
55
+ def _child_env() -> dict[str, str]:
56
+ """This process's env plus a forced UTF-8 stdio for the child.
57
+
58
+ Neither launch below gives the child a console — one redirects its output into
59
+ ``auto.log``, the other puts it on a pipe — and on Windows a non-console stdout
60
+ falls back to the locale encoding (cp1252 on most boxes). cp1252 cannot encode
61
+ the ``✓``/``→``/``⚠`` the updater and the loop print, so the child used to die
62
+ with ``UnicodeEncodeError`` before doing any work. The child's own ``main()``
63
+ calls ``ensure_utf8_stdio``; setting it here as well covers a panel started
64
+ straight through uvicorn (whose ``main()`` never ran) and any grandchild.
65
+ """
66
+ env = dict(os.environ)
67
+ env.setdefault("PYTHONIOENCODING", "utf-8:replace")
68
+ return env
69
+
70
+
55
71
  # ── Fire-and-forget execution ──────────────────────────────────────────────
56
72
 
57
73
  def spawn_auto(*args: str) -> dict:
@@ -93,6 +109,7 @@ def spawn_auto(*args: str) -> dict:
93
109
  stdout=stdout,
94
110
  stderr=stderr,
95
111
  close_fds=True,
112
+ env=_child_env(),
96
113
  creationflags=creationflags,
97
114
  start_new_session=start_new_session,
98
115
  cwd=str(Path.home()),
@@ -115,6 +132,7 @@ async def run_auto_capture(*args: str, timeout: float = 180.0) -> dict:
115
132
  stdout=asyncio.subprocess.PIPE,
116
133
  stderr=asyncio.subprocess.STDOUT,
117
134
  cwd=str(Path.home()),
135
+ env=_child_env(),
118
136
  )
119
137
  out, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
120
138
  return {
@@ -127,7 +127,20 @@ def create_app():
127
127
  return {"ok": True}
128
128
 
129
129
  # Static assets (index.html, setup.html, app.js, styles.css).
130
- app.mount("/", StaticFiles(directory=str(_STATIC_DIR), html=True), name="static")
130
+ #
131
+ # `no-cache` does NOT mean "don't cache" — it means "revalidate before reuse".
132
+ # The browser keeps its copy and sends If-None-Match; an unchanged file answers
133
+ # 304 (no body). The panel updates itself under the user's feet on every SDK
134
+ # release, and without this a browser happily serves a months-old app.js from
135
+ # disk cache with no request at all — a new page in the nav simply never appears
136
+ # and the only fix is a hard refresh nobody knows to do.
137
+ class _RevalidatingStatic(StaticFiles):
138
+ def file_response(self, *args, **kwargs): # noqa: ANN002, ANN003
139
+ resp = super().file_response(*args, **kwargs)
140
+ resp.headers["Cache-Control"] = "no-cache"
141
+ return resp
142
+
143
+ app.mount("/", _RevalidatingStatic(directory=str(_STATIC_DIR), html=True), name="static")
131
144
 
132
145
  # SPA-ish fallback: unknown non-API path → dashboard shell.
133
146
  @app.exception_handler(404)
@@ -174,6 +187,11 @@ def _service_command(cmd: str, argv: list[str]) -> None:
174
187
 
175
188
 
176
189
  def main() -> None:
190
+ # Windows: as a service (Scheduled Task) the panel's stdout is a redirected
191
+ # file, not a console — cp1252 there, which cannot encode the glyphs we print.
192
+ from sdk.platform_compat import ensure_utf8_stdio
193
+ ensure_utf8_stdio()
194
+
177
195
  argv = sys.argv[1:]
178
196
  cmd = argv[0] if argv and not argv[0].startswith("-") else "serve"
179
197
  if cmd in ("install", "uninstall", "status"):
@@ -1156,44 +1156,95 @@
1156
1156
  let agents = {}; // name -> {variables: [...]}
1157
1157
  let editingId = null;
1158
1158
 
1159
- const PRESETS = [
1160
- ["30 9 * * *", "Every day at 09:30"],
1161
- ["0 * * * *", "Every hour, on the hour"],
1162
- ["0 9 * * 1-5", "Weekdays at 09:00"],
1163
- ["0 18 * * 5", "Fridays at 18:00"],
1164
- ["0 9 1 * *", "1st of each month, 09:00"],
1165
- ["", "Custom…"],
1166
- ];
1167
-
1168
- function renderPresets() {
1169
- $("sc-preset").innerHTML = PRESETS
1170
- .map(([cron, label]) => `<option value="${esc(cron)}">${esc(label)}</option>`).join("");
1171
- $("sc-preset").addEventListener("change", () => {
1172
- const v = $("sc-preset").value;
1173
- if (v) $("sc-cron").value = v;
1174
- $("sc-cron").focus();
1175
- });
1176
- $("sc-cron").value = PRESETS[0][0];
1159
+ // ── Time frame → cron ──────────────────────────────────────────────────
1160
+ // Cron is the storage format (the scheduler parses it), but almost nobody
1161
+ // writes it correctly by hand — "0 9 * * 1" and "0 9 1 * *" differ by a factor
1162
+ // of thirty and look identical at a glance. So the UI builds the expression
1163
+ // from plain choices and SHOWS the next few fire times before you commit;
1164
+ // "Custom cron" stays available for anything this doesn't cover.
1165
+ function buildCron() {
1166
+ const mode = $("sc-every").value;
1167
+ if (mode === "cron") return $("sc-cron").value.trim();
1168
+ const n = Math.max(1, parseInt($("sc-n").value, 10) || 1);
1169
+ if (mode === "minutes") return `*/${Math.min(n, 59)} * * * *`;
1170
+ if (mode === "hours") return `0 */${Math.min(n, 23)} * * *`;
1171
+
1172
+ const [hh, mm] = ($("sc-time").value || "09:30").split(":");
1173
+ const h = parseInt(hh, 10) || 0, m = parseInt(mm, 10) || 0;
1174
+ // Day NAMES, never numbers: APScheduler's day-of-week is 0=Monday while
1175
+ // standard cron is 0=Sunday, so "1-5" here would silently mean Tue–Sat.
1176
+ if (mode === "daily") return `${m} ${h} * * *`;
1177
+ if (mode === "weekdays") return `${m} ${h} * * mon-fri`;
1178
+ if (mode === "weekly") return `${m} ${h} * * ${$("sc-dow").value}`;
1179
+ if (mode === "monthly") return `${m} ${h} ${Math.max(1, parseInt($("sc-dom").value, 10) || 1)} * *`;
1180
+ return `${m} ${h} * * *`;
1181
+ }
1182
+
1183
+ function syncFields() {
1184
+ const mode = $("sc-every").value;
1185
+ const show = (id, on) => { $(id).style.display = on ? "" : "none"; };
1186
+ show("sc-wrap-n", mode === "minutes" || mode === "hours");
1187
+ show("sc-wrap-time", ["daily", "weekdays", "weekly", "monthly"].includes(mode));
1188
+ show("sc-wrap-dow", mode === "weekly");
1189
+ show("sc-wrap-dom", mode === "monthly");
1190
+ $("sc-cron").style.display = mode === "cron" ? "" : "none";
1191
+ if (mode !== "cron") $("sc-cron").value = buildCron();
1192
+ refreshPreview();
1193
+ }
1194
+
1195
+ let previewTimer = null;
1196
+ function refreshPreview() {
1197
+ clearTimeout(previewTimer);
1198
+ previewTimer = setTimeout(async () => {
1199
+ const cron = buildCron();
1200
+ if (!cron) { $("sc-preview").textContent = ""; return; }
1201
+ try {
1202
+ const d = await api("/api/ui/agents/schedules/preview?cron="
1203
+ + encodeURIComponent(cron) + "&timezone=Asia/Karachi");
1204
+ $("sc-preview").innerHTML = d.ok
1205
+ ? `<code>${esc(cron)}</code> — next: ${d.next.map(esc).join(" · ")}`
1206
+ : `<span style="color:#c00">${esc(d.error)}</span>`;
1207
+ } catch (e) { $("sc-preview").textContent = ""; }
1208
+ }, 250);
1209
+ }
1210
+
1211
+ // ── Inputs ─────────────────────────────────────────────────────────────
1212
+ // Rows are name+value pairs. The agent's own {{variables}} are pre-seeded with
1213
+ // their names locked (removing one would guarantee a failed run); anything the
1214
+ // user adds is free-form, because a prompt can reference a variable the row's
1215
+ // declared list never mentioned.
1216
+ function varRow(name, value, locked) {
1217
+ const div = document.createElement("div");
1218
+ div.className = "controls";
1219
+ div.style.marginTop = "6px";
1220
+ div.innerHTML =
1221
+ `<input data-k type="text" placeholder="name" value="${esc(name || "")}"
1222
+ style="max-width:220px" ${locked ? "readonly" : ""} />
1223
+ <input data-v type="text" placeholder="value" value="${esc(value || "")}" />
1224
+ ${locked ? "" : `<button class="btn" type="button" data-x>Remove</button>`}`;
1225
+ const rm = div.querySelector("[data-x]");
1226
+ if (rm) rm.addEventListener("click", () => div.remove());
1227
+ return div;
1177
1228
  }
1178
1229
 
1179
- // One input per {{variable}}. A scheduled run has nobody to ask, so the
1180
- // values are stored WITH the schedule rather than read from the prefill store.
1181
1230
  function renderVars(name, current) {
1182
- const vars = (agents[name] || {}).variables || [];
1183
- if (!vars.length) {
1184
- $("sc-vars").innerHTML = `<p class="muted">This agent needs no inputs.</p>`;
1185
- return;
1231
+ const host = $("sc-vars");
1232
+ host.innerHTML = "";
1233
+ const declared = (agents[name] || {}).variables || [];
1234
+ declared.forEach(v => host.appendChild(varRow(v, (current || {})[v] || "", true)));
1235
+ Object.keys(current || {})
1236
+ .filter(k => !declared.includes(k))
1237
+ .forEach(k => host.appendChild(varRow(k, current[k], false)));
1238
+ if (!host.children.length) {
1239
+ host.innerHTML = `<p class="muted">This agent declares no inputs — add one only if its prompt expects it.</p>`;
1186
1240
  }
1187
- $("sc-vars").innerHTML =
1188
- `<label>Inputs (saved with the schedule and reused every run)</label>` +
1189
- vars.map(v => `<input data-var="${esc(v)}" type="text" placeholder="${esc(v)}"
1190
- value="${esc((current || {})[v] || "")}" style="margin-top:6px" />`).join("");
1191
1241
  }
1192
1242
 
1193
1243
  function collectVars() {
1194
1244
  const out = {};
1195
- document.querySelectorAll("#sc-vars [data-var]").forEach(el => {
1196
- out[el.getAttribute("data-var")] = el.value;
1245
+ document.querySelectorAll("#sc-vars .controls").forEach(row => {
1246
+ const k = row.querySelector("[data-k]"), v = row.querySelector("[data-v]");
1247
+ if (k && v && k.value.trim()) out[k.value.trim()] = v.value;
1197
1248
  });
1198
1249
  return out;
1199
1250
  }
@@ -1304,6 +1355,17 @@
1304
1355
 
1305
1356
  $("sc-agent").addEventListener("change", () => renderVars($("sc-agent").value, {}));
1306
1357
  $("sc-hist-agent").addEventListener("change", () => loadHistory());
1358
+ $("sc-every").addEventListener("change", syncFields);
1359
+ ["sc-n", "sc-time", "sc-dow", "sc-dom", "sc-cron"].forEach(id =>
1360
+ $(id).addEventListener("input", () => {
1361
+ if ($("sc-every").value !== "cron") $("sc-cron").value = buildCron();
1362
+ refreshPreview();
1363
+ }));
1364
+ $("sc-add-var").addEventListener("click", () => {
1365
+ const host = $("sc-vars");
1366
+ if (host.querySelector("p")) host.innerHTML = "";
1367
+ host.appendChild(varRow("", "", false));
1368
+ });
1307
1369
 
1308
1370
  $("sc-save").addEventListener("click", async () => {
1309
1371
  const btn = $("sc-save"); btn.disabled = true;
@@ -1313,7 +1375,7 @@
1313
1375
  method: "POST",
1314
1376
  body: {
1315
1377
  agent: $("sc-agent").value,
1316
- cron: $("sc-cron").value.trim(),
1378
+ cron: buildCron(),
1317
1379
  timezone: "Asia/Karachi",
1318
1380
  variables: collectVars(),
1319
1381
  scope: (agents[$("sc-agent").value] || {}).source === "default" ? "default" : "custom",
@@ -1325,7 +1387,7 @@
1325
1387
  finally { btn.disabled = false; }
1326
1388
  });
1327
1389
 
1328
- renderPresets();
1390
+ syncFields();
1329
1391
  loadAgents().then(() => { loadSchedules(); loadHistory(); });
1330
1392
  }
1331
1393
 
@@ -0,0 +1,102 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>AgentOS — Schedules</title>
7
+ <link rel="stylesheet" href="/styles.css" />
8
+ </head>
9
+ <body data-page="schedules">
10
+ <div id="topbar"></div>
11
+
12
+ <div class="wrap">
13
+ <div class="panelbox">
14
+ <div class="hd"><h2>Schedule an agent</h2></div>
15
+ <div class="bd">
16
+ <label for="sc-agent">Agent</label>
17
+ <select id="sc-agent"></select>
18
+
19
+ <label for="sc-every" style="margin-top:12px">How often</label>
20
+ <select id="sc-every">
21
+ <option value="minutes">Every N minutes</option>
22
+ <option value="hours">Every N hours</option>
23
+ <option value="daily" selected>Every day</option>
24
+ <option value="weekdays">Every weekday (Mon–Fri)</option>
25
+ <option value="weekly">Every week</option>
26
+ <option value="monthly">Every month</option>
27
+ <option value="cron">Custom cron expression</option>
28
+ </select>
29
+
30
+ <div class="controls" style="margin-top:8px">
31
+ <span id="sc-wrap-n" style="display:none">
32
+ <label for="sc-n" style="display:inline">Every</label>
33
+ <input id="sc-n" type="number" min="1" max="59" value="30" style="width:80px" />
34
+ </span>
35
+ <span id="sc-wrap-time">
36
+ <label for="sc-time" style="display:inline">at</label>
37
+ <input id="sc-time" type="time" value="09:30" />
38
+ </span>
39
+ <span id="sc-wrap-dow" style="display:none">
40
+ <label for="sc-dow" style="display:inline">on</label>
41
+ <!-- Day NAMES, not numbers: APScheduler counts 0=Monday, while
42
+ standard cron counts 0=Sunday, so a numeric day-of-week here is
43
+ off by one against every cron reference a user might check. -->
44
+ <select id="sc-dow">
45
+ <option value="mon">Monday</option><option value="tue">Tuesday</option>
46
+ <option value="wed">Wednesday</option><option value="thu">Thursday</option>
47
+ <option value="fri">Friday</option><option value="sat">Saturday</option>
48
+ <option value="sun">Sunday</option>
49
+ </select>
50
+ </span>
51
+ <span id="sc-wrap-dom" style="display:none">
52
+ <label for="sc-dom" style="display:inline">on day</label>
53
+ <input id="sc-dom" type="number" min="1" max="28" value="1" style="width:80px" />
54
+ </span>
55
+ </div>
56
+
57
+ <input id="sc-cron" type="text" placeholder="30 9 * * *" style="margin-top:8px; display:none" />
58
+ <p class="muted" style="margin:6px 0 0">
59
+ Times are Pakistan time (Asia/Karachi).
60
+ </p>
61
+ <p class="muted" id="sc-preview" style="margin:8px 0 0"></p>
62
+
63
+ <div style="margin-top:16px">
64
+ <label>Inputs</label>
65
+ <p class="muted" style="margin:2px 0 8px">
66
+ Saved with the schedule and reused on every run — a scheduled run has no
67
+ one to ask. Add your own below if the agent's prompt expects extras.
68
+ </p>
69
+ <div id="sc-vars"></div>
70
+ <button id="sc-add-var" class="btn" style="margin-top:8px" type="button">+ Add input</button>
71
+ </div>
72
+
73
+ <div class="controls" style="margin-top:14px">
74
+ <button id="sc-save" class="btn primary">Add schedule</button>
75
+ <span id="sc-msg" class="muted"></span>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="section">
81
+ <div class="panelbox">
82
+ <div class="hd"><h2>Schedules on this machine</h2></div>
83
+ <div class="bd"><div id="sc-list"></div></div>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="section">
88
+ <div class="panelbox">
89
+ <div class="hd"><h2>Run history</h2></div>
90
+ <div class="bd">
91
+ <label for="sc-hist-agent">Agent</label>
92
+ <select id="sc-hist-agent"></select>
93
+ <p class="muted" id="sc-folder" style="margin:8px 0 0"></p>
94
+ <div id="sc-files" style="margin-top:12px"></div>
95
+ <div id="sc-file-body" class="log" style="margin-top:12px; white-space:pre-wrap"></div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <script src="/app.js"></script>
101
+ </body>
102
+ </html>