devops-bot-sdk 1.4.13__tar.gz → 1.4.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 (38) hide show
  1. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/ipc/handlers.py +23 -2
  7. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/local_exec.py +97 -6
  8. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/README.md +0 -0
  9. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  10. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  11. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  12. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/requires.txt +0 -0
  13. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  14. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/files.py +0 -0
  16. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/process.py +0 -0
  17. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/screenshot.py +0 -0
  18. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/config.py +0 -0
  19. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/crucial.py +0 -0
  20. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/exceptions.py +0 -0
  21. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/git_ops.py +0 -0
  22. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/graphify.py +0 -0
  23. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/hooks/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/hooks/crucial_guard.py +0 -0
  25. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/ipc/__init__.py +0 -0
  26. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/ipc/electron_bridge.py +0 -0
  27. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.13
3
+ Version: 1.4.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.4.13
3
+ Version: 1.4.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.4.13"
7
+ version = "1.4.14"
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.13
3
+ Version: 1.4.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.4.13"
33
+ __version__ = "1.4.14"
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.13"
39
+ SDK_VERSION = "1.4.14"
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
@@ -1202,8 +1202,13 @@ _PATH_ROOTS = (
1202
1202
  "home|root|var|opt|srv|usr|mnt|media|data|app|apps|"
1203
1203
  "project|projects|workspace|workspaces|repos|code|tmp|Users"
1204
1204
  )
1205
- _ABS_PATH_RE = re.compile(rf"(?<![\w.])/(?:{_PATH_ROOTS})(?:/[A-Za-z0-9._\-]+)+")
1206
- _HOME_PATH_RE = re.compile(r"(?<![\w.])~(?:/[A-Za-z0-9._\-]+)+")
1205
+ # Unix / ~-home paths. Folder names may contain SPACES too (e.g.
1206
+ # "/home/me/BA activities/repo"), so segments allow spaces and the path runs to
1207
+ # end-of-line. The root is anchored to a known top-level dir to limit false
1208
+ # positives in prose; for an inline path followed by other words on the SAME
1209
+ # line, quote it (see _QUOTED_PATH_RE) to avoid grabbing the trailing text.
1210
+ _ABS_PATH_RE = re.compile(rf"(?<![\w.])/(?:{_PATH_ROOTS})(?:/[A-Za-z0-9._\- ]+)+")
1211
+ _HOME_PATH_RE = re.compile(r"(?<![\w.])~(?:/[A-Za-z0-9._\- ]+)+")
1207
1212
  # Windows path. Folder names may contain SPACES (e.g. "BA activities clone"), so a
1208
1213
  # segment is any run of valid path chars (everything except \ / : * ? " < > | and
1209
1214
  # newline) up to the next separator — it must NOT stop at the first space.
@@ -1429,6 +1434,19 @@ def _extract_repo_targets(text: str | None) -> list[dict]:
1429
1434
  return targets
1430
1435
 
1431
1436
 
1437
+ def _task_created_key(task: dict):
1438
+ """Sortable Jira creation time for a raw task (e.g. ``createdAt`` =
1439
+ "2026-06-22T15:07:52.452Z"). Used to process tickets oldest-first. A
1440
+ missing/invalid timestamp sorts LAST so well-formed, earlier tickets win."""
1441
+ from datetime import datetime, timezone
1442
+
1443
+ raw = str(task.get("createdAt") or task.get("created_at") or "").strip()
1444
+ try:
1445
+ return datetime.fromisoformat(raw.replace("Z", "+00:00"))
1446
+ except (ValueError, TypeError):
1447
+ return datetime.max.replace(tzinfo=timezone.utc)
1448
+
1449
+
1432
1450
  def _resolve_local_project_path(task: dict, payload: dict) -> str:
1433
1451
  """Pick the LOCAL directory to write a task's code into (on the user's box).
1434
1452
 
@@ -1579,6 +1597,9 @@ async def handle_orchestrate_auto_local_run(
1579
1597
 
1580
1598
  # RAW /tasks-ml records carry agentMode / jiraIssueKey / humanInstructions.
1581
1599
  tasks = await client.list_todo_tasks_raw(status="To Do")
1600
+ # Process oldest-created first (FIFO by Jira `createdAt`) so the ticket
1601
+ # raised earliest is started first. Unknown/invalid timestamps sort last.
1602
+ tasks = sorted(tasks, key=_task_created_key)
1582
1603
 
1583
1604
  await send(Envelope(
1584
1605
  type="pipeline_started", thread_id=thread_id,
@@ -22,6 +22,7 @@ import json
22
22
  import os
23
23
  import re
24
24
  import shutil
25
+ from datetime import datetime
25
26
  from pathlib import Path
26
27
  from typing import Any, Awaitable, Callable
27
28
 
@@ -92,6 +93,70 @@ def _parse_reset_epoch(text: str | None) -> int | None:
92
93
  return None
93
94
 
94
95
 
96
+ # ── Per-run claude log file (on the user's machine, local-timezone stamps) ──
97
+ # Every local claude run writes a readable, timestamped transcript so a box can
98
+ # be debugged after the fact. Dir is AGENTOS_LOG_DIR or ~/.agentos/logs.
99
+
100
+ def _open_run_log(project_path: str, resume_session_id: str | None):
101
+ """Open a per-run log file under the local logs dir. Returns (file, path) or
102
+ (None, None). Best-effort — never breaks the run."""
103
+ try:
104
+ base = os.getenv("AGENTOS_LOG_DIR") or str(Path.home() / ".agentos" / "logs")
105
+ d = Path(base).expanduser()
106
+ d.mkdir(parents=True, exist_ok=True)
107
+ now = datetime.now().astimezone() # local system tz
108
+ proj = Path(project_path).name or "run"
109
+ # Filenames are filesystem-safe (no ':'); stamp encodes the local tz offset.
110
+ fname = f"claude_{proj}_{now.strftime('%Y-%m-%d_%H-%M-%S%z')}.log"
111
+ path = d / fname
112
+ f = open(path, "a", encoding="utf-8")
113
+ return f, str(path)
114
+ except Exception: # noqa: BLE001
115
+ return None, None
116
+
117
+
118
+ def _logln(f, msg: str) -> None:
119
+ """Append a local-timezone timestamped line. Best-effort."""
120
+ if not f:
121
+ return
122
+ try:
123
+ ts = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
124
+ f.write(f"[{ts}] {msg}\n")
125
+ f.flush()
126
+ except Exception: # noqa: BLE001
127
+ pass
128
+
129
+
130
+ def _event_logline(event: dict) -> str | None:
131
+ """A compact, human-readable line for one stream-json event (or None to skip)."""
132
+ def _short(s, n=200):
133
+ return " ".join(str(s or "").split())[:n]
134
+
135
+ t = event.get("type")
136
+ if t == "system":
137
+ return f"SYSTEM {event.get('subtype', '')}".rstrip()
138
+ if t == "assistant":
139
+ parts: list[str] = []
140
+ for c in event.get("message", {}).get("content", []):
141
+ if c.get("type") == "text" and c.get("text", "").strip():
142
+ parts.append("TEXT: " + _short(c["text"], 300))
143
+ elif c.get("type") == "tool_use":
144
+ inp = c.get("input") or {}
145
+ detail = (inp.get("command") or inp.get("file_path") or inp.get("pattern")
146
+ or inp.get("url") or inp.get("description") or "")
147
+ parts.append(f"TOOL {c.get('name', 'tool')}" + (f" → {_short(detail)}" if detail else ""))
148
+ return " | ".join(parts) if parts else None
149
+ if t == "user":
150
+ tr = event.get("tool_use_result") or event.get("message", {})
151
+ if isinstance(tr, dict) and tr.get("is_error"):
152
+ return "TOOL_ERROR: " + _short(tr.get("content") or tr.get("stderr") or "", 300)
153
+ return None
154
+ if t == "result":
155
+ return (f"RESULT subtype={event.get('subtype')} is_error={event.get('is_error')} "
156
+ f"cost=${event.get('total_cost_usd', 0)}")
157
+ return None
158
+
159
+
95
160
  def _ctx_exceeded(text: str | None) -> bool:
96
161
  low = (text or "").lower()
97
162
  return any(m in low for m in _CONTEXT_MARKERS)
@@ -218,6 +283,26 @@ async def run_claude_local(
218
283
  started = _time.monotonic()
219
284
  stall = {"timed_out": False, "reason": ""}
220
285
 
286
+ # Per-run local log (timezone-stamped) for after-the-fact debugging.
287
+ logf, logpath = _open_run_log(str(path), resume_session_id)
288
+ _logln(logf, f"START claude run | project={path} | resume={resume_session_id or '-'} "
289
+ f"| idle_timeout={idle_timeout:.0f}s | cap={timeout:.0f}s")
290
+ _logln(logf, "PROMPT: " + " ".join((prompt or "").split())[:600])
291
+
292
+ def _finish(result: dict) -> dict:
293
+ """Write the run footer, close the log, and return the result."""
294
+ _logln(logf, f"END ok={result.get('ok')} error={result.get('error')} "
295
+ f"tokens={result.get('total_tokens')} cost=${result.get('cost_usd')} "
296
+ f"files_changed={len(result.get('files_changed') or [])} "
297
+ f"limit_exceeded={result.get('limit_exceeded')} "
298
+ f"session={result.get('session_id')}")
299
+ if logf:
300
+ try:
301
+ logf.close()
302
+ except Exception: # noqa: BLE001
303
+ pass
304
+ return result
305
+
221
306
  async def _drain_stdout() -> None:
222
307
  nonlocal result_event, session_id
223
308
  assert proc.stdout is not None
@@ -250,6 +335,10 @@ async def run_claude_local(
250
335
  except json.JSONDecodeError:
251
336
  continue
252
337
 
338
+ _ll = _event_logline(event)
339
+ if _ll:
340
+ _logln(logf, _ll)
341
+
253
342
  # Capture the claude session id (emitted on init + result events) so
254
343
  # the caller can --resume this exact session later.
255
344
  if event.get("session_id"):
@@ -284,23 +373,25 @@ async def run_claude_local(
284
373
  proc.kill()
285
374
  except ProcessLookupError:
286
375
  pass
287
- return {
376
+ return _finish({
288
377
  "ok": False, "result": "", "error": f"claude CLI timed out — {stall['reason']}",
289
378
  "input_tokens": 0, "output_tokens": 0, "total_tokens": 0,
290
379
  "cost_usd": 0.0, "files_changed": files_changed,
291
380
  "session_id": session_id, "context_exceeded": False,
292
- }
381
+ })
293
382
 
294
383
  if result_event is None:
295
384
  stderr = (await proc.stderr.read()).decode("utf-8", errors="replace") if proc.stderr else ""
296
385
  err = stderr.strip()[:500] or "no result event from claude CLI"
297
- return {
386
+ if stderr.strip():
387
+ _logln(logf, "STDERR: " + " ".join(stderr.split())[:600])
388
+ return _finish({
298
389
  "ok": False, "result": "", "error": err,
299
390
  "input_tokens": 0, "output_tokens": 0, "total_tokens": 0,
300
391
  "cost_usd": 0.0, "files_changed": files_changed,
301
392
  "session_id": session_id, "context_exceeded": _ctx_exceeded(err),
302
393
  "limit_exceeded": _is_limit(err), "limit_reset_epoch": _parse_reset_epoch(err),
303
- }
394
+ })
304
395
 
305
396
  usage = result_event.get("usage") or {}
306
397
  in_tok = int(usage.get("input_tokens", 0) or 0)
@@ -321,7 +412,7 @@ async def run_claude_local(
321
412
  elif limit_hit:
322
413
  err_text = result_text or "claude usage limit reached"
323
414
 
324
- return {
415
+ return _finish({
325
416
  "ok": (not is_error) and (not limit_hit),
326
417
  "result": result_text,
327
418
  "input_tokens": in_tok,
@@ -334,4 +425,4 @@ async def run_claude_local(
334
425
  "context_exceeded": is_error and _ctx_exceeded(err_text),
335
426
  "limit_exceeded": limit_hit,
336
427
  "limit_reset_epoch": reset_epoch,
337
- }
428
+ })