devops-bot-sdk 1.4.1__tar.gz → 1.4.3__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 (36) hide show
  1. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/git_ops.py +95 -5
  7. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/ipc/handlers.py +8 -3
  8. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/local_exec.py +16 -1
  9. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/README.md +0 -0
  10. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  11. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  12. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  13. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/requires.txt +0 -0
  14. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  15. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/__init__.py +0 -0
  16. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/files.py +0 -0
  17. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/process.py +0 -0
  18. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/screenshot.py +0 -0
  19. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/config.py +0 -0
  20. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/crucial.py +0 -0
  21. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/exceptions.py +0 -0
  22. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/graphify.py +0 -0
  23. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/ipc/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/ipc/electron_bridge.py +0 -0
  25. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/__init__.py +0 -0
  26. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/envelope.py +0 -0
  27. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/requests.py +0 -0
  28. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/responses.py +0 -0
  29. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/snapshots.py +0 -0
  30. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/py.typed +0 -0
  31. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/run_auto.py +0 -0
  32. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/sse.py +0 -0
  33. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/test.py +0 -0
  34. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/test_pipeline.py +0 -0
  35. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/updater.py +0 -0
  36. {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.1
3
+ Version: 1.4.3
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.4.1
3
+ Version: 1.4.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.4.1"
7
+ version = "1.4.3"
8
8
  description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.1
3
+ Version: 1.4.3
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.1"
33
+ __version__ = "1.4.3"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -36,7 +36,7 @@ from sdk.sse import _check_status, stream_with_reconnect
36
36
 
37
37
  logger = logging.getLogger(__name__)
38
38
 
39
- SDK_VERSION = "1.4.1"
39
+ SDK_VERSION = "1.4.3"
40
40
  _POLL_INTERVAL = 3.0
41
41
  _POLL_TIMEOUT = 600.0
42
42
  _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
@@ -9,11 +9,14 @@ names). Everything here is best-effort and never raises into the run.
9
9
  from __future__ import annotations
10
10
 
11
11
  import asyncio
12
+ import logging
12
13
  import os
13
14
  import re
14
15
  import shutil
15
16
  from pathlib import Path
16
17
 
18
+ logger = logging.getLogger(__name__)
19
+
17
20
 
18
21
  def slugify(text: str | None, max_len: int = 40) -> str:
19
22
  s = re.sub(r"[^a-z0-9]+", "-", (text or "").lower()).strip("-")
@@ -92,13 +95,72 @@ async def start_branch(
92
95
  return {"branch": branch, "base": base}
93
96
 
94
97
 
98
+ async def _remote_owner_repo(path: str, env: dict) -> tuple[str, str] | None:
99
+ """Parse origin's URL into (owner, repo). Handles https and ssh forms."""
100
+ rc, out, _ = await _run(["git", "remote", "get-url", "origin"], path, env)
101
+ if rc != 0 or not out:
102
+ return None
103
+ m = re.search(r"github\.com[:/]+([^/]+)/(.+?)(?:\.git)?/?$", out.strip())
104
+ if not m:
105
+ return None
106
+ return m.group(1), m.group(2)
107
+
108
+
109
+ async def _create_pr_via_api(
110
+ owner: str, repo: str, base: str, head: str, title: str, body: str, token: str,
111
+ ) -> str | None:
112
+ """Open a PR through the GitHub REST API (no `gh` CLI needed).
113
+
114
+ The token from /ml-api/github/token carries `repo` scope, so it can both push
115
+ and open PRs. Returns the PR html_url, or the existing open PR's url when one
116
+ already exists for this head (422). Returns None (and logs) on any other error.
117
+ """
118
+ import httpx
119
+
120
+ api = f"https://api.github.com/repos/{owner}/{repo}/pulls"
121
+ headers = {
122
+ "Authorization": f"token {token}",
123
+ "Accept": "application/vnd.github+json",
124
+ "X-GitHub-Api-Version": "2022-11-28",
125
+ }
126
+ try:
127
+ async with httpx.AsyncClient(timeout=30.0) as c:
128
+ resp = await c.post(
129
+ api, headers=headers,
130
+ json={"title": title, "head": head, "base": base, "body": body},
131
+ )
132
+ if resp.status_code == 201:
133
+ return resp.json().get("html_url")
134
+ # 422 = a PR already exists for this head (or validation issue). Try to
135
+ # surface the existing open PR rather than reporting "no PR".
136
+ if resp.status_code == 422:
137
+ async with httpx.AsyncClient(timeout=30.0) as c:
138
+ r2 = await c.get(
139
+ api, headers=headers,
140
+ params={"head": f"{owner}:{head}", "state": "open"},
141
+ )
142
+ if r2.status_code == 200 and r2.json():
143
+ return r2.json()[0].get("html_url")
144
+ logger.warning(
145
+ "git_ops.pr_api_failed owner=%s repo=%s head=%s status=%s body=%s",
146
+ owner, repo, head, resp.status_code, resp.text[:300],
147
+ )
148
+ except Exception as exc: # noqa: BLE001
149
+ logger.warning("git_ops.pr_api_error head=%s error=%s", head, exc)
150
+ return None
151
+
152
+
95
153
  async def finish_pr(
96
154
  project_path: str, branch: str, base: str, title: str, body: str,
97
155
  github_token: str | None,
98
156
  ) -> dict:
99
157
  """Stage all, commit, push the branch, open a PR. Best-effort.
100
158
 
101
- Returns {"pushed": bool, "pr_url": str | None}.
159
+ PR creation prefers the GitHub REST API (works with just the token — no `gh`
160
+ install required) and falls back to the `gh` CLI. Every failure path is
161
+ logged so a missing PR is diagnosable instead of silent.
162
+
163
+ Returns {"pushed": bool, "pr_url": str | None, "pr_error": str | None}.
102
164
  """
103
165
  path = str(Path(project_path).expanduser())
104
166
  env = _git_env(github_token)
@@ -107,18 +169,46 @@ async def finish_pr(
107
169
  # commit — no-op (non-zero) when there's nothing staged; ignore that case.
108
170
  await _run(["git", "commit", "-m", title], path, env)
109
171
 
110
- rc, _, _ = await _run(
172
+ rc, _, push_err = await _run(
111
173
  ["git", "push", "-u", "origin", branch, "--force-with-lease"],
112
174
  path, env, timeout=300.0,
113
175
  )
114
176
  pushed = rc == 0
177
+ if not pushed:
178
+ logger.warning("git_ops.push_failed branch=%s error=%s", branch, push_err[:300])
179
+ return {"pushed": False, "pr_url": None, "pr_error": f"push failed: {push_err[:200]}"}
180
+
115
181
  pr_url: str | None = None
116
- if pushed and shutil.which("gh"):
117
- rc, out, _ = await _run(
182
+ pr_error: str | None = None
183
+
184
+ # 1. Preferred: REST API (no `gh` dependency).
185
+ owner_repo = await _remote_owner_repo(path, env)
186
+ if github_token and owner_repo:
187
+ owner, repo = owner_repo
188
+ pr_url = await _create_pr_via_api(owner, repo, base, branch, title, body, github_token)
189
+
190
+ # 2. Fallback: gh CLI, if installed.
191
+ if not pr_url and shutil.which("gh"):
192
+ rc, out, gh_err = await _run(
118
193
  ["gh", "pr", "create", "--base", base, "--head", branch,
119
194
  "--title", title, "--body", body],
120
195
  path, env, timeout=120.0,
121
196
  )
122
197
  if rc == 0 and out:
123
198
  pr_url = out.strip().splitlines()[-1]
124
- return {"pushed": pushed, "pr_url": pr_url}
199
+ else:
200
+ pr_error = f"gh pr create failed: {gh_err[:200]}"
201
+ logger.warning("git_ops.gh_pr_failed branch=%s error=%s", branch, gh_err[:300])
202
+
203
+ if not pr_url and pr_error is None:
204
+ # Pushed, but no PR and no specific error captured above.
205
+ pr_error = (
206
+ "no PR created — REST API returned no URL"
207
+ if (github_token and owner_repo)
208
+ else "no PR created — missing GitHub token or unrecognized origin remote"
209
+ if not shutil.which("gh")
210
+ else "no PR created"
211
+ )
212
+ logger.warning("git_ops.no_pr branch=%s reason=%s", branch, pr_error)
213
+
214
+ return {"pushed": pushed, "pr_url": pr_url, "pr_error": pr_error}
@@ -521,15 +521,20 @@ async def handle_pipeline_approve(
521
521
  body=(result.get("result") or "")[:4000],
522
522
  github_token=gh_token,
523
523
  )
524
- step = f"Pushed {branch_info['branch']}"
524
+ if pr.get("pushed"):
525
+ step = f"Pushed {branch_info['branch']}"
526
+ else:
527
+ step = f"Push failed for {branch_info['branch']}"
525
528
  if pr.get("pr_url"):
526
529
  step += f" · PR: {pr['pr_url']}"
530
+ elif pr.get("pr_error"):
531
+ step += f" · PR not created: {pr['pr_error']}"
527
532
  await send(Envelope(
528
533
  type="step_update", thread_id=thread_id,
529
534
  data={"status": "running", "current_step": step},
530
535
  ).model_dump())
531
- except Exception: # noqa: BLE001
532
- pass
536
+ except Exception as exc: # noqa: BLE001
537
+ logger.warning("local_run.finish_pr_failed", error=str(exc))
533
538
 
534
539
  # ── Notify the user on WhatsApp for a hard error or context overflow ──
535
540
  # The backend forwards `message` to WhatsApp (POST /approvals-ml). Best-effort.
@@ -32,6 +32,14 @@ DEFAULT_ALLOWED_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"]
32
32
  # Hard ceiling so a runaway agent can't hang the sidecar forever.
33
33
  _DEFAULT_TIMEOUT_S = 1800.0
34
34
 
35
+ # StreamReader buffer for the claude CLI's stdout. `--output-format stream-json`
36
+ # emits one JSON object per line, and a single event can embed a whole file's
37
+ # contents (e.g. a Read tool_use_result), easily exceeding asyncio's default
38
+ # 64 KB line limit — which makes readline() raise LimitOverrunError ("Separator
39
+ # is found, but chunk is longer than limit"). 64 MB comfortably covers large
40
+ # file reads/diffs in one event.
41
+ _STREAM_LIMIT_BYTES = 64 * 1024 * 1024
42
+
35
43
  # Markers that indicate the model ran out of context window.
36
44
  _CONTEXT_MARKERS = (
37
45
  "context window", "context length", "maximum context", "too many tokens",
@@ -135,6 +143,7 @@ async def run_claude_local(
135
143
  stdout=asyncio.subprocess.PIPE,
136
144
  stderr=asyncio.subprocess.PIPE,
137
145
  env=env,
146
+ limit=_STREAM_LIMIT_BYTES,
138
147
  )
139
148
 
140
149
  result_event: dict | None = None
@@ -145,7 +154,13 @@ async def run_claude_local(
145
154
  nonlocal result_event, session_id
146
155
  assert proc.stdout is not None
147
156
  while True:
148
- raw = await proc.stdout.readline()
157
+ try:
158
+ raw = await proc.stdout.readline()
159
+ except ValueError:
160
+ # Line exceeded even _STREAM_LIMIT_BYTES. readline() has already
161
+ # advanced past the oversized chunk, so we can safely skip this
162
+ # one event and keep streaming the rest rather than aborting.
163
+ continue
149
164
  if not raw:
150
165
  break
151
166
  line = raw.decode("utf-8", errors="replace").strip()
File without changes
File without changes