Skip to content

Commit c744e87

Browse files
authored
chore: Add support for detecting pi agent and additional cursor agent logic (#4)
1 parent 0a4af33 commit c744e87

5 files changed

Lines changed: 38 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.2.0
4+
5+
- Improve detection for cursor agents
6+
- Add support for pi coding agent
7+
38
## 0.1.1
49

510
- Improve detection for cursor usage to avoid mistaking humans as agents

detect_agent/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
OPENCODE: Literal["opencode"] = "opencode"
1818
GITHUB_COPILOT: Literal["github-copilot"] = "github-copilot"
1919
GITHUB_COPILOT_CLI: Literal["github-copilot-cli"] = "github-copilot-cli"
20+
PI: Literal["pi"] = "pi"
2021

2122
KnownAgentNames = Literal[
2223
"cursor",
@@ -31,6 +32,7 @@
3132
"augment-cli",
3233
"opencode",
3334
"github-copilot",
35+
"pi",
3436
]
3537

3638

@@ -51,6 +53,7 @@ class AgentResultNone(TypedDict):
5153
AgentResult = Union[AgentResultAgent, AgentResultNone]
5254

5355
KNOWN_AGENTS = {
56+
"PI": PI,
5457
"CURSOR": CURSOR,
5558
"CURSOR_CLI": CURSOR_CLI,
5659
"CLAUDE": CLAUDE,
@@ -75,10 +78,16 @@ def determine_agent() -> AgentResult:
7578
return {"is_agent": True, "agent": {"name": GITHUB_COPILOT}}
7679
return {"is_agent": True, "agent": {"name": name}} # type: ignore[return-value]
7780

81+
if os.environ.get("PI_CODING_AGENT"):
82+
return {"is_agent": True, "agent": {"name": PI}}
83+
7884
if os.environ.get("CURSOR_AGENT"):
7985
return {"is_agent": True, "agent": {"name": CURSOR}}
8086

81-
if os.environ.get("CURSOR_INVOKED_AS") == "agent":
87+
if (
88+
os.environ.get("CURSOR_INVOKED_AS") == "agent"
89+
or os.environ.get("CURSOR_EXTENSION_HOST_ROLE") == "agent-exec"
90+
):
8291
return {"is_agent": True, "agent": {"name": CURSOR_CLI}}
8392

8493
if os.environ.get("GEMINI_CLI"):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ packages = ["detect_agent"]
77

88
[project]
99
name = "detect_agent"
10-
version = "0.1.1"
10+
version = "0.2.0"
1111
description = "Detect if code is running in an AI agent or automated development environment"
1212
readme = "README.md"
1313
requires-python = ">=3.9"

tests/test_detect_agent.py

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
# Env vars we reset so tests don't leak into each other
1515
_AGENT_ENV_VARS = (
1616
"AI_AGENT",
17-
"CURSOR_TRACE_ID",
17+
"PI_CODING_AGENT",
1818
"CURSOR_AGENT",
19+
"CURSOR_EXTENSION_HOST_ROLE",
1920
"GEMINI_CLI",
2021
"CODEX_SANDBOX",
2122
"CODEX_CI",
@@ -58,6 +59,11 @@ def test_ai_agent_set_detects_custom_agent(self, monkeypatch):
5859
class TestGitHubCopilotDetection:
5960
"""GitHub Copilot detection."""
6061

62+
def test_pi_coding_agent_set_detects_pi(self, monkeypatch):
63+
monkeypatch.setenv("PI_CODING_AGENT", "1")
64+
result = determine_agent()
65+
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["PI"]}}
66+
6167
def test_from_ai_agent_github_copilot(self, monkeypatch):
6268
monkeypatch.setenv("AI_AGENT", "github-copilot")
6369
result = determine_agent()
@@ -87,12 +93,8 @@ def test_from_copilot_github_token(self, monkeypatch):
8793
class TestCursorDetection:
8894
"""Cursor detection."""
8995

90-
def test_cursor_trace_id_not_set_returns_no_agent(self):
91-
result = determine_agent()
92-
assert result == {"is_agent": False, "agent": None}
93-
94-
def test_cursor_trace_id_set_detects_cursor(self, monkeypatch):
95-
monkeypatch.setenv("CURSOR_TRACE_ID", "some-uuid")
96+
def test_cursor_agent_set_detects_cursor(self, monkeypatch):
97+
monkeypatch.setenv("CURSOR_AGENT", "1")
9698
result = determine_agent()
9799
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR"]}}
98100

@@ -107,8 +109,18 @@ def test_cursor_agent_not_set_returns_no_agent(self):
107109
def test_cursor_agent_set_detects_cursor_cli(self, monkeypatch):
108110
monkeypatch.setenv("CURSOR_AGENT", "1")
109111
result = determine_agent()
112+
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR"]}}
113+
114+
def test_cursor_extension_host_role_agent_exec_detects_cursor_cli(self, monkeypatch):
115+
monkeypatch.setenv("CURSOR_EXTENSION_HOST_ROLE", "agent-exec")
116+
result = determine_agent()
110117
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR_CLI"]}}
111118

119+
def test_cursor_extension_host_role_other_value_returns_no_agent(self, monkeypatch):
120+
monkeypatch.setenv("CURSOR_EXTENSION_HOST_ROLE", "something-else")
121+
result = determine_agent()
122+
assert result == {"is_agent": False, "agent": None}
123+
112124

113125
class TestGeminiDetection:
114126
"""Gemini detection."""
@@ -263,7 +275,6 @@ class TestPriorityOrderDetection:
263275

264276
def test_ai_agent_takes_highest_priority(self, monkeypatch):
265277
monkeypatch.setenv("AI_AGENT", "custom-priority")
266-
monkeypatch.setenv("CURSOR_TRACE_ID", "some-uuid")
267278
monkeypatch.setenv("CURSOR_AGENT", "1")
268279
monkeypatch.setenv("GEMINI_CLI", "1")
269280
monkeypatch.setenv("CODEX_SANDBOX", "seatbelt")
@@ -280,24 +291,6 @@ def test_ai_agent_takes_highest_priority(self, monkeypatch):
280291
result = determine_agent()
281292
assert result == {"is_agent": True, "agent": {"name": "custom-priority"}}
282293

283-
def test_cursor_trace_id_takes_priority_over_other_agents(self, monkeypatch):
284-
monkeypatch.setenv("CURSOR_TRACE_ID", "some-uuid")
285-
monkeypatch.setenv("CURSOR_AGENT", "1")
286-
monkeypatch.setenv("GEMINI_CLI", "1")
287-
monkeypatch.setenv("CODEX_SANDBOX", "seatbelt")
288-
monkeypatch.setenv("ANTIGRAVITY_AGENT", "1")
289-
monkeypatch.setenv("AUGMENT_AGENT", "1")
290-
monkeypatch.setenv("OPENCODE_CLIENT", "opencode")
291-
monkeypatch.setenv("CLAUDE_CODE", "1")
292-
monkeypatch.setenv("REPL_ID", "1")
293-
monkeypatch.setenv("COPILOT_MODEL", "gpt-5")
294-
monkeypatch.setenv("COPILOT_ALLOW_ALL", "true")
295-
monkeypatch.setenv("COPILOT_GITHUB_TOKEN", "ghp_xxx")
296-
with patch.object(Path, "exists") as mock_exists:
297-
mock_exists.side_effect = lambda self: str(self) == DEVIN_LOCAL_PATH
298-
result = determine_agent()
299-
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR"]}}
300-
301294
def test_cursor_agent_takes_priority_over_remaining_agents(self, monkeypatch):
302295
monkeypatch.setenv("CURSOR_AGENT", "1")
303296
monkeypatch.setenv("GEMINI_CLI", "1")
@@ -313,15 +306,14 @@ def test_cursor_agent_takes_priority_over_remaining_agents(self, monkeypatch):
313306
with patch.object(Path, "exists") as mock_exists:
314307
mock_exists.side_effect = lambda self: str(self) == DEVIN_LOCAL_PATH
315308
result = determine_agent()
316-
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR_CLI"]}}
309+
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR"]}}
317310

318311

319312
class TestEdgeCases:
320313
"""Edge cases."""
321314

322315
def test_empty_string_env_vars(self, monkeypatch):
323316
monkeypatch.setenv("AI_AGENT", "")
324-
monkeypatch.setenv("CURSOR_TRACE_ID", "")
325317
result = determine_agent()
326318
assert result == {"is_agent": False, "agent": None}
327319

@@ -355,7 +347,7 @@ def test_is_agent_boolean(self, monkeypatch):
355347
assert result["is_agent"] is True
356348

357349
def test_agent_details_when_detected(self, monkeypatch):
358-
monkeypatch.setenv("CURSOR_TRACE_ID", "some-id")
350+
monkeypatch.setenv("CURSOR_AGENT", "1")
359351
result = determine_agent()
360352
assert result["is_agent"] is True
361353
assert result.get("agent") is not None

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)