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.
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agentd.py +5 -1
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/chat.py +6 -2
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/ticket_run.py +38 -7
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/cli.py +6 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/platform_compat.py +36 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/run_auto.py +7 -1
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/api.py +35 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/runner.py +18 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/server.py +19 -1
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/app.js +94 -32
- devops_bot_sdk-1.6.14/sdk/ui/static/schedules.html +102 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/updater.py +32 -11
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_run_ticket.py +64 -0
- devops_bot_sdk-1.6.14/tests/test_console_encoding.py +170 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_schedule_api.py +57 -0
- devops_bot_sdk-1.6.10/sdk/ui/static/schedules.html +0 -58
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/README.md +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.10 → devops_bot_sdk-1.6.14}/tests/test_vps_health.py +0 -0
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
110
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
|
1196
|
-
|
|
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:
|
|
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
|
-
|
|
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>
|