devops-bot-sdk 1.4.158__tar.gz → 1.5.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 (107) hide show
  1. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/PKG-INFO +2 -1
  2. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/PKG-INFO +2 -1
  3. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/SOURCES.txt +7 -1
  4. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/requires.txt +1 -0
  5. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/pyproject.toml +5 -0
  6. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/__init__.py +2 -2
  7. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agentd.py +18 -0
  8. devops_bot_sdk-1.5.0/sdk/agents/activity.py +110 -0
  9. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/cli.py +73 -2
  10. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/client.py +107 -10
  11. devops_bot_sdk-1.5.0/sdk/github_ingest.py +345 -0
  12. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/api.py +75 -22
  13. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/server.py +45 -1
  14. devops_bot_sdk-1.5.0/sdk/vps_health.py +264 -0
  15. devops_bot_sdk-1.5.0/tests/test_agent_activity.py +93 -0
  16. devops_bot_sdk-1.5.0/tests/test_github_ingest.py +327 -0
  17. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_otp_login.py +8 -0
  18. devops_bot_sdk-1.5.0/tests/test_vps_health.py +209 -0
  19. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/README.md +0 -0
  20. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  21. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  22. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  23. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/builder.py +0 -0
  25. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/chat.py +0 -0
  26. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/connectors.py +0 -0
  27. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/claude_code.py +0 -0
  29. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/http.py +0 -0
  30. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/mcp.py +0 -0
  31. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/n8n.py +0 -0
  32. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/shell.py +0 -0
  33. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/integrations.py +0 -0
  34. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/inventory.py +0 -0
  35. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/library/__init__.py +0 -0
  36. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/manifest.py +0 -0
  37. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/prompt_studio.py +0 -0
  38. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/prompt_template.py +0 -0
  39. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/prompt_vars.py +0 -0
  40. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/run_log.py +0 -0
  41. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/secrets.py +0 -0
  42. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/ticket_run.py +0 -0
  43. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/usage.py +0 -0
  44. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/varstore.py +0 -0
  45. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/__init__.py +0 -0
  46. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/files.py +0 -0
  47. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/process.py +0 -0
  48. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/screenshot.py +0 -0
  49. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/config.py +0 -0
  50. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/crucial.py +0 -0
  51. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/__init__.py +0 -0
  52. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/assets.py +0 -0
  53. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/bootstrap.py +0 -0
  54. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/browser.py +0 -0
  55. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/compare.py +0 -0
  56. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/loop.py +0 -0
  57. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/exceptions.py +0 -0
  58. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/git_ops.py +0 -0
  59. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/graphify.py +0 -0
  60. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/hooks/__init__.py +0 -0
  61. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/hooks/crucial_guard.py +0 -0
  62. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ipc/__init__.py +0 -0
  63. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ipc/electron_bridge.py +0 -0
  64. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ipc/handlers.py +0 -0
  65. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/local_exec.py +0 -0
  66. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/__init__.py +0 -0
  67. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/envelope.py +0 -0
  68. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/requests.py +0 -0
  69. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/responses.py +0 -0
  70. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/snapshots.py +0 -0
  71. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/platform_compat.py +0 -0
  72. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/py.typed +0 -0
  73. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/run_auto.py +0 -0
  74. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/sse.py +0 -0
  75. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/test.py +0 -0
  76. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/test_pipeline.py +0 -0
  77. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/__init__.py +0 -0
  78. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/dashboard.py +0 -0
  79. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/health.py +0 -0
  80. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/ip_allowlist.py +0 -0
  81. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/jobs.py +0 -0
  82. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/models.py +0 -0
  83. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/runner.py +0 -0
  84. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/security.py +0 -0
  85. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/service.py +0 -0
  86. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/session.py +0 -0
  87. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/setup_manager.py +0 -0
  88. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/agents.html +0 -0
  89. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/app.js +0 -0
  90. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/custom-agents.html +0 -0
  91. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/default-agents.html +0 -0
  92. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/index.html +0 -0
  93. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/login.html +0 -0
  94. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/runs.html +0 -0
  95. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/setup.html +0 -0
  96. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/styles.css +0 -0
  97. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/tickets.html +0 -0
  98. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/updater.py +0 -0
  99. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/setup.cfg +0 -0
  100. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_chat_features.py +0 -0
  101. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_prompt_async.py +0 -0
  102. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_prompt_models.py +0 -0
  103. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_run_ticket.py +0 -0
  104. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agents_connectors_builder.py +0 -0
  105. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_git_action.py +0 -0
  106. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_health.py +0 -0
  107. {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_updater_extras.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.158
3
+ Version: 1.5.0
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
@@ -20,6 +20,7 @@ Requires-Dist: psutil>=5.9; extra == "process"
20
20
  Provides-Extra: ui
21
21
  Requires-Dist: fastapi>=0.110; extra == "ui"
22
22
  Requires-Dist: uvicorn>=0.29; extra == "ui"
23
+ Requires-Dist: psutil>=5.9; extra == "ui"
23
24
  Provides-Extra: all
24
25
  Requires-Dist: Pillow>=10.0; extra == "all"
25
26
  Requires-Dist: psutil>=5.9; extra == "all"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.158
3
+ Version: 1.5.0
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
@@ -20,6 +20,7 @@ Requires-Dist: psutil>=5.9; extra == "process"
20
20
  Provides-Extra: ui
21
21
  Requires-Dist: fastapi>=0.110; extra == "ui"
22
22
  Requires-Dist: uvicorn>=0.29; extra == "ui"
23
+ Requires-Dist: psutil>=5.9; extra == "ui"
23
24
  Provides-Extra: all
24
25
  Requires-Dist: Pillow>=10.0; extra == "all"
25
26
  Requires-Dist: psutil>=5.9; extra == "all"
@@ -14,6 +14,7 @@ sdk/config.py
14
14
  sdk/crucial.py
15
15
  sdk/exceptions.py
16
16
  sdk/git_ops.py
17
+ sdk/github_ingest.py
17
18
  sdk/graphify.py
18
19
  sdk/local_exec.py
19
20
  sdk/platform_compat.py
@@ -23,7 +24,9 @@ sdk/sse.py
23
24
  sdk/test.py
24
25
  sdk/test_pipeline.py
25
26
  sdk/updater.py
27
+ sdk/vps_health.py
26
28
  sdk/agents/__init__.py
29
+ sdk/agents/activity.py
27
30
  sdk/agents/builder.py
28
31
  sdk/agents/chat.py
29
32
  sdk/agents/connectors.py
@@ -88,12 +91,15 @@ sdk/ui/static/runs.html
88
91
  sdk/ui/static/setup.html
89
92
  sdk/ui/static/styles.css
90
93
  sdk/ui/static/tickets.html
94
+ tests/test_agent_activity.py
91
95
  tests/test_agent_chat_features.py
92
96
  tests/test_agent_prompt_async.py
93
97
  tests/test_agent_prompt_models.py
94
98
  tests/test_agent_run_ticket.py
95
99
  tests/test_agents_connectors_builder.py
96
100
  tests/test_git_action.py
101
+ tests/test_github_ingest.py
97
102
  tests/test_health.py
98
103
  tests/test_otp_login.py
99
- tests/test_updater_extras.py
104
+ tests/test_updater_extras.py
105
+ tests/test_vps_health.py
@@ -19,3 +19,4 @@ Pillow>=10.0
19
19
  [ui]
20
20
  fastapi>=0.110
21
21
  uvicorn>=0.29
22
+ psutil>=5.9
@@ -31,6 +31,11 @@ process = [
31
31
  ui = [
32
32
  "fastapi>=0.110", # sdk/ui — control-panel web app
33
33
  "uvicorn>=0.29", # ASGI server for `agentos-ui`
34
+ "psutil>=5.9", # sdk/ui/health.py — CPU/RAM/disk/uptime for GET /api/ui/health
35
+ # AND the 2-hourly allocation PATCH (sdk/vps_health.py). The
36
+ # bootstrap installs `[ui]` only, so without it here every
37
+ # VPS reports "resources.available: False" and the health
38
+ # cron can only send status + IP.
34
39
  ]
35
40
  all = [
36
41
  "Pillow>=10.0",
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.154
3
+ Version: 1.5.0
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.4.158" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.5.0" # 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
@@ -85,6 +85,7 @@ from typing import TYPE_CHECKING, Any
85
85
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
86
86
  from apscheduler.triggers.cron import CronTrigger
87
87
 
88
+ from sdk.agents import activity
88
89
  from sdk.client import BackendClient
89
90
  from sdk.models.requests import OrchestrateRequest
90
91
 
@@ -354,6 +355,9 @@ async def _dispatch_agent_run(
354
355
 
355
356
  policy = manifest.approval.policy
356
357
  handled: set[str] = set() # session/task ids already round-tripped by this dispatch
358
+ # How this run was triggered — carried into the `agent_activities` line so the
359
+ # feed distinguishes a cron firing from a ticket trigger from a human "Run".
360
+ _activity_kind = "run-ticket" if auto_approve else ("ticket_label" if task else "scheduled")
357
361
 
358
362
  async def _on_status_update(status: str, state: dict) -> None:
359
363
  if status != "awaiting_approval":
@@ -402,11 +406,21 @@ async def _dispatch_agent_run(
402
406
  async def _tee(ev: dict) -> None:
403
407
  run_log.log_event(_log_path, ev)
404
408
 
409
+ _started = time.monotonic()
405
410
  local_result = await claude_code_engine.run(
406
411
  manifest, skill_text, mcp_config, extra_env=effective_secrets or None,
407
412
  on_event=_tee,
408
413
  )
409
414
  run_log.finish_run(_log_path, local_result)
415
+ # Local ledger + platform `agent_activities` feed. EVERY trigger path
416
+ # reaches this function (cron schedule, ticket label, on-demand
417
+ # run-on-ticket from the UI / CLI / desktop IPC), so a VPS agent run is
418
+ # recorded exactly once regardless of origin — see sdk.agents.activity.
419
+ await activity.record_run(
420
+ manifest.name, _activity_kind, local_result,
421
+ time.monotonic() - _started, client=client,
422
+ detail=f"task {task_id}" if task_id else "",
423
+ )
410
424
  outcome.update({
411
425
  "task_id": task_id,
412
426
  "result": local_result.get("result", ""),
@@ -418,6 +432,10 @@ async def _dispatch_agent_run(
418
432
  manifest.name, task_id, exc,
419
433
  )
420
434
  outcome["error"] = str(exc)
435
+ await activity.record_run(
436
+ manifest.name, _activity_kind, {"ok": False, "error": str(exc)},
437
+ 0.0, client=client, detail=f"task {task_id}" if task_id else "",
438
+ )
421
439
  return
422
440
 
423
441
  # FINALIZE — same field names sdk.local_exec.run_claude_local's result
@@ -0,0 +1,110 @@
1
+ """Agent-activity reporting — the ONE place a custom / department agent run is
2
+ recorded, for both sinks:
3
+
4
+ * the LOCAL usage ledger (``sdk.agents.usage`` → ``~/.agentos/usage.jsonl``),
5
+ which backs ``GET /api/ui/agents/usage`` and, through it, the AI backend
6
+ report's "VPS Agent Logs" section, and
7
+ * the PLATFORM activity feed — ``POST {ML_GATEWAY}/ml-api/agent-activity``
8
+ with ``{activity, agentId}``, which the ML backend writes into the Postgres
9
+ ``agent_activities`` table (docs/DATABASE_SCHEMA.md #3: ``user_id``,
10
+ ``activity``, ``agent_id``) and the AI backend reads back RBAC-scoped for
11
+ reporting + chat.
12
+
13
+ Every VPS-side agent run goes through here no matter how it was triggered — the
14
+ control panel (chat / run / run-on-ticket), an ``agentd`` cron schedule, an
15
+ ``agentd`` ticket-label trigger, the CLI, or the desktop IPC bridge. Before this
16
+ module only the control-panel paths emitted, so scheduled and ticket-triggered
17
+ runs never reached ``agent_activities``.
18
+
19
+ ``agent_activities.agent_id`` is an FK onto ``agents.id``, so only agents that
20
+ exist in the BACKEND emit — ``BackendClient.resolve_agent_id`` returns None for
21
+ a local-only manifest and the emit is skipped (the local ledger still records
22
+ it). Both custom and department-default agents resolve, so both are tracked.
23
+
24
+ Best-effort throughout: nothing in here raises into a run.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ from typing import TYPE_CHECKING, Any
30
+
31
+ if TYPE_CHECKING: # pragma: no cover — typing only
32
+ from sdk.client import BackendClient
33
+
34
+ logger = logging.getLogger("sdk.agents.activity")
35
+
36
+ # How a run reached us → the verb that opens the activity line. Keep these short:
37
+ # the feed is rendered as one line per row in reports and in chat answers.
38
+ _VERB = {
39
+ "chat": "Chatted with",
40
+ "run": "Ran",
41
+ "run-ticket": "Ran",
42
+ "scheduled": "Ran (schedule)",
43
+ "ticket_label": "Ran (ticket trigger)",
44
+ "cli": "Ran (cli)",
45
+ }
46
+
47
+
48
+ def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
49
+ detail: str = "") -> str:
50
+ """The human-readable ``activity`` string stored in ``agent_activities``."""
51
+ res = result or {}
52
+ verb = _VERB.get(kind, "Ran")
53
+ status = "ok" if res.get("ok") else "error"
54
+ line = f"{verb} agent '{name}' — {status}"
55
+ if duration_s:
56
+ line += f" ({round(float(duration_s), 1)}s)"
57
+ err = str(res.get("error") or "").strip()
58
+ if status == "error" and err:
59
+ line += f": {err[:200]}"
60
+ if detail:
61
+ line += f" — {detail}"
62
+ return line
63
+
64
+
65
+ async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
66
+ *, client: "BackendClient | None" = None, detail: str = "") -> bool:
67
+ """Emit ONE ``{activity, agentId}`` event for a finished agent run.
68
+
69
+ Returns True when the webhook accepted it, False otherwise (unknown agent,
70
+ no token, backend down) — never raises.
71
+ """
72
+ try:
73
+ if client is None:
74
+ from sdk.client import BackendClient
75
+ client = BackendClient.from_config()
76
+ agent_id = await client.resolve_agent_id(name)
77
+ if not agent_id: # local-only agent → not tracked
78
+ return False
79
+ return await client.log_agent_activity(
80
+ describe(name, kind, result, duration_s, detail), agent_id,
81
+ )
82
+ except Exception as exc: # noqa: BLE001 — activity emit must never break a run
83
+ logger.debug("activity emit failed agent=%s err=%s", name, exc)
84
+ return False
85
+
86
+
87
+ async def record_run(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
88
+ *, client: "BackendClient | None" = None, detail: str = "") -> None:
89
+ """Record a finished run to BOTH sinks: local ledger, then platform feed.
90
+
91
+ The ledger write happens first and independently of the webhook, so a
92
+ backend outage still leaves per-agent utilisation on the VPS (which is what
93
+ ``/api/ui/agents/usage`` and the report's "VPS Agent Logs" read).
94
+ """
95
+ res: dict[str, Any] = result or {}
96
+ try:
97
+ from sdk.agents import usage as usage_mod
98
+ usage_mod.record(
99
+ name,
100
+ kind=kind,
101
+ duration_s=duration_s,
102
+ input_tokens=res.get("input_tokens", 0) or 0,
103
+ output_tokens=res.get("output_tokens", 0) or 0,
104
+ cost_usd=res.get("cost_usd", 0.0) or 0.0,
105
+ ok=bool(res.get("ok")),
106
+ )
107
+ except Exception as exc: # noqa: BLE001 — ledger write must never break a run
108
+ logger.debug("activity ledger write failed agent=%s err=%s", name, exc)
109
+
110
+ await emit(name, kind, res, duration_s, client=client, detail=detail)
@@ -96,12 +96,16 @@ def main() -> None:
96
96
  _default_agents_command(rest)
97
97
  return
98
98
 
99
+ if noun == "github":
100
+ _github_command(rest)
101
+ return
102
+
99
103
  _print_usage()
100
104
  sys.exit(1)
101
105
 
102
106
 
103
107
  def _print_usage() -> None:
104
- print("usage: agentos <configure|agent|integration|default-agents|agentd> ...")
108
+ print("usage: agentos <configure|agent|integration|default-agents|agentd|github> ...")
105
109
  print()
106
110
  print(" agentos configure [--rotate]")
107
111
  print(" agentos agent <list|run NAME|chat NAME MSG|logs NAME|enable NAME|disable NAME|rm NAME>")
@@ -109,6 +113,7 @@ def _print_usage() -> None:
109
113
  print(" agentos integration <add PATH|test NAME|list|rm NAME>")
110
114
  print(" agentos default-agents list [--department-id UUID] # read-only; agents are created in the web app")
111
115
  print(" agentos agentd <start [--foreground]|status>")
116
+ print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
112
117
 
113
118
 
114
119
  # ── agent ────────────────────────────────────────────────────────────────
@@ -167,10 +172,21 @@ def _agent_command(args: list[str]) -> None:
167
172
 
168
173
 
169
174
  def _agent_chat(name: str, message: str) -> None:
175
+ import time as _time
176
+
177
+ from sdk.agents import activity
170
178
  from sdk.agents.chat import chat_with_agent
171
179
 
180
+ async def _run() -> dict:
181
+ started = _time.monotonic()
182
+ res = await chat_with_agent(name, message)
183
+ # Same two sinks as the control panel: local usage ledger + the platform
184
+ # `agent_activities` feed. A CLI chat is as much an agent run as a UI one.
185
+ await activity.record_run(name, "chat", res, _time.monotonic() - started)
186
+ return res
187
+
172
188
  try:
173
- result = asyncio.run(chat_with_agent(name, message))
189
+ result = asyncio.run(_run())
174
190
  except LookupError as exc:
175
191
  print(f"error: {exc}")
176
192
  sys.exit(1)
@@ -579,5 +595,60 @@ def _agentd_command(args: list[str]) -> None:
579
595
  sys.exit(1)
580
596
 
581
597
 
598
+ # ── github ───────────────────────────────────────────────────────────────
599
+
600
+ def _github_command(args: list[str]) -> None:
601
+ """`agentos github ingest [--days N]` / `agentos github status`.
602
+
603
+ The daily ingestion normally runs by itself inside `agentos-ui` (see
604
+ sdk.github_ingest). This is the manual handle: `ingest` runs one now,
605
+ `--days N` forces an N-day window — that is the backfill.
606
+ """
607
+ from sdk import github_ingest as gi
608
+
609
+ verb = args[0] if args else None
610
+
611
+ if verb == "status":
612
+ st = gi.status()
613
+ state = st.get("state") or {}
614
+ print(f"enabled : {st['enabled']}")
615
+ print(f"daily slot : {st['slot_utc']} UTC")
616
+ print(f"catch-up cap : {st['lookback_days']} days")
617
+ print(f"first-run seed : {st['backfill_days']} days"
618
+ f"{'' if st['backfilled'] else ' ← not run yet; the next run backfills'}")
619
+ print(f"last success : {state.get('last_success_at') or '(never)'}")
620
+ print(f"last status : {state.get('last_status') or '(none)'}")
621
+ if state.get("last_job_id"):
622
+ print(f"last job : {state['last_job_id']} {state.get('last_status_url', '')}")
623
+ if state.get("last_error"):
624
+ print(f"last error : {state['last_error']}")
625
+ return
626
+
627
+ if verb == "ingest":
628
+ days = None
629
+ if "--days" in args:
630
+ try:
631
+ days = int(args[args.index("--days") + 1])
632
+ except (IndexError, ValueError):
633
+ print("error: --days needs a whole number of days, e.g. --days 50")
634
+ sys.exit(1)
635
+ result = asyncio.run(gi.run_once(days=days))
636
+ status = result.get("status")
637
+ if status == "queued":
638
+ print(f"queued: PRs since {result['since']} job_id={result.get('job_id') or '(none)'}")
639
+ if result.get("status_url"):
640
+ print(f"poll : {result['status_url']} (on the AI backend)")
641
+ return
642
+ if status == "no_github_link":
643
+ print("nothing to do: this account has no GitHub connected.")
644
+ return
645
+ print(f"failed: {result.get('reason') or 'unknown error'}")
646
+ sys.exit(1)
647
+
648
+ print("usage: agentos github ingest [--days N] # N-day backfill; omit for the normal window")
649
+ print(" agentos github status")
650
+ sys.exit(1)
651
+
652
+
582
653
  if __name__ == "__main__":
583
654
  main()
@@ -763,13 +763,18 @@ class BackendClient:
763
763
  except Exception:
764
764
  return "pending"
765
765
 
766
- async def github_token(self) -> str | None:
767
- """GET /ml-api/github/token → the user's GitHub access token (gho_…).
768
-
769
- Used by the LOCAL agent to authenticate git/gh for clone/push/PR. The
770
- endpoint lives on the ML backend (devopsbot-be), authorised by the same
771
- co_ token. Best-effort: returns None on any failure so a run without a
772
- connected GitHub account still proceeds (clone/PR steps just won't auth).
766
+ async def github_token_record(self) -> dict | None:
767
+ """GET /ml-api/github/token → the caller's WHOLE GitHub link record.
768
+
769
+ Returns the ``data`` object — ``{id, userId, githubId, username,
770
+ accessToken, scope, …}`` — or None when this user has no GitHub link, the
771
+ token is rejected, or the backend is unreachable. Inherently per-user: the
772
+ ML backend resolves WHICH link to return from the ``co_`` token, so there
773
+ is no list-all form.
774
+
775
+ ``github_token()`` below is the accessToken-only view of this. The daily
776
+ PR-ingest cron (``sdk.github_ingest``) needs ``username`` too, which is why
777
+ the full record is exposed. Best-effort — never raises.
773
778
  """
774
779
  from sdk.config import BE_DEFAULT_URL
775
780
 
@@ -785,9 +790,72 @@ class BackendClient:
785
790
  resp = await client.get(url, headers=headers)
786
791
  if resp.status_code >= 400:
787
792
  return None
788
- data = (resp.json() or {}).get("data") or {}
789
- return data.get("accessToken") or None
790
- except Exception:
793
+ return (resp.json() or {}).get("data") or None
794
+ except Exception: # noqa: BLE001
795
+ return None
796
+
797
+ async def github_token(self) -> str | None:
798
+ """GET /ml-api/github/token → the user's GitHub access token (gho_…).
799
+
800
+ Used by the LOCAL agent to authenticate git/gh for clone/push/PR. The
801
+ endpoint lives on the ML backend (devopsbot-be), authorised by the same
802
+ co_ token. Best-effort: returns None on any failure so a run without a
803
+ connected GitHub account still proceeds (clone/PR steps just won't auth).
804
+ """
805
+ record = await self.github_token_record()
806
+ return (record or {}).get("accessToken") or None
807
+
808
+ async def ingest_github_prs(
809
+ self,
810
+ *,
811
+ github_username: str,
812
+ github_access_token: str,
813
+ since: str | None = None,
814
+ user_id: str = "",
815
+ ) -> dict | None:
816
+ """POST /api/v1/github/prs — queue PR ingestion for THIS user on the backend.
817
+
818
+ This is the daily GitHub ingestion trigger. It runs on the AI backend
819
+ (``devopsbot-ai.apiswagger.co.uk`` — ``self._url``, the same base every other
820
+ ``/api/v1`` call uses), NOT the ML gateway: the backend owns the LLM
821
+ summarise, the downstream daily-summary forward, and the Qdrant upsert.
822
+ The SDK only decides WHEN, because only this machine knows this user's
823
+ schedule and its own last-success watermark.
824
+
825
+ ``since`` is an ISO date (``YYYY-MM-DD``) → the backend's
826
+ ``GetUserPRsRequest.date_from``. Omitted means "today", which is what a
827
+ never-missed daily run wants; ``sdk.github_ingest`` sends the catch-up
828
+ watermark instead so a machine that was off does not silently lose days.
829
+
830
+ The credentials are sent in the body because that is what the endpoint
831
+ accepts today. The ``Authorization: co_`` header goes with every request
832
+ regardless, so once the backend resolves the GitHub link server-side from
833
+ that token (see docs/GITHUB_SCHEDULING_SDK_MIGRATION.md §3 step 1) it can
834
+ ignore the body fields without breaking this client.
835
+
836
+ Returns the response JSON (``{job_id, …}``) or None on any failure —
837
+ never raises into the cron.
838
+ """
839
+ payload = [{
840
+ "userId": user_id or self._user_id_from_token(),
841
+ "githubUsername": github_username,
842
+ "githubAccessToken": github_access_token,
843
+ "apiTokenHash": self._token, # downstream daily-summary Authorization
844
+ }]
845
+ if since:
846
+ payload[0]["date_from"] = since
847
+ try:
848
+ async with httpx.AsyncClient(timeout=60.0) as client:
849
+ resp = await client.post(
850
+ self._url("/api/v1/github/prs"), json=payload, headers=self._headers,
851
+ )
852
+ if resp.status_code >= 400:
853
+ logger.warning("client.ingest_github_prs failed status=%s body=%s",
854
+ resp.status_code, resp.text[:200])
855
+ return None
856
+ return resp.json()
857
+ except Exception as exc: # noqa: BLE001
858
+ logger.warning("client.ingest_github_prs error err=%s", exc)
791
859
  return None
792
860
 
793
861
  async def move_to_backlog(self, task_id: str) -> bool:
@@ -1093,6 +1161,35 @@ class BackendClient:
1093
1161
  _AGENT_ID_CACHE[nm] = "" # negative-cache: not a backend agent
1094
1162
  return None
1095
1163
 
1164
+ async def report_vps_allocation(self, payload: dict) -> bool:
1165
+ """PATCH ``{ML_GATEWAY_URL}/ml-api/contabo/allocation`` — this machine's health.
1166
+
1167
+ The allocation row is resolved SERVER-side from the ``co_`` token (one row
1168
+ per user — ``contabo_allocation``, docs/DATABASE_SCHEMA.md #18), so the body
1169
+ carries metrics only: ``status``, ``cpuUsage``, ``ramUsage``, ``usedRam``,
1170
+ ``totalRam``, ``diskUsage``, ``totalDisk``, ``bandwidthUsed``, ``ipAddress``,
1171
+ ``uptimeSeconds``, ``lastPingedAt``.
1172
+
1173
+ Driven by the SDK-side health cron (``sdk.vps_health``) — deliberately NOT by
1174
+ the AI backend, so VPS liveness reporting does not depend on that service.
1175
+ Best-effort: never raises; a failed ping is logged and the next tick retries.
1176
+ """
1177
+ from sdk.config import ML_GATEWAY_URL
1178
+
1179
+ url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/contabo/allocation"
1180
+ headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
1181
+ try:
1182
+ async with httpx.AsyncClient(timeout=20.0) as client:
1183
+ resp = await client.patch(url, json=payload, headers=headers)
1184
+ if resp.status_code >= 400:
1185
+ logger.warning("client.report_vps_allocation failed status=%s body=%s",
1186
+ resp.status_code, resp.text[:200])
1187
+ return False
1188
+ return True
1189
+ except Exception as exc: # noqa: BLE001
1190
+ logger.warning("client.report_vps_allocation error err=%s", exc)
1191
+ return False
1192
+
1096
1193
  async def notify_whatsapp(
1097
1194
  self,
1098
1195
  task_id: str,