agentpool-cli 0.1.6__tar.gz → 0.1.7__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.
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/CHANGELOG.md +9 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/PKG-INFO +1 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/pyproject.toml +1 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/server.json +2 -2
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/__init__.py +1 -1
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/git_worktree.py +7 -18
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/runtimes/tmux.py +27 -31
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/_common.py +3 -29
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/codex.py +3 -5
- agentpool_cli-0.1.7/src/agentpool/utils.py +139 -0
- agentpool_cli-0.1.7/tests/unit/test_subprocess_safety.py +143 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_usage_probes.py +5 -13
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/uv.lock +1 -1
- agentpool_cli-0.1.6/src/agentpool/utils.py +0 -59
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.cursor/mcp.json.example +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.github/CODEOWNERS +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.github/ISSUE_TEMPLATE/provider_probe.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.github/dependabot.yml +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.github/workflows/ci.yml +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.github/workflows/release.yml +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.gitignore +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/.mcp.json.example +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/AGENTS.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/CONTRIBUTING.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/LICENSE +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/README.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/SECURITY.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/agent-cli-and-mcp.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/agentpool-skill.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/architecture.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/examples/README.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/examples.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/install.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/mcp-clients.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/mcp-tools.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/model-catalog.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/onboarding.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/provider-adapters.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/provider-lifecycle-matrix.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/quickstart.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/release.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/security.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/setup-claude-code.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/setup-codex.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/setup-copilot.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/setup-cursor-cli.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/setup-cursor.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/setup-devin.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/setup-droid.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/stats.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/usage-detection.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/docs/usage-probe-matrix.md +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/scripts/install.sh +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/agent_io.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/artifacts.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/cli.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/config.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/event_detection.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_approval_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_common.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_completed_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_idle_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_limit_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_patch_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_question_agent.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/mcp/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/mcp/resources.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/mcp/tools.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/mcp_server.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/models.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/onboarding.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/policy.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/preferences.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/provider_model_catalog.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/providers/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/providers/base.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/providers/registry.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/redaction.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/runtimes/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/runtimes/base.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/session_manager.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/stats/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/stats/card.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/stats/compute.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/stats/queries.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/stats/render.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/stats/window.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/store.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/__init__.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/ccusage.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/claude.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/codexbar.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/combine.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/copilot.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/devin.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/parsers.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/probes.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/provider_parsers.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/usage/summary.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/fixtures/provider_model_catalog_golden.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/fixtures/stats_seed.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/fixtures/usage/claude_usage.txt +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/fixtures/usage/codex_rate_limits.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/fixtures/usage/copilot_user.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/fixtures/usage/devin_plan_status.json +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/integration/test_fake_tmux_flow.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_agent_io.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_cli.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_event_policy.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_mcp_surface.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_mcp_tools.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_models_config_store.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_onboarding.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_redaction.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_stats_cli.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_stats_mcp.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_stats_window.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_usage_provider_parsers.py +0 -0
- {agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/unit/test_usage_summary_enrichment.py +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.7 - 2026-06-01
|
|
6
|
+
|
|
7
|
+
- Centralize non-interactive subprocess execution behind terminal-safe helpers.
|
|
8
|
+
Git, tmux client operations, provider detection, Cursor status checks, Codex
|
|
9
|
+
app-server probes, and external usage helpers now detach from the host TTY by
|
|
10
|
+
default.
|
|
11
|
+
- Add a subprocess-safety regression test so new product code cannot introduce
|
|
12
|
+
raw `subprocess.run`/`Popen` calls outside the shared utility.
|
|
13
|
+
|
|
5
14
|
## 0.1.6 - 2026-06-01
|
|
6
15
|
|
|
7
16
|
- Run external usage helper commands in isolated, non-interactive subprocess
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentpool-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Make full use of every coding-agent subscription you pay for: a local CLI + MCP server that surfaces live usage limits and offloads work to providers with headroom.
|
|
5
5
|
Author: AgentPool contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentpool-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
description = "Make full use of every coding-agent subscription you pay for: a local CLI + MCP server that surfaces live usage limits and offloads work to providers with headroom."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.sidduHERE/agentpool",
|
|
4
4
|
"title": "AgentPool",
|
|
5
5
|
"description": "See each coding-agent subscription's live limits and offload work to one with headroom.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.7",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/sidduHERE/agentpool",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "pypi",
|
|
15
15
|
"identifier": "agentpool-cli",
|
|
16
|
-
"version": "0.1.
|
|
16
|
+
"version": "0.1.7",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
2
|
from pathlib import Path
|
|
5
3
|
|
|
6
4
|
from agentpool.models import ToolError
|
|
@@ -41,12 +39,9 @@ def create_worktree(repo_path: Path, provider_id: str, session_id: str) -> Path:
|
|
|
41
39
|
parent.mkdir(parents=True, exist_ok=True)
|
|
42
40
|
worktree_path = parent / session_id
|
|
43
41
|
branch = agentpool_branch(provider_id, session_id)
|
|
44
|
-
proc =
|
|
42
|
+
proc = run_capture(
|
|
45
43
|
["git", "worktree", "add", "-b", branch, str(worktree_path)],
|
|
46
|
-
cwd=
|
|
47
|
-
text=True,
|
|
48
|
-
capture_output=True,
|
|
49
|
-
check=False,
|
|
44
|
+
cwd=repo_path,
|
|
50
45
|
)
|
|
51
46
|
if proc.returncode != 0:
|
|
52
47
|
raise ToolError(
|
|
@@ -66,12 +61,9 @@ def delete_agentpool_branch(repo_path: Path, provider_id: str, session_id: str)
|
|
|
66
61
|
if not is_git_repo(repo_path):
|
|
67
62
|
return {"deleted": False, "reason": "not_git_repo"}
|
|
68
63
|
branch = agentpool_branch(provider_id, session_id)
|
|
69
|
-
proc =
|
|
64
|
+
proc = run_capture(
|
|
70
65
|
["git", "branch", "-D", branch],
|
|
71
|
-
cwd=
|
|
72
|
-
text=True,
|
|
73
|
-
capture_output=True,
|
|
74
|
-
check=False,
|
|
66
|
+
cwd=repo_path,
|
|
75
67
|
)
|
|
76
68
|
if proc.returncode != 0:
|
|
77
69
|
return {"deleted": False, "branch": branch, "stderr": proc.stderr.strip()}
|
|
@@ -81,12 +73,9 @@ def delete_agentpool_branch(repo_path: Path, provider_id: str, session_id: str)
|
|
|
81
73
|
def list_agentpool_worktrees(repo_path: Path) -> list[dict[str, str | bool]]:
|
|
82
74
|
if not is_git_repo(repo_path):
|
|
83
75
|
return []
|
|
84
|
-
proc =
|
|
76
|
+
proc = run_capture(
|
|
85
77
|
["git", "worktree", "list", "--porcelain"],
|
|
86
|
-
cwd=
|
|
87
|
-
text=True,
|
|
88
|
-
capture_output=True,
|
|
89
|
-
check=False,
|
|
78
|
+
cwd=repo_path,
|
|
90
79
|
)
|
|
91
80
|
if proc.returncode != 0:
|
|
92
81
|
return []
|
|
@@ -128,7 +117,7 @@ def cleanup_worktree(repo_path: Path, worktree_path: Path, force: bool = False)
|
|
|
128
117
|
if force:
|
|
129
118
|
args.append("--force")
|
|
130
119
|
args.append(str(worktree_path))
|
|
131
|
-
proc =
|
|
120
|
+
proc = run_capture(args, cwd=repo_path)
|
|
132
121
|
if proc.returncode != 0:
|
|
133
122
|
raise ToolError(
|
|
134
123
|
"WORKTREE_CLEANUP_FAILED",
|
|
@@ -3,10 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import signal
|
|
5
5
|
import shutil
|
|
6
|
-
import subprocess
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
from agentpool.models import RuntimeKind, TmuxSessionRef, ToolError
|
|
9
|
+
from agentpool.utils import run_capture
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TmuxRuntime:
|
|
@@ -28,29 +28,22 @@ class TmuxRuntime:
|
|
|
28
28
|
merged_env = os.environ.copy()
|
|
29
29
|
if env:
|
|
30
30
|
merged_env.update(env)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
capture_output=True,
|
|
37
|
-
check=True,
|
|
38
|
-
)
|
|
39
|
-
except subprocess.CalledProcessError as exc:
|
|
31
|
+
proc = run_capture(
|
|
32
|
+
[tmux, "new-session", "-d", "-s", session_name, "-c", str(cwd), *command],
|
|
33
|
+
env=merged_env,
|
|
34
|
+
)
|
|
35
|
+
if proc.returncode != 0:
|
|
40
36
|
raise ToolError(
|
|
41
37
|
"SPAWN_FAILED",
|
|
42
38
|
f"Failed to create tmux session {session_name}.",
|
|
43
|
-
{"stderr":
|
|
44
|
-
)
|
|
39
|
+
{"stderr": proc.stderr, "stdout": proc.stdout, "command": command},
|
|
40
|
+
)
|
|
45
41
|
return TmuxSessionRef(session_name=session_name)
|
|
46
42
|
|
|
47
43
|
def capture(self, ref: TmuxSessionRef, lines: int = 300) -> str:
|
|
48
44
|
tmux = self.require_tmux()
|
|
49
|
-
proc =
|
|
45
|
+
proc = run_capture(
|
|
50
46
|
[tmux, "capture-pane", "-p", "-J", "-t", ref.target, "-S", f"-{lines}"],
|
|
51
|
-
text=True,
|
|
52
|
-
capture_output=True,
|
|
53
|
-
check=False,
|
|
54
47
|
)
|
|
55
48
|
if proc.returncode != 0:
|
|
56
49
|
raise ToolError(
|
|
@@ -66,18 +59,27 @@ class TmuxRuntime:
|
|
|
66
59
|
return
|
|
67
60
|
tmux = self.require_tmux()
|
|
68
61
|
buffer_name = f"agentpool-{ref.session_name}"
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
load = run_capture([tmux, "load-buffer", "-b", buffer_name, "-"], input_text=text)
|
|
63
|
+
if load.returncode != 0:
|
|
64
|
+
raise ToolError(
|
|
65
|
+
"TMUX_SEND_FAILED",
|
|
66
|
+
f"Could not load tmux paste buffer for {ref.target}.",
|
|
67
|
+
{"stderr": load.stderr},
|
|
68
|
+
)
|
|
69
|
+
paste = run_capture([tmux, "paste-buffer", "-b", buffer_name, "-t", ref.target])
|
|
70
|
+
if paste.returncode != 0:
|
|
71
|
+
raise ToolError(
|
|
72
|
+
"TMUX_SEND_FAILED",
|
|
73
|
+
f"Could not paste tmux buffer to {ref.target}.",
|
|
74
|
+
{"stderr": paste.stderr},
|
|
75
|
+
)
|
|
71
76
|
if submit and not text.endswith("\n"):
|
|
72
77
|
self.send_keys(ref, ["Enter"])
|
|
73
78
|
|
|
74
79
|
def send_keys(self, ref: TmuxSessionRef, keys: list[str]) -> None:
|
|
75
80
|
tmux = self.require_tmux()
|
|
76
|
-
proc =
|
|
81
|
+
proc = run_capture(
|
|
77
82
|
[tmux, "send-keys", "-t", ref.target, *keys],
|
|
78
|
-
text=True,
|
|
79
|
-
capture_output=True,
|
|
80
|
-
check=False,
|
|
81
83
|
)
|
|
82
84
|
if proc.returncode != 0:
|
|
83
85
|
raise ToolError(
|
|
@@ -95,7 +97,7 @@ class TmuxRuntime:
|
|
|
95
97
|
def terminate(self, ref: TmuxSessionRef) -> None:
|
|
96
98
|
tmux = self.require_tmux()
|
|
97
99
|
pgid = self._pane_process_group(ref)
|
|
98
|
-
|
|
100
|
+
run_capture([tmux, "kill-session", "-t", ref.session_name])
|
|
99
101
|
if pgid is not None:
|
|
100
102
|
try:
|
|
101
103
|
os.killpg(pgid, signal.SIGTERM)
|
|
@@ -107,22 +109,16 @@ class TmuxRuntime:
|
|
|
107
109
|
def exists(self, ref: TmuxSessionRef) -> bool:
|
|
108
110
|
if not self.tmux_binary:
|
|
109
111
|
return False
|
|
110
|
-
proc =
|
|
112
|
+
proc = run_capture(
|
|
111
113
|
[self.tmux_binary, "has-session", "-t", ref.session_name],
|
|
112
|
-
text=True,
|
|
113
|
-
capture_output=True,
|
|
114
|
-
check=False,
|
|
115
114
|
)
|
|
116
115
|
return proc.returncode == 0
|
|
117
116
|
|
|
118
117
|
def _pane_process_group(self, ref: TmuxSessionRef) -> int | None:
|
|
119
118
|
if not self.tmux_binary:
|
|
120
119
|
return None
|
|
121
|
-
proc =
|
|
120
|
+
proc = run_capture(
|
|
122
121
|
[self.tmux_binary, "display-message", "-p", "-t", ref.target, "#{pane_pid}"],
|
|
123
|
-
text=True,
|
|
124
|
-
capture_output=True,
|
|
125
|
-
check=False,
|
|
126
122
|
)
|
|
127
123
|
if proc.returncode != 0:
|
|
128
124
|
return None
|
|
@@ -17,6 +17,7 @@ import certifi
|
|
|
17
17
|
|
|
18
18
|
from agentpool.models import CapacitySnapshot, Confidence, TmuxSessionRef, UsageStatus, UsageWindow, UsageWindowKind
|
|
19
19
|
from agentpool.runtimes.tmux import TmuxRuntime
|
|
20
|
+
from agentpool.utils import run_capture, terminate_process_group
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class ProbeError(Exception):
|
|
@@ -65,34 +66,12 @@ def _urlopen(
|
|
|
65
66
|
return urllib.request.urlopen(request, timeout=timeout, context=context)
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def _probe_env() -> dict[str, str]:
|
|
69
|
-
env = os.environ.copy()
|
|
70
|
-
env.update(
|
|
71
|
-
{
|
|
72
|
-
"TERM": "dumb",
|
|
73
|
-
"NO_COLOR": "1",
|
|
74
|
-
"CLICOLOR": "0",
|
|
75
|
-
"FORCE_COLOR": "0",
|
|
76
|
-
}
|
|
77
|
-
)
|
|
78
|
-
return env
|
|
79
|
-
|
|
80
|
-
|
|
81
69
|
def _run_probe_command(
|
|
82
70
|
command: list[str],
|
|
83
71
|
*,
|
|
84
72
|
timeout: float,
|
|
85
73
|
) -> subprocess.CompletedProcess[str]:
|
|
86
|
-
return
|
|
87
|
-
command,
|
|
88
|
-
stdin=subprocess.DEVNULL,
|
|
89
|
-
capture_output=True,
|
|
90
|
-
text=True,
|
|
91
|
-
timeout=timeout,
|
|
92
|
-
check=False,
|
|
93
|
-
start_new_session=True,
|
|
94
|
-
env=_probe_env(),
|
|
95
|
-
)
|
|
74
|
+
return run_capture(command, timeout=timeout, terminal_dumb=True)
|
|
96
75
|
|
|
97
76
|
|
|
98
77
|
def _request_json(request: urllib.request.Request) -> dict[str, Any]:
|
|
@@ -248,12 +227,7 @@ def _clean_optional_string(value: object) -> str | None:
|
|
|
248
227
|
|
|
249
228
|
|
|
250
229
|
def _terminate_process(proc: subprocess.Popen[str]) -> None:
|
|
251
|
-
|
|
252
|
-
proc.terminate()
|
|
253
|
-
try:
|
|
254
|
-
proc.wait(timeout=1)
|
|
255
|
-
except subprocess.TimeoutExpired:
|
|
256
|
-
proc.kill()
|
|
230
|
+
terminate_process_group(proc)
|
|
257
231
|
|
|
258
232
|
|
|
259
233
|
def _safe_read_pipe(pipe: Any) -> str:
|
|
@@ -20,6 +20,7 @@ from agentpool.usage._common import (
|
|
|
20
20
|
unavailable,
|
|
21
21
|
unknown,
|
|
22
22
|
)
|
|
23
|
+
from agentpool.utils import popen_text
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def codex_cli_usage_snapshot(provider_id: str, binary: str | None = None) -> CapacitySnapshot:
|
|
@@ -79,12 +80,9 @@ def parse_codex_rate_limits(provider_id: str, payload: dict[str, Any]) -> Capaci
|
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
def _codex_rpc_rate_limits(executable: str) -> dict[str, Any]:
|
|
82
|
-
proc =
|
|
83
|
+
proc = popen_text(
|
|
83
84
|
[executable, "-s", "read-only", "-a", "untrusted", "app-server"],
|
|
84
|
-
|
|
85
|
-
stdout=subprocess.PIPE,
|
|
86
|
-
stderr=subprocess.PIPE,
|
|
87
|
-
text=True,
|
|
85
|
+
terminal_dumb=True,
|
|
88
86
|
)
|
|
89
87
|
try:
|
|
90
88
|
_json_rpc_request(proc, 1, "initialize", {"clientInfo": {"name": "agentpool", "version": "0.1.0"}}, 8.0)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def utc_now_iso() -> str:
|
|
15
|
+
return datetime.now(timezone.utc).isoformat()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def new_session_id() -> str:
|
|
19
|
+
return f"ap_{uuid.uuid4().hex[:12]}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def repo_hash(path: Path) -> str:
|
|
23
|
+
return hashlib.sha256(str(path.resolve()).encode("utf-8")).hexdigest()[:16]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sha256_file(path: Path) -> str:
|
|
27
|
+
digest = hashlib.sha256()
|
|
28
|
+
with path.open("rb") as fh:
|
|
29
|
+
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
|
30
|
+
digest.update(chunk)
|
|
31
|
+
return digest.hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_json(path: Path, data: Any) -> None:
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
path.write_text(json.dumps(data, indent=2, sort_keys=True, default=str) + "\n")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def append_jsonl(path: Path, data: Any) -> None:
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
42
|
+
fh.write(json.dumps(data, sort_keys=True, default=str) + "\n")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def subprocess_env(
|
|
46
|
+
env: dict[str, str] | None = None,
|
|
47
|
+
*,
|
|
48
|
+
terminal_dumb: bool = False,
|
|
49
|
+
) -> dict[str, str]:
|
|
50
|
+
merged = os.environ.copy()
|
|
51
|
+
if env:
|
|
52
|
+
merged.update(env)
|
|
53
|
+
if terminal_dumb:
|
|
54
|
+
merged.update(
|
|
55
|
+
{
|
|
56
|
+
"TERM": "dumb",
|
|
57
|
+
"NO_COLOR": "1",
|
|
58
|
+
"CLICOLOR": "0",
|
|
59
|
+
"FORCE_COLOR": "0",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
return merged
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_capture(
|
|
66
|
+
args: list[str],
|
|
67
|
+
cwd: Path | None = None,
|
|
68
|
+
timeout: float = 10,
|
|
69
|
+
*,
|
|
70
|
+
env: dict[str, str] | None = None,
|
|
71
|
+
input_text: str | None = None,
|
|
72
|
+
terminal_dumb: bool = False,
|
|
73
|
+
) -> subprocess.CompletedProcess[str]:
|
|
74
|
+
stdin = subprocess.PIPE if input_text is not None else subprocess.DEVNULL
|
|
75
|
+
proc = subprocess.Popen(
|
|
76
|
+
args,
|
|
77
|
+
cwd=str(cwd) if cwd else None,
|
|
78
|
+
stdin=stdin,
|
|
79
|
+
stdout=subprocess.PIPE,
|
|
80
|
+
stderr=subprocess.PIPE,
|
|
81
|
+
text=True,
|
|
82
|
+
start_new_session=True,
|
|
83
|
+
env=subprocess_env(env, terminal_dumb=terminal_dumb),
|
|
84
|
+
)
|
|
85
|
+
try:
|
|
86
|
+
stdout, stderr = proc.communicate(input_text, timeout=timeout)
|
|
87
|
+
except subprocess.TimeoutExpired as exc:
|
|
88
|
+
terminate_process_group(proc)
|
|
89
|
+
stdout = exc.stdout or ""
|
|
90
|
+
stderr = exc.stderr or "timed out"
|
|
91
|
+
if isinstance(stdout, bytes):
|
|
92
|
+
stdout = stdout.decode("utf-8", errors="replace")
|
|
93
|
+
if isinstance(stderr, bytes):
|
|
94
|
+
stderr = stderr.decode("utf-8", errors="replace")
|
|
95
|
+
return subprocess.CompletedProcess(args, 124, stdout, stderr)
|
|
96
|
+
return subprocess.CompletedProcess(args, proc.returncode, stdout, stderr)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def popen_text(
|
|
100
|
+
args: list[str],
|
|
101
|
+
*,
|
|
102
|
+
env: dict[str, str] | None = None,
|
|
103
|
+
terminal_dumb: bool = False,
|
|
104
|
+
) -> subprocess.Popen[str]:
|
|
105
|
+
return subprocess.Popen(
|
|
106
|
+
args,
|
|
107
|
+
stdin=subprocess.PIPE,
|
|
108
|
+
stdout=subprocess.PIPE,
|
|
109
|
+
stderr=subprocess.PIPE,
|
|
110
|
+
text=True,
|
|
111
|
+
start_new_session=True,
|
|
112
|
+
env=subprocess_env(env, terminal_dumb=terminal_dumb),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def terminate_process_group(proc: subprocess.Popen[str], timeout: float = 1) -> None:
|
|
117
|
+
if proc.poll() is not None:
|
|
118
|
+
return
|
|
119
|
+
try:
|
|
120
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
121
|
+
except OSError:
|
|
122
|
+
try:
|
|
123
|
+
proc.terminate()
|
|
124
|
+
except OSError:
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
proc.wait(timeout=timeout)
|
|
128
|
+
except subprocess.TimeoutExpired:
|
|
129
|
+
try:
|
|
130
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
131
|
+
except OSError:
|
|
132
|
+
try:
|
|
133
|
+
proc.kill()
|
|
134
|
+
except OSError:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def expand_user_path(value: str) -> Path:
|
|
139
|
+
return Path(os.path.expandvars(value)).expanduser()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from agentpool import utils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_run_capture_detaches_from_host_terminal(monkeypatch) -> None:
|
|
10
|
+
seen: dict[str, object] = {}
|
|
11
|
+
|
|
12
|
+
class FakeProcess:
|
|
13
|
+
pid = 12345
|
|
14
|
+
returncode = 0
|
|
15
|
+
|
|
16
|
+
def communicate(self, input: str | None = None, timeout: float | None = None) -> tuple[str, str]:
|
|
17
|
+
seen["communicate_input"] = input
|
|
18
|
+
seen["communicate_timeout"] = timeout
|
|
19
|
+
return "out", ""
|
|
20
|
+
|
|
21
|
+
def fake_popen(command: list[str], **kwargs: object) -> FakeProcess:
|
|
22
|
+
seen["command"] = command
|
|
23
|
+
seen.update(kwargs)
|
|
24
|
+
return FakeProcess()
|
|
25
|
+
|
|
26
|
+
monkeypatch.setattr(utils.subprocess, "Popen", fake_popen)
|
|
27
|
+
|
|
28
|
+
result = utils.run_capture(["helper", "usage"], cwd=Path("/tmp"), timeout=3, terminal_dumb=True)
|
|
29
|
+
|
|
30
|
+
assert result.stdout == "out"
|
|
31
|
+
assert seen["command"] == ["helper", "usage"]
|
|
32
|
+
assert seen["cwd"] == "/tmp"
|
|
33
|
+
assert seen["stdin"] is subprocess.DEVNULL
|
|
34
|
+
assert seen["stdout"] is subprocess.PIPE
|
|
35
|
+
assert seen["stderr"] is subprocess.PIPE
|
|
36
|
+
assert seen["text"] is True
|
|
37
|
+
assert seen["start_new_session"] is True
|
|
38
|
+
assert seen["communicate_timeout"] == 3
|
|
39
|
+
env = seen["env"]
|
|
40
|
+
assert isinstance(env, dict)
|
|
41
|
+
assert env["TERM"] == "dumb"
|
|
42
|
+
assert env["NO_COLOR"] == "1"
|
|
43
|
+
assert env["CLICOLOR"] == "0"
|
|
44
|
+
assert env["FORCE_COLOR"] == "0"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_run_capture_uses_pipe_only_for_explicit_input(monkeypatch) -> None:
|
|
48
|
+
seen: dict[str, object] = {}
|
|
49
|
+
|
|
50
|
+
class FakeProcess:
|
|
51
|
+
pid = 12345
|
|
52
|
+
returncode = 0
|
|
53
|
+
|
|
54
|
+
def communicate(self, input: str | None = None, timeout: float | None = None) -> tuple[str, str]:
|
|
55
|
+
seen["communicate_input"] = input
|
|
56
|
+
return "", ""
|
|
57
|
+
|
|
58
|
+
def fake_popen(command: list[str], **kwargs: object) -> FakeProcess:
|
|
59
|
+
seen.update(kwargs)
|
|
60
|
+
return FakeProcess()
|
|
61
|
+
|
|
62
|
+
monkeypatch.setattr(utils.subprocess, "Popen", fake_popen)
|
|
63
|
+
|
|
64
|
+
utils.run_capture(["tmux", "load-buffer"], input_text="hello")
|
|
65
|
+
|
|
66
|
+
assert seen["stdin"] is subprocess.PIPE
|
|
67
|
+
assert seen["communicate_input"] == "hello"
|
|
68
|
+
assert seen["start_new_session"] is True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_run_capture_timeout_terminates_process_group(monkeypatch) -> None:
|
|
72
|
+
seen: dict[str, object] = {}
|
|
73
|
+
|
|
74
|
+
class FakeProcess:
|
|
75
|
+
pid = 12345
|
|
76
|
+
returncode = None
|
|
77
|
+
|
|
78
|
+
def communicate(self, input: str | None = None, timeout: float | None = None) -> tuple[str, str]:
|
|
79
|
+
raise subprocess.TimeoutExpired(["slow"], timeout or 0, output="partial")
|
|
80
|
+
|
|
81
|
+
def poll(self) -> None:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def terminate(self) -> None:
|
|
85
|
+
seen["terminated"] = True
|
|
86
|
+
|
|
87
|
+
def wait(self, timeout: float | None = None) -> None:
|
|
88
|
+
seen["wait_timeout"] = timeout
|
|
89
|
+
self.returncode = -15
|
|
90
|
+
|
|
91
|
+
def fake_popen(command: list[str], **kwargs: object) -> FakeProcess:
|
|
92
|
+
return FakeProcess()
|
|
93
|
+
|
|
94
|
+
monkeypatch.setattr(utils.subprocess, "Popen", fake_popen)
|
|
95
|
+
monkeypatch.setattr(utils.os, "getpgid", lambda pid: pid)
|
|
96
|
+
monkeypatch.setattr(utils.os, "killpg", lambda pgid, sig: seen.update({"pgid": pgid, "signal": sig}))
|
|
97
|
+
|
|
98
|
+
result = utils.run_capture(["slow"], timeout=0.01)
|
|
99
|
+
|
|
100
|
+
assert result.returncode == 124
|
|
101
|
+
assert result.stdout == "partial"
|
|
102
|
+
assert seen["pgid"] == 12345
|
|
103
|
+
assert seen["wait_timeout"] == 1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_popen_text_detaches_from_host_terminal(monkeypatch) -> None:
|
|
107
|
+
seen: dict[str, object] = {}
|
|
108
|
+
|
|
109
|
+
class FakeProcess:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
def fake_popen(command: list[str], **kwargs: object) -> FakeProcess:
|
|
113
|
+
seen["command"] = command
|
|
114
|
+
seen.update(kwargs)
|
|
115
|
+
return FakeProcess()
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(utils.subprocess, "Popen", fake_popen)
|
|
118
|
+
|
|
119
|
+
proc = utils.popen_text(["codex", "app-server"], terminal_dumb=True)
|
|
120
|
+
|
|
121
|
+
assert isinstance(proc, FakeProcess)
|
|
122
|
+
assert seen["stdin"] is subprocess.PIPE
|
|
123
|
+
assert seen["stdout"] is subprocess.PIPE
|
|
124
|
+
assert seen["stderr"] is subprocess.PIPE
|
|
125
|
+
assert seen["text"] is True
|
|
126
|
+
assert seen["start_new_session"] is True
|
|
127
|
+
env = seen["env"]
|
|
128
|
+
assert isinstance(env, dict)
|
|
129
|
+
assert env["TERM"] == "dumb"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_product_code_uses_subprocess_only_through_utils() -> None:
|
|
133
|
+
root = Path(__file__).resolve().parents[2] / "src" / "agentpool"
|
|
134
|
+
offenders: list[str] = []
|
|
135
|
+
for path in root.rglob("*.py"):
|
|
136
|
+
if path.name == "utils.py":
|
|
137
|
+
continue
|
|
138
|
+
text = path.read_text(encoding="utf-8")
|
|
139
|
+
for needle in ("subprocess.run(", "subprocess.Popen(", "subprocess.check_output(", "subprocess.check_call("):
|
|
140
|
+
if needle in text:
|
|
141
|
+
offenders.append(f"{path.relative_to(root)} uses {needle}")
|
|
142
|
+
|
|
143
|
+
assert offenders == []
|
|
@@ -311,27 +311,19 @@ def test_usage_http_requests_use_certifi_context(monkeypatch: pytest.MonkeyPatch
|
|
|
311
311
|
def test_usage_probe_subprocesses_are_tty_isolated(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
312
312
|
seen: dict[str, object] = {}
|
|
313
313
|
|
|
314
|
-
def
|
|
314
|
+
def fake_run_capture(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
315
|
+
seen["command"] = command
|
|
315
316
|
seen.update(kwargs)
|
|
316
317
|
return subprocess.CompletedProcess(command, 0, "{}", "")
|
|
317
318
|
|
|
318
|
-
monkeypatch.setattr(usage_common
|
|
319
|
+
monkeypatch.setattr(usage_common, "run_capture", fake_run_capture)
|
|
319
320
|
|
|
320
321
|
result = usage_common._run_probe_command(["codexbar", "usage"], timeout=7)
|
|
321
322
|
|
|
322
323
|
assert result.returncode == 0
|
|
323
|
-
assert seen["
|
|
324
|
-
assert seen["capture_output"] is True
|
|
325
|
-
assert seen["text"] is True
|
|
324
|
+
assert seen["command"] == ["codexbar", "usage"]
|
|
326
325
|
assert seen["timeout"] == 7
|
|
327
|
-
assert seen["
|
|
328
|
-
assert seen["start_new_session"] is True
|
|
329
|
-
env = seen["env"]
|
|
330
|
-
assert isinstance(env, dict)
|
|
331
|
-
assert env["TERM"] == "dumb"
|
|
332
|
-
assert env["NO_COLOR"] == "1"
|
|
333
|
-
assert env["CLICOLOR"] == "0"
|
|
334
|
-
assert env["FORCE_COLOR"] == "0"
|
|
326
|
+
assert seen["terminal_dumb"] is True
|
|
335
327
|
|
|
336
328
|
|
|
337
329
|
def test_devin_plan_status_request_contains_auth_and_top_up_flag() -> None:
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
import uuid
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def utc_now_iso() -> str:
|
|
14
|
-
return datetime.now(timezone.utc).isoformat()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def new_session_id() -> str:
|
|
18
|
-
return f"ap_{uuid.uuid4().hex[:12]}"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def repo_hash(path: Path) -> str:
|
|
22
|
-
return hashlib.sha256(str(path.resolve()).encode("utf-8")).hexdigest()[:16]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def sha256_file(path: Path) -> str:
|
|
26
|
-
digest = hashlib.sha256()
|
|
27
|
-
with path.open("rb") as fh:
|
|
28
|
-
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
|
29
|
-
digest.update(chunk)
|
|
30
|
-
return digest.hexdigest()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def write_json(path: Path, data: Any) -> None:
|
|
34
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
-
path.write_text(json.dumps(data, indent=2, sort_keys=True, default=str) + "\n")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def append_jsonl(path: Path, data: Any) -> None:
|
|
39
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
with path.open("a", encoding="utf-8") as fh:
|
|
41
|
-
fh.write(json.dumps(data, sort_keys=True, default=str) + "\n")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def run_capture(args: list[str], cwd: Path | None = None, timeout: float = 10) -> subprocess.CompletedProcess[str]:
|
|
45
|
-
try:
|
|
46
|
-
return subprocess.run(
|
|
47
|
-
args,
|
|
48
|
-
cwd=str(cwd) if cwd else None,
|
|
49
|
-
text=True,
|
|
50
|
-
capture_output=True,
|
|
51
|
-
timeout=timeout,
|
|
52
|
-
check=False,
|
|
53
|
-
)
|
|
54
|
-
except subprocess.TimeoutExpired as exc:
|
|
55
|
-
return subprocess.CompletedProcess(args, 124, exc.stdout or "", exc.stderr or "timed out")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def expand_user_path(value: str) -> Path:
|
|
59
|
-
return Path(os.path.expandvars(value)).expanduser()
|
|
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
|
|
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
|
{agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_common.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_idle_agent.py
RENAMED
|
File without changes
|
{agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_limit_agent.py
RENAMED
|
File without changes
|
{agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/src/agentpool/fixtures/fake_agents/fake_patch_agent.py
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentpool_cli-0.1.6 → agentpool_cli-0.1.7}/tests/fixtures/provider_model_catalog_golden.json
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
|