task-agent 1.2.1__tar.gz → 1.3.0__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.
- {task_agent-1.2.1 → task_agent-1.3.0}/PKG-INFO +1 -1
- {task_agent-1.2.1 → task_agent-1.3.0}/pyproject.toml +1 -1
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/chat/__init__.py +6 -0
- task_agent-1.3.0/src/taskagent/chat/last_used.py +200 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/cli.py +88 -1
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/mcp.py +41 -0
- task_agent-1.3.0/tests/test_chat_last_used.py +86 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_mcp.py +1 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/uv.lock +1 -1
- {task_agent-1.2.1 → task_agent-1.3.0}/.agents/skills/next/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.env +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.gemini/settings.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.github/workflows/ci.yml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.github/workflows/publish.yml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.gitignore +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.mise.toml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.pre-commit-config.yaml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.python-version +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/agy-cli/meta.toml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/antigravity/meta.toml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/gh/meta.toml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/minimal/meta.toml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/uat-aws/meta.toml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.ta/worker +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/.ta-config.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/CLAUDE.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/GEMINI.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/Makefile +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/agent.env +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/STATION-MAP.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/architecture/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/chats/gemini-conversation-1773283034591.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/chats/project-init-to-v0.1.46.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/cli/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/development/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/task-agents.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/workflow/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/docs/workflow/trivial_worker_policy.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/opencode.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/.mcp.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/hooks/hooks.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/hooks/session_start.sh +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/rules/task_agent.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/scripts/statusline.sh +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/next/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/.mcp.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/hooks/hooks.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/hooks/session_start.sh +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/scripts/statusline.sh +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/codex/.mcp.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/codex/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/copilot/.mcp.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/copilot/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/grok/.mcp.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/plugins/grok/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/scripts/clean_duplicates.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/scripts/migrate_tasks.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/scripts/recover_dates.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/.env.example +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/instructions/manager.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/instructions/validator.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/instructions/worker.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/profiles/default.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/pyproject.toml +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/uv.lock +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/worker.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/skills/README.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/skills/next/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/skills/next-task/SKILL.md +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/__init__.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/__main__.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/agent.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/agent_registry.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/audit.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/chat/config.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/chat/discovery.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/config.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/discovery.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/__init__.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/antigravity.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/base.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/claude_code.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/generic.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/inbox.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/manager.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/models/__init__.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/models/issue.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/models/metric.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/perf.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/plugins/__init__.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/plugins/github.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/runner.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/store_registry.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/templates.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/theme.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/test-dir/opencode.json +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/conftest.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/e2e/test_agent_sandboxing.sh +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_agent.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_agent_registry.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_agy_cli_worker.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_atomic_mutations.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_auto_commit.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_chat_config.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_chat_discovery.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_cli.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_cli_windows.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_discovery.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_github_token.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_importers.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_inbox.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_inbox_watch.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_lease.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_manager.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_manager_git.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_metrics.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_migration.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_perf.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_plugin_antigravity.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_plugin_claude.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_project_host_root.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_rebind.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_remote_attach.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_remote_create.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_repo_resolve.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_runner.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_store_registry.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_store_symlink.py +0 -0
- {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_templates.py +0 -0
|
@@ -11,13 +11,19 @@ from taskagent.chat.discovery import (
|
|
|
11
11
|
DiscoveredChat,
|
|
12
12
|
discover_agent_chats,
|
|
13
13
|
)
|
|
14
|
+
from taskagent.chat.last_used import (
|
|
15
|
+
AgentLastUsedInfo,
|
|
16
|
+
get_last_active_agents,
|
|
17
|
+
)
|
|
14
18
|
|
|
15
19
|
__all__ = [
|
|
20
|
+
"AgentLastUsedInfo",
|
|
16
21
|
"ChatConfig",
|
|
17
22
|
"DiscoveredChat",
|
|
18
23
|
"RetentionConfig",
|
|
19
24
|
"S3Config",
|
|
20
25
|
"SummarizationConfig",
|
|
21
26
|
"discover_agent_chats",
|
|
27
|
+
"get_last_active_agents",
|
|
22
28
|
"load_chat_config",
|
|
23
29
|
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Module for discovering and extracting last-active agent session info."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from agent_registry import (
|
|
10
|
+
AgentCLIInfo,
|
|
11
|
+
DiscoveredChat,
|
|
12
|
+
discover_agent_chats,
|
|
13
|
+
get_agent_cli_registry,
|
|
14
|
+
)
|
|
15
|
+
from taskagent.store_registry import project_host_root
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AgentLastUsedInfo:
|
|
20
|
+
"""Information on the last active session for an agent CLI in a repository."""
|
|
21
|
+
|
|
22
|
+
agent_id: str
|
|
23
|
+
agent_name: str
|
|
24
|
+
description: str
|
|
25
|
+
last_active: datetime
|
|
26
|
+
last_user_comment: str
|
|
27
|
+
log_path: Path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_last_user_comment_and_timestamp(
|
|
31
|
+
discovered: DiscoveredChat,
|
|
32
|
+
) -> Tuple[datetime, str]:
|
|
33
|
+
"""Extract timestamp and first 200 chars of last user comment from chat log file."""
|
|
34
|
+
path = discovered.path
|
|
35
|
+
file_mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
|
|
36
|
+
user_comment = ""
|
|
37
|
+
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
return file_mtime, user_comment
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
if discovered.parser_type == "jsonl":
|
|
43
|
+
text_lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
44
|
+
for line in reversed(text_lines):
|
|
45
|
+
line = line.strip()
|
|
46
|
+
if not line:
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(line)
|
|
50
|
+
if isinstance(data, dict):
|
|
51
|
+
role = (
|
|
52
|
+
data.get("type")
|
|
53
|
+
or data.get("role")
|
|
54
|
+
or data.get("source")
|
|
55
|
+
)
|
|
56
|
+
if role in (
|
|
57
|
+
"user",
|
|
58
|
+
"USER_INPUT",
|
|
59
|
+
"USER_EXPLICIT",
|
|
60
|
+
"human",
|
|
61
|
+
):
|
|
62
|
+
content = (
|
|
63
|
+
data.get("content")
|
|
64
|
+
or data.get("text")
|
|
65
|
+
or data.get("message")
|
|
66
|
+
)
|
|
67
|
+
if isinstance(content, list):
|
|
68
|
+
text_parts = []
|
|
69
|
+
for part in content:
|
|
70
|
+
if (
|
|
71
|
+
isinstance(part, dict)
|
|
72
|
+
and part.get("type") == "text"
|
|
73
|
+
):
|
|
74
|
+
text_parts.append(part.get("text", ""))
|
|
75
|
+
elif isinstance(part, str):
|
|
76
|
+
text_parts.append(part)
|
|
77
|
+
content = " ".join(text_parts)
|
|
78
|
+
if isinstance(content, str) and content.strip():
|
|
79
|
+
user_comment = content.strip()
|
|
80
|
+
break
|
|
81
|
+
except Exception:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
elif discovered.parser_type == "json":
|
|
85
|
+
raw_text = path.read_text(encoding="utf-8", errors="replace")
|
|
86
|
+
data = json.loads(raw_text)
|
|
87
|
+
messages = []
|
|
88
|
+
if isinstance(data, list):
|
|
89
|
+
messages = data
|
|
90
|
+
elif isinstance(data, dict):
|
|
91
|
+
messages = (
|
|
92
|
+
data.get("messages")
|
|
93
|
+
or data.get("steps")
|
|
94
|
+
or data.get("history")
|
|
95
|
+
or []
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if isinstance(messages, list):
|
|
99
|
+
for msg in reversed(messages):
|
|
100
|
+
if isinstance(msg, dict):
|
|
101
|
+
role = (
|
|
102
|
+
msg.get("say")
|
|
103
|
+
or msg.get("role")
|
|
104
|
+
or msg.get("type")
|
|
105
|
+
or msg.get("source")
|
|
106
|
+
)
|
|
107
|
+
if role in (
|
|
108
|
+
"user",
|
|
109
|
+
"say",
|
|
110
|
+
"user_feedback",
|
|
111
|
+
"USER_INPUT",
|
|
112
|
+
"human",
|
|
113
|
+
):
|
|
114
|
+
content = (
|
|
115
|
+
msg.get("text")
|
|
116
|
+
or msg.get("content")
|
|
117
|
+
or msg.get("message")
|
|
118
|
+
)
|
|
119
|
+
if isinstance(content, str) and content.strip():
|
|
120
|
+
user_comment = content.strip()
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
elif discovered.parser_type == "markdown":
|
|
124
|
+
raw_text = path.read_text(encoding="utf-8", errors="replace")
|
|
125
|
+
text_lines = raw_text.splitlines()
|
|
126
|
+
user_blocks = []
|
|
127
|
+
current_block = []
|
|
128
|
+
in_user = False
|
|
129
|
+
for line in text_lines:
|
|
130
|
+
if line.startswith("#") or line.startswith("> "):
|
|
131
|
+
if in_user and current_block:
|
|
132
|
+
user_blocks.append("\n".join(current_block).strip())
|
|
133
|
+
current_block = []
|
|
134
|
+
in_user = ("user" in line.lower() or "human" in line.lower())
|
|
135
|
+
elif in_user:
|
|
136
|
+
current_block.append(line)
|
|
137
|
+
if in_user and current_block:
|
|
138
|
+
user_blocks.append("\n".join(current_block).strip())
|
|
139
|
+
if user_blocks:
|
|
140
|
+
user_comment = user_blocks[-1]
|
|
141
|
+
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
user_comment = " ".join(user_comment.split())
|
|
146
|
+
|
|
147
|
+
if len(user_comment) > 200:
|
|
148
|
+
user_comment = user_comment[:197] + "..."
|
|
149
|
+
|
|
150
|
+
return file_mtime, user_comment
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_last_active_agents(
|
|
154
|
+
project_dir: Optional[Path] = None,
|
|
155
|
+
limit: int = 5,
|
|
156
|
+
) -> List[AgentLastUsedInfo]:
|
|
157
|
+
"""Find and rank most recently active AI agent CLIs in a project repository."""
|
|
158
|
+
if project_dir is None:
|
|
159
|
+
project_dir = Path.cwd()
|
|
160
|
+
|
|
161
|
+
host_root = project_host_root(project_dir)
|
|
162
|
+
discovered_chats = discover_agent_chats(project_dir=host_root)
|
|
163
|
+
registry = get_agent_cli_registry()
|
|
164
|
+
|
|
165
|
+
agent_chats_map: dict[str, List[DiscoveredChat]] = {}
|
|
166
|
+
for chat in discovered_chats:
|
|
167
|
+
agent_chats_map.setdefault(chat.agent_id, []).append(chat)
|
|
168
|
+
|
|
169
|
+
results: List[AgentLastUsedInfo] = []
|
|
170
|
+
|
|
171
|
+
for agent_id, chats in agent_chats_map.items():
|
|
172
|
+
agent_info: Optional[AgentCLIInfo] = registry.get(agent_id)
|
|
173
|
+
name = agent_info.name if agent_info else agent_id.capitalize()
|
|
174
|
+
description = agent_info.description if agent_info else ""
|
|
175
|
+
|
|
176
|
+
newest_mtime: Optional[datetime] = None
|
|
177
|
+
newest_comment: str = ""
|
|
178
|
+
newest_path: Optional[Path] = None
|
|
179
|
+
|
|
180
|
+
for chat in chats:
|
|
181
|
+
mtime, comment = _parse_last_user_comment_and_timestamp(chat)
|
|
182
|
+
if newest_mtime is None or mtime > newest_mtime:
|
|
183
|
+
newest_mtime = mtime
|
|
184
|
+
newest_comment = comment
|
|
185
|
+
newest_path = chat.path
|
|
186
|
+
|
|
187
|
+
if newest_mtime is not None and newest_path is not None:
|
|
188
|
+
results.append(
|
|
189
|
+
AgentLastUsedInfo(
|
|
190
|
+
agent_id=agent_id,
|
|
191
|
+
agent_name=name,
|
|
192
|
+
description=description,
|
|
193
|
+
last_active=newest_mtime,
|
|
194
|
+
last_user_comment=newest_comment,
|
|
195
|
+
log_path=newest_path,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
results.sort(key=lambda item: item.last_active, reverse=True)
|
|
200
|
+
return results[:limit]
|
|
@@ -4232,6 +4232,65 @@ def cmd_agent_import(
|
|
|
4232
4232
|
sys.exit(1)
|
|
4233
4233
|
|
|
4234
4234
|
|
|
4235
|
+
def cmd_agent_last_used(
|
|
4236
|
+
console: Console,
|
|
4237
|
+
path_arg: Optional[str] = None,
|
|
4238
|
+
limit: int = 5,
|
|
4239
|
+
json_format: bool = False,
|
|
4240
|
+
) -> None:
|
|
4241
|
+
"""Handler for 'ta agent last-used' subcommand."""
|
|
4242
|
+
from rich import box
|
|
4243
|
+
from taskagent.chat.last_used import get_last_active_agents
|
|
4244
|
+
|
|
4245
|
+
target_path = Path(path_arg).resolve() if path_arg else Path.cwd()
|
|
4246
|
+
results = get_last_active_agents(project_dir=target_path, limit=limit)
|
|
4247
|
+
|
|
4248
|
+
if json_format:
|
|
4249
|
+
output = [
|
|
4250
|
+
{
|
|
4251
|
+
"agent_id": item.agent_id,
|
|
4252
|
+
"agent_name": item.agent_name,
|
|
4253
|
+
"description": item.description,
|
|
4254
|
+
"last_active": item.last_active.isoformat(),
|
|
4255
|
+
"last_user_comment": item.last_user_comment,
|
|
4256
|
+
"log_path": str(item.log_path),
|
|
4257
|
+
}
|
|
4258
|
+
for item in results
|
|
4259
|
+
]
|
|
4260
|
+
print(json.dumps(output, indent=2))
|
|
4261
|
+
return
|
|
4262
|
+
|
|
4263
|
+
if not results:
|
|
4264
|
+
console.print(
|
|
4265
|
+
f"[yellow]No agent chat sessions found for repository at [bold]{target_path}[/bold].[/yellow]"
|
|
4266
|
+
)
|
|
4267
|
+
return
|
|
4268
|
+
|
|
4269
|
+
console.print(
|
|
4270
|
+
f"\n[bold green]🤖 Recently Active AI Agents[/bold green] [dim]({target_path})[/dim]\n"
|
|
4271
|
+
)
|
|
4272
|
+
|
|
4273
|
+
table = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan")
|
|
4274
|
+
table.add_column("#", style="dim", width=3)
|
|
4275
|
+
table.add_column("Agent CLI", style="bold yellow")
|
|
4276
|
+
table.add_column("Description", style="white")
|
|
4277
|
+
table.add_column("Last Active", style="green")
|
|
4278
|
+
table.add_column("Last Comment Snippet", style="dim white")
|
|
4279
|
+
|
|
4280
|
+
for idx, item in enumerate(results, start=1):
|
|
4281
|
+
rel_time = item.last_active.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
4282
|
+
comment_snippet = item.last_user_comment if item.last_user_comment else "[dim](no prompt comment logged)[/dim]"
|
|
4283
|
+
table.add_row(
|
|
4284
|
+
str(idx),
|
|
4285
|
+
item.agent_name,
|
|
4286
|
+
item.description,
|
|
4287
|
+
rel_time,
|
|
4288
|
+
comment_snippet,
|
|
4289
|
+
)
|
|
4290
|
+
|
|
4291
|
+
console.print(table)
|
|
4292
|
+
|
|
4293
|
+
|
|
4235
4294
|
def cmd_version(
|
|
4236
4295
|
console: Console,
|
|
4237
4296
|
promote: Optional[str] = None,
|
|
@@ -6055,6 +6114,27 @@ Start working on a task. This command automates the following workflow:
|
|
|
6055
6114
|
action="store_true",
|
|
6056
6115
|
help="Output result as JSON",
|
|
6057
6116
|
)
|
|
6117
|
+
agent_last_used_p = agent_sub.add_parser(
|
|
6118
|
+
"last-used",
|
|
6119
|
+
help="List AI coding agents recently active in this repository",
|
|
6120
|
+
)
|
|
6121
|
+
agent_last_used_p.add_argument(
|
|
6122
|
+
"path",
|
|
6123
|
+
nargs="?",
|
|
6124
|
+
default=None,
|
|
6125
|
+
help="Target repository directory (defaults to cwd)",
|
|
6126
|
+
)
|
|
6127
|
+
agent_last_used_p.add_argument(
|
|
6128
|
+
"--limit",
|
|
6129
|
+
type=int,
|
|
6130
|
+
default=5,
|
|
6131
|
+
help="Maximum number of active agents to return (default: 5)",
|
|
6132
|
+
)
|
|
6133
|
+
agent_last_used_p.add_argument(
|
|
6134
|
+
"--json",
|
|
6135
|
+
action="store_true",
|
|
6136
|
+
help="Output result as JSON",
|
|
6137
|
+
)
|
|
6058
6138
|
|
|
6059
6139
|
# plan
|
|
6060
6140
|
subparsers.add_parser("plan", help="View or edit the project plan")
|
|
@@ -6738,9 +6818,16 @@ TA_STRATEGY_COOLDOWN_HOURS environment variable.
|
|
|
6738
6818
|
file_path=args.file,
|
|
6739
6819
|
json_format=args.json,
|
|
6740
6820
|
)
|
|
6821
|
+
elif args.agent_subcommand == "last-used":
|
|
6822
|
+
cmd_agent_last_used(
|
|
6823
|
+
console,
|
|
6824
|
+
path_arg=args.path,
|
|
6825
|
+
limit=args.limit,
|
|
6826
|
+
json_format=args.json,
|
|
6827
|
+
)
|
|
6741
6828
|
else:
|
|
6742
6829
|
console.print(
|
|
6743
|
-
"[yellow]Unknown agent subcommand. Use 'ta agent import'.[/yellow]"
|
|
6830
|
+
"[yellow]Unknown agent subcommand. Use 'ta agent import' or 'ta agent last-used'.[/yellow]"
|
|
6744
6831
|
)
|
|
6745
6832
|
elif args.command == "push":
|
|
6746
6833
|
cmd_push(console, manager)
|
|
@@ -1431,6 +1431,47 @@ def import_agent_tasks(
|
|
|
1431
1431
|
)
|
|
1432
1432
|
|
|
1433
1433
|
|
|
1434
|
+
@mcp.tool()
|
|
1435
|
+
def get_last_active_agent(
|
|
1436
|
+
path: str = "",
|
|
1437
|
+
limit: int = 5,
|
|
1438
|
+
) -> str:
|
|
1439
|
+
"""Find and rank most recently active AI coding agent CLIs in a project repository.
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
path: Target repository directory path (defaults to working directory if empty).
|
|
1443
|
+
limit: Maximum number of active agents to return (default: 5).
|
|
1444
|
+
|
|
1445
|
+
Returns:
|
|
1446
|
+
Formatted summary list of recently active AI agent CLIs, sorted descending by last active date,
|
|
1447
|
+
including description and first 200 characters of the last user comment.
|
|
1448
|
+
"""
|
|
1449
|
+
from pathlib import Path
|
|
1450
|
+
from taskagent.chat.last_used import get_last_active_agents
|
|
1451
|
+
|
|
1452
|
+
target_path = Path(path).resolve() if path and path.strip() else Path.cwd()
|
|
1453
|
+
results = get_last_active_agents(project_dir=target_path, limit=limit)
|
|
1454
|
+
|
|
1455
|
+
if not results:
|
|
1456
|
+
return f"No agent chat sessions found for repository at {target_path}."
|
|
1457
|
+
|
|
1458
|
+
lines = [f"### 🤖 Recently Active AI Agents ({target_path})\n"]
|
|
1459
|
+
for idx, item in enumerate(results, start=1):
|
|
1460
|
+
rel_time = item.last_active.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
1461
|
+
comment_snippet = (
|
|
1462
|
+
item.last_user_comment
|
|
1463
|
+
if item.last_user_comment
|
|
1464
|
+
else "(no prompt comment logged)"
|
|
1465
|
+
)
|
|
1466
|
+
lines.append(f"{idx}. **{item.agent_name}** (`{item.agent_id}`)")
|
|
1467
|
+
lines.append(f" - **Description**: {item.description}")
|
|
1468
|
+
lines.append(f" - **Last Active**: {rel_time}")
|
|
1469
|
+
lines.append(f" - **Last Comment Snippet**: {comment_snippet}")
|
|
1470
|
+
lines.append("")
|
|
1471
|
+
|
|
1472
|
+
return "\n".join(lines).strip()
|
|
1473
|
+
|
|
1474
|
+
|
|
1434
1475
|
def run_mcp_server():
|
|
1435
1476
|
"""Main entry point to run the MCP server."""
|
|
1436
1477
|
mcp.run()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from agent_registry.models import DiscoveredChat
|
|
4
|
+
from taskagent.chat.last_used import (
|
|
5
|
+
AgentLastUsedInfo,
|
|
6
|
+
_parse_last_user_comment_and_timestamp,
|
|
7
|
+
get_last_active_agents,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_parse_last_user_comment_jsonl(tmp_path):
|
|
12
|
+
log_file = tmp_path / "claude_session.jsonl"
|
|
13
|
+
lines = [
|
|
14
|
+
json.dumps({"type": "system", "content": "Init"}),
|
|
15
|
+
json.dumps({"type": "user", "content": "First prompt from user"}),
|
|
16
|
+
json.dumps({"type": "assistant", "content": "AI response"}),
|
|
17
|
+
json.dumps({"type": "user", "content": "Please refactor the database connector for production"}),
|
|
18
|
+
]
|
|
19
|
+
log_file.write_text("\n".join(lines))
|
|
20
|
+
|
|
21
|
+
chat = DiscoveredChat(agent_id="claude", path=log_file, parser_type="jsonl")
|
|
22
|
+
mtime, comment = _parse_last_user_comment_and_timestamp(chat)
|
|
23
|
+
|
|
24
|
+
assert comment == "Please refactor the database connector for production"
|
|
25
|
+
assert mtime is not None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_parse_last_user_comment_json(tmp_path):
|
|
29
|
+
log_file = tmp_path / "session.json"
|
|
30
|
+
data = {
|
|
31
|
+
"messages": [
|
|
32
|
+
{"say": "user", "text": "Initial task message"},
|
|
33
|
+
{"say": "assistant", "text": "Working on it"},
|
|
34
|
+
{"say": "user", "text": "Add unit tests for the chat discovery module"},
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
log_file.write_text(json.dumps(data))
|
|
38
|
+
|
|
39
|
+
chat = DiscoveredChat(agent_id="agy", path=log_file, parser_type="json")
|
|
40
|
+
mtime, comment = _parse_last_user_comment_and_timestamp(chat)
|
|
41
|
+
|
|
42
|
+
assert comment == "Add unit tests for the chat discovery module"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_parse_last_user_comment_markdown(tmp_path):
|
|
46
|
+
log_file = tmp_path / ".aider.chat.history.md"
|
|
47
|
+
content = """# session log
|
|
48
|
+
|
|
49
|
+
#### user
|
|
50
|
+
Fix memory leak in background worker queue
|
|
51
|
+
|
|
52
|
+
#### assistant
|
|
53
|
+
Analyzing queue implementation...
|
|
54
|
+
"""
|
|
55
|
+
log_file.write_text(content)
|
|
56
|
+
|
|
57
|
+
chat = DiscoveredChat(agent_id="aider", path=log_file, parser_type="markdown")
|
|
58
|
+
mtime, comment = _parse_last_user_comment_and_timestamp(chat)
|
|
59
|
+
|
|
60
|
+
assert "Fix memory leak in background worker queue" in comment
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_get_last_active_agents(monkeypatch, tmp_path):
|
|
64
|
+
f1 = tmp_path / "claude.jsonl"
|
|
65
|
+
f1.write_text(json.dumps({"type": "user", "content": "Claude task"}))
|
|
66
|
+
|
|
67
|
+
f2 = tmp_path / "agy.json"
|
|
68
|
+
f2.write_text(json.dumps([{"say": "user", "text": "Antigravity task"}]))
|
|
69
|
+
|
|
70
|
+
discovered = [
|
|
71
|
+
DiscoveredChat(agent_id="claude", path=f1, parser_type="jsonl"),
|
|
72
|
+
DiscoveredChat(agent_id="agy", path=f2, parser_type="json"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(
|
|
76
|
+
"taskagent.chat.last_used.discover_agent_chats",
|
|
77
|
+
lambda project_dir=None: discovered,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
results = get_last_active_agents(project_dir=tmp_path, limit=5)
|
|
81
|
+
assert len(results) == 2
|
|
82
|
+
agent_ids = {r.agent_id for r in results}
|
|
83
|
+
assert agent_ids == {"claude", "agy"}
|
|
84
|
+
for r in results:
|
|
85
|
+
assert isinstance(r, AgentLastUsedInfo)
|
|
86
|
+
assert r.description != ""
|
|
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
|
|
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
|
|
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
|