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.
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/client.py +1 -1
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/ipc/handlers.py +23 -2
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/local_exec.py +97 -6
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/README.md +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
version = "1.4.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
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
|
+
})
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.13 → devops_bot_sdk-1.4.14}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|