multi-agent-registry 0.2.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.
- multi_agent_registry-0.2.0/.github/workflows/ci.yml +37 -0
- multi_agent_registry-0.2.0/.github/workflows/publish.yml +41 -0
- multi_agent_registry-0.2.0/.gitignore +7 -0
- multi_agent_registry-0.2.0/PKG-INFO +107 -0
- multi_agent_registry-0.2.0/README.md +97 -0
- multi_agent_registry-0.2.0/pyproject.toml +33 -0
- multi_agent_registry-0.2.0/src/multi_agent_registry/__init__.py +25 -0
- multi_agent_registry-0.2.0/src/multi_agent_registry/discovery.py +232 -0
- multi_agent_registry-0.2.0/src/multi_agent_registry/models.py +31 -0
- multi_agent_registry-0.2.0/src/multi_agent_registry/py.typed +0 -0
- multi_agent_registry-0.2.0/src/multi_agent_registry/registry.py +340 -0
- multi_agent_registry-0.2.0/tests/test_registry.py +105 -0
- multi_agent_registry-0.2.0/uv.lock +69 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lint:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
# https://github.com/actions/checkout
|
|
19
|
+
- uses: actions/checkout@v6
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
|
|
23
|
+
# https://github.com/astral-sh/setup-uv
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v7
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Set up Python
|
|
30
|
+
run: uv python install
|
|
31
|
+
|
|
32
|
+
- name: Run ruff
|
|
33
|
+
run: uv run ruff check .
|
|
34
|
+
|
|
35
|
+
- name: Run pytest
|
|
36
|
+
run: uv run pytest
|
|
37
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
name: Build and publish
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
contents: write
|
|
18
|
+
attestations: write
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
# https://github.com/actions/checkout
|
|
22
|
+
- uses: actions/checkout@v6
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
# https://github.com/astral-sh/setup-uv
|
|
27
|
+
- name: Install uv
|
|
28
|
+
uses: astral-sh/setup-uv@v7
|
|
29
|
+
with:
|
|
30
|
+
enable-cache: true
|
|
31
|
+
|
|
32
|
+
- name: Set up Python
|
|
33
|
+
run: uv python install
|
|
34
|
+
|
|
35
|
+
- name: Build package
|
|
36
|
+
run: uv build
|
|
37
|
+
|
|
38
|
+
- name: Publish to PyPI
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
40
|
+
with:
|
|
41
|
+
verbose: true
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: multi-agent-registry
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Unified detection, configuration, and chat discovery registry for AI coding agent CLIs
|
|
5
|
+
Author-email: Mark Stouffer <1802850+InTEGr8or@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: verkit>=0.1.4
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Multi-Agent Registry 🤖
|
|
12
|
+
|
|
13
|
+
Unified detection, configuration, plugin, and chat history discovery registry for AI coding agent CLIs.
|
|
14
|
+
|
|
15
|
+
`multi_agent_registry` gives any tool a single place to ask "which AI coding agents are installed on this machine, where do they keep their config/plugins, and where did they leave their chat history?" — instead of every consumer re-implementing per-agent path guessing.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install multi-agent-registry
|
|
23
|
+
# or using uv
|
|
24
|
+
uv add multi-agent-registry
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The PyPI **distribution** name is `multi-agent-registry`; the importable **module** name is `multi_agent_registry`:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import multi_agent_registry
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Multi-Agent CLI Detection**: A registry of 15 agent CLIs — Claude Code, Antigravity (`agy`), OpenCode, GitHub Copilot, Grok Build, Cursor, Windsurf, Aider, Codex, Continue, Cline, Roo Code, Goose, ShellGPT, and Open Interpreter — with binary name, description, and config paths for each.
|
|
36
|
+
- **Installation & MCP Inspection**: `inspect_agent_cli()`/`inspect_all_agent_clis()` check whether each agent's binary is on `PATH`, whether it's registered as an MCP server, and whether a plugin is installed for it.
|
|
37
|
+
- **Chat Log Discovery**: `discover_agent_chats()` scans the on-disk chat log locations for agents that expose them (currently Claude Code, Antigravity, OpenCode, Aider, Cline, and Roo Code), with recursive-glob patterns pruned to skip `node_modules`/`.venv`/`.git`/`.gwt` for speed.
|
|
38
|
+
- **Chat Inspection Helpers**: `get_chat_workspace()` and `get_chat_last_active()` read each agent's own on-disk format to resolve which project a chat belongs to and when it was truly last active (not just file mtime).
|
|
39
|
+
- **Plugin Enable/Disable State**: Per-agent plugin opt-out, persisted to `~/.config/task-agent/config.json`, for tools that install agent-specific plugins/skills.
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from multi_agent_registry import get_agent_cli_registry, discover_agent_chats, get_chat_workspace
|
|
45
|
+
|
|
46
|
+
# What agents does this machine have?
|
|
47
|
+
for agent_id, info in get_agent_cli_registry().items():
|
|
48
|
+
print(agent_id, info.name, info.binary)
|
|
49
|
+
|
|
50
|
+
# Where has Claude Code been chatting, and about which projects?
|
|
51
|
+
for chat in discover_agent_chats(agent_id="claude"):
|
|
52
|
+
workspace = get_chat_workspace(chat)
|
|
53
|
+
print(chat.path, "->", workspace)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API Reference
|
|
57
|
+
|
|
58
|
+
### Registry & detection
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from multi_agent_registry import AgentCLIInfo, get_agent_cli_registry, inspect_agent_cli, inspect_all_agent_clis
|
|
62
|
+
|
|
63
|
+
registry: dict[str, AgentCLIInfo] = get_agent_cli_registry()
|
|
64
|
+
info = registry["claude"]
|
|
65
|
+
info.id, info.name, info.binary, info.description
|
|
66
|
+
info.config_paths # list[Path] of possible config file locations
|
|
67
|
+
info.mcp_support, info.mcp_command_example
|
|
68
|
+
info.plugin_support, info.plugin_path, info.skills_path, info.plugin_template
|
|
69
|
+
info.chat_log_patterns # list[str] glob patterns, [] if not yet supported
|
|
70
|
+
info.chat_parser_type # "json" | "jsonl" | "markdown"
|
|
71
|
+
|
|
72
|
+
status = inspect_agent_cli("claude") # dict: installed, mcp_registered, plugin_installed, ...
|
|
73
|
+
all_status = inspect_all_agent_clis() # same, for every registered agent
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Chat discovery
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from multi_agent_registry import DiscoveredChat, discover_agent_chats
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
|
|
82
|
+
# All chats for one agent, scanned globally (patterns rooted at ~ or /)
|
|
83
|
+
# or within specific project roots (patterns relative to a project).
|
|
84
|
+
chats: list[DiscoveredChat] = discover_agent_chats(
|
|
85
|
+
agent_id="aider", # omit to scan every agent
|
|
86
|
+
search_roots=[Path.home() / "repos"], # only used for project-relative patterns
|
|
87
|
+
)
|
|
88
|
+
# chat.agent_id, chat.path, chat.parser_type
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Agents currently wired up for chat discovery: `claude`, `agy` (Antigravity), `opencode`, `aider`, `cline`, `roo`. The rest are registered for detection/config/plugin purposes but don't yet have `chat_log_patterns` populated — contributions welcome.
|
|
92
|
+
|
|
93
|
+
### Chat inspection
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from multi_agent_registry import get_chat_workspace, get_chat_last_active
|
|
97
|
+
|
|
98
|
+
get_chat_workspace(chat) # -> Path | None, the project dir the chat belongs to
|
|
99
|
+
get_chat_last_active(chat) # -> datetime | None, true last-message time (jsonl only for now);
|
|
100
|
+
# callers should fall back to file mtime when None
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Notes for tools built on this library
|
|
104
|
+
|
|
105
|
+
- Absolute/home-relative `chat_log_patterns` (e.g. `~/.claude/projects/**/*.jsonl`) are scanned once, globally — `search_roots`/`project_dir` only affects patterns relative to a project (e.g. Aider's `**/.aider.chat.history.md`).
|
|
106
|
+
- `discover_agent_chats()` shells out to `find` for bare recursive-filename patterns (pruning noise directories natively) rather than `glob.glob(recursive=True)`, which must fully traverse a tree before filtering — orders of magnitude faster on large repo trees.
|
|
107
|
+
- `get_chat_workspace()`/`get_chat_last_active()` read each agent's actual on-disk message format (e.g. scanning forward past Claude Code's leading metadata-only jsonl lines for the first `cwd`/`timestamp`) rather than assuming a fixed line/file layout.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Multi-Agent Registry 🤖
|
|
2
|
+
|
|
3
|
+
Unified detection, configuration, plugin, and chat history discovery registry for AI coding agent CLIs.
|
|
4
|
+
|
|
5
|
+
`multi_agent_registry` gives any tool a single place to ask "which AI coding agents are installed on this machine, where do they keep their config/plugins, and where did they leave their chat history?" — instead of every consumer re-implementing per-agent path guessing.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install multi-agent-registry
|
|
13
|
+
# or using uv
|
|
14
|
+
uv add multi-agent-registry
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The PyPI **distribution** name is `multi-agent-registry`; the importable **module** name is `multi_agent_registry`:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import multi_agent_registry
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Multi-Agent CLI Detection**: A registry of 15 agent CLIs — Claude Code, Antigravity (`agy`), OpenCode, GitHub Copilot, Grok Build, Cursor, Windsurf, Aider, Codex, Continue, Cline, Roo Code, Goose, ShellGPT, and Open Interpreter — with binary name, description, and config paths for each.
|
|
26
|
+
- **Installation & MCP Inspection**: `inspect_agent_cli()`/`inspect_all_agent_clis()` check whether each agent's binary is on `PATH`, whether it's registered as an MCP server, and whether a plugin is installed for it.
|
|
27
|
+
- **Chat Log Discovery**: `discover_agent_chats()` scans the on-disk chat log locations for agents that expose them (currently Claude Code, Antigravity, OpenCode, Aider, Cline, and Roo Code), with recursive-glob patterns pruned to skip `node_modules`/`.venv`/`.git`/`.gwt` for speed.
|
|
28
|
+
- **Chat Inspection Helpers**: `get_chat_workspace()` and `get_chat_last_active()` read each agent's own on-disk format to resolve which project a chat belongs to and when it was truly last active (not just file mtime).
|
|
29
|
+
- **Plugin Enable/Disable State**: Per-agent plugin opt-out, persisted to `~/.config/task-agent/config.json`, for tools that install agent-specific plugins/skills.
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from multi_agent_registry import get_agent_cli_registry, discover_agent_chats, get_chat_workspace
|
|
35
|
+
|
|
36
|
+
# What agents does this machine have?
|
|
37
|
+
for agent_id, info in get_agent_cli_registry().items():
|
|
38
|
+
print(agent_id, info.name, info.binary)
|
|
39
|
+
|
|
40
|
+
# Where has Claude Code been chatting, and about which projects?
|
|
41
|
+
for chat in discover_agent_chats(agent_id="claude"):
|
|
42
|
+
workspace = get_chat_workspace(chat)
|
|
43
|
+
print(chat.path, "->", workspace)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API Reference
|
|
47
|
+
|
|
48
|
+
### Registry & detection
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from multi_agent_registry import AgentCLIInfo, get_agent_cli_registry, inspect_agent_cli, inspect_all_agent_clis
|
|
52
|
+
|
|
53
|
+
registry: dict[str, AgentCLIInfo] = get_agent_cli_registry()
|
|
54
|
+
info = registry["claude"]
|
|
55
|
+
info.id, info.name, info.binary, info.description
|
|
56
|
+
info.config_paths # list[Path] of possible config file locations
|
|
57
|
+
info.mcp_support, info.mcp_command_example
|
|
58
|
+
info.plugin_support, info.plugin_path, info.skills_path, info.plugin_template
|
|
59
|
+
info.chat_log_patterns # list[str] glob patterns, [] if not yet supported
|
|
60
|
+
info.chat_parser_type # "json" | "jsonl" | "markdown"
|
|
61
|
+
|
|
62
|
+
status = inspect_agent_cli("claude") # dict: installed, mcp_registered, plugin_installed, ...
|
|
63
|
+
all_status = inspect_all_agent_clis() # same, for every registered agent
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Chat discovery
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from multi_agent_registry import DiscoveredChat, discover_agent_chats
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
|
|
72
|
+
# All chats for one agent, scanned globally (patterns rooted at ~ or /)
|
|
73
|
+
# or within specific project roots (patterns relative to a project).
|
|
74
|
+
chats: list[DiscoveredChat] = discover_agent_chats(
|
|
75
|
+
agent_id="aider", # omit to scan every agent
|
|
76
|
+
search_roots=[Path.home() / "repos"], # only used for project-relative patterns
|
|
77
|
+
)
|
|
78
|
+
# chat.agent_id, chat.path, chat.parser_type
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Agents currently wired up for chat discovery: `claude`, `agy` (Antigravity), `opencode`, `aider`, `cline`, `roo`. The rest are registered for detection/config/plugin purposes but don't yet have `chat_log_patterns` populated — contributions welcome.
|
|
82
|
+
|
|
83
|
+
### Chat inspection
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from multi_agent_registry import get_chat_workspace, get_chat_last_active
|
|
87
|
+
|
|
88
|
+
get_chat_workspace(chat) # -> Path | None, the project dir the chat belongs to
|
|
89
|
+
get_chat_last_active(chat) # -> datetime | None, true last-message time (jsonl only for now);
|
|
90
|
+
# callers should fall back to file mtime when None
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Notes for tools built on this library
|
|
94
|
+
|
|
95
|
+
- Absolute/home-relative `chat_log_patterns` (e.g. `~/.claude/projects/**/*.jsonl`) are scanned once, globally — `search_roots`/`project_dir` only affects patterns relative to a project (e.g. Aider's `**/.aider.chat.history.md`).
|
|
96
|
+
- `discover_agent_chats()` shells out to `find` for bare recursive-filename patterns (pruning noise directories natively) rather than `glob.glob(recursive=True)`, which must fully traverse a tree before filtering — orders of magnitude faster on large repo trees.
|
|
97
|
+
- `get_chat_workspace()`/`get_chat_last_active()` read each agent's actual on-disk message format (e.g. scanning forward past Claude Code's leading metadata-only jsonl lines for the first `cwd`/`timestamp`) rather than assuming a fixed line/file layout.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "multi-agent-registry"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Unified detection, configuration, and chat discovery registry for AI coding agent CLIs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Mark Stouffer", email = "1802850+InTEGr8or@users.noreply.github.com" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"verkit>=0.1.4",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build]
|
|
20
|
+
exclude = [
|
|
21
|
+
"docs/",
|
|
22
|
+
".task-agent/",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/multi_agent_registry"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.metadata]
|
|
29
|
+
allow-direct-references = true
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Agent Registry package initialization."""
|
|
2
|
+
|
|
3
|
+
from multi_agent_registry.discovery import (
|
|
4
|
+
DiscoveredChat,
|
|
5
|
+
discover_agent_chats,
|
|
6
|
+
get_chat_last_active,
|
|
7
|
+
get_chat_workspace,
|
|
8
|
+
)
|
|
9
|
+
from multi_agent_registry.registry import (
|
|
10
|
+
AgentCLIInfo,
|
|
11
|
+
get_agent_cli_registry,
|
|
12
|
+
inspect_agent_cli,
|
|
13
|
+
inspect_all_agent_clis,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AgentCLIInfo",
|
|
18
|
+
"DiscoveredChat",
|
|
19
|
+
"get_agent_cli_registry",
|
|
20
|
+
"inspect_agent_cli",
|
|
21
|
+
"inspect_all_agent_clis",
|
|
22
|
+
"discover_agent_chats",
|
|
23
|
+
"get_chat_workspace",
|
|
24
|
+
"get_chat_last_active",
|
|
25
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Discovery module for scanning agent chat log files."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import fnmatch
|
|
6
|
+
import glob
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from multi_agent_registry.registry import AgentCLIInfo, get_agent_cli_registry
|
|
15
|
+
|
|
16
|
+
EXCLUDED_DIR_NAMES = {"node_modules", ".venv", ".git", ".gwt"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_host_root(project_dir: Path) -> Path:
|
|
20
|
+
"""Resolve project directory host root, unwrapping .gwt/<slug> worktrees if present."""
|
|
21
|
+
try:
|
|
22
|
+
from taskagent.store_registry import project_host_root
|
|
23
|
+
|
|
24
|
+
return project_host_root(project_dir)
|
|
25
|
+
except ImportError:
|
|
26
|
+
resolved = project_dir.resolve()
|
|
27
|
+
if ".gwt" in resolved.parts:
|
|
28
|
+
try:
|
|
29
|
+
idx = list(resolved.parts).index(".gwt")
|
|
30
|
+
return Path(*resolved.parts[:idx])
|
|
31
|
+
except ValueError:
|
|
32
|
+
pass
|
|
33
|
+
return resolved
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class DiscoveredChat:
|
|
38
|
+
"""Represents a discovered agent chat log file."""
|
|
39
|
+
|
|
40
|
+
agent_id: str
|
|
41
|
+
path: Path
|
|
42
|
+
parser_type: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_excluded(path: Path) -> bool:
|
|
46
|
+
return any(part in EXCLUDED_DIR_NAMES for part in path.parts)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _find_recursive(host_root: Path, filename: str) -> List[str]:
|
|
50
|
+
prune_expr = []
|
|
51
|
+
for name in EXCLUDED_DIR_NAMES:
|
|
52
|
+
if prune_expr:
|
|
53
|
+
prune_expr.append("-o")
|
|
54
|
+
prune_expr += ["-name", name]
|
|
55
|
+
|
|
56
|
+
cmd = (
|
|
57
|
+
["find", str(host_root), "("] + prune_expr + [")", "-prune", "-o"]
|
|
58
|
+
+ ["-type", "f", "-name", filename, "-print"]
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
62
|
+
return [line for line in result.stdout.splitlines() if line.strip()]
|
|
63
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
64
|
+
matched = []
|
|
65
|
+
for root, dirnames, filenames in os.walk(host_root):
|
|
66
|
+
dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIR_NAMES]
|
|
67
|
+
for name in filenames:
|
|
68
|
+
if fnmatch.fnmatch(name, filename):
|
|
69
|
+
matched.append(os.path.join(root, name))
|
|
70
|
+
return matched
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _expand_pattern(pattern: str, host_root: Path) -> List[Path]:
|
|
74
|
+
if pattern.startswith("~"):
|
|
75
|
+
expanded = str(Path(pattern).expanduser())
|
|
76
|
+
matched = glob.glob(expanded, recursive=True)
|
|
77
|
+
elif pattern.startswith("/"):
|
|
78
|
+
matched = glob.glob(pattern, recursive=True)
|
|
79
|
+
else:
|
|
80
|
+
if not host_root.exists():
|
|
81
|
+
return []
|
|
82
|
+
if pattern.startswith("**/") and "/" not in pattern[3:]:
|
|
83
|
+
# A bare recursive filename match (e.g. Aider's
|
|
84
|
+
# "**/.aider.chat.history.md"). glob.glob(recursive=True) must
|
|
85
|
+
# fully traverse every directory -- including huge
|
|
86
|
+
# node_modules/.venv trees -- before results can be filtered
|
|
87
|
+
# out, and even a pruning os.walk is too slow in pure Python
|
|
88
|
+
# over large repo trees (seconds -> minutes). Shell out to
|
|
89
|
+
# `find`, which prunes natively and is ~1000x faster in
|
|
90
|
+
# practice; fall back to a pruning walk if `find` is missing.
|
|
91
|
+
filename = pattern[3:]
|
|
92
|
+
matched = _find_recursive(host_root, filename)
|
|
93
|
+
else:
|
|
94
|
+
joined = str(host_root / pattern)
|
|
95
|
+
matched = glob.glob(joined, recursive=True)
|
|
96
|
+
|
|
97
|
+
results: List[Path] = []
|
|
98
|
+
for match in matched:
|
|
99
|
+
p = Path(match)
|
|
100
|
+
if p.is_file() and not _is_excluded(p):
|
|
101
|
+
results.append(p)
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def discover_agent_chats(
|
|
106
|
+
agent_id: Optional[str] = None,
|
|
107
|
+
project_dir: Optional[Path] = None,
|
|
108
|
+
search_roots: Optional[List[Path]] = None,
|
|
109
|
+
) -> List[DiscoveredChat]:
|
|
110
|
+
"""Discover chat log files for one or all registered agent CLIs.
|
|
111
|
+
|
|
112
|
+
Absolute/home-relative patterns (e.g. ``~/.claude/projects/**/*.jsonl``)
|
|
113
|
+
are scanned once, independent of any root. Patterns relative to a
|
|
114
|
+
project (e.g. Aider's ``**/.aider.chat.history.md``) are joined against
|
|
115
|
+
each of ``search_roots`` when given, or a single resolved
|
|
116
|
+
``project_dir``/cwd otherwise. Recursive scans skip noise directories
|
|
117
|
+
(``node_modules``, ``.venv``, ``.git``, ``.gwt``).
|
|
118
|
+
"""
|
|
119
|
+
registry = get_agent_cli_registry()
|
|
120
|
+
|
|
121
|
+
if agent_id is not None:
|
|
122
|
+
if agent_id not in registry:
|
|
123
|
+
raise ValueError(f"Unknown agent CLI: '{agent_id}'")
|
|
124
|
+
agents_to_scan: List[AgentCLIInfo] = [registry[agent_id]]
|
|
125
|
+
else:
|
|
126
|
+
agents_to_scan = list(registry.values())
|
|
127
|
+
|
|
128
|
+
if search_roots:
|
|
129
|
+
host_roots = [_resolve_host_root(r) for r in search_roots]
|
|
130
|
+
else:
|
|
131
|
+
if project_dir is None:
|
|
132
|
+
project_dir = Path.cwd()
|
|
133
|
+
host_roots = [_resolve_host_root(project_dir)]
|
|
134
|
+
|
|
135
|
+
discovered: List[DiscoveredChat] = []
|
|
136
|
+
seen_paths = set()
|
|
137
|
+
|
|
138
|
+
for agent in agents_to_scan:
|
|
139
|
+
if not agent.chat_log_patterns:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
for pattern in agent.chat_log_patterns:
|
|
143
|
+
is_rooted = pattern.startswith("~") or pattern.startswith("/")
|
|
144
|
+
roots_to_try = [host_roots[0]] if is_rooted else host_roots
|
|
145
|
+
|
|
146
|
+
for host_root in roots_to_try:
|
|
147
|
+
found_paths = _expand_pattern(pattern, host_root)
|
|
148
|
+
for path in found_paths:
|
|
149
|
+
resolved = path.resolve()
|
|
150
|
+
if resolved not in seen_paths:
|
|
151
|
+
seen_paths.add(resolved)
|
|
152
|
+
discovered.append(
|
|
153
|
+
DiscoveredChat(
|
|
154
|
+
agent_id=agent.id,
|
|
155
|
+
path=path,
|
|
156
|
+
parser_type=agent.chat_parser_type,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
discovered.sort(key=lambda item: item.path)
|
|
161
|
+
return discovered
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_chat_workspace(chat: DiscoveredChat) -> Optional[Path]:
|
|
165
|
+
"""Best-effort resolution of which project/repo a discovered chat belongs to.
|
|
166
|
+
|
|
167
|
+
Reads the on-disk structure each agent uses to record its working
|
|
168
|
+
directory, rather than relying on the chat file's own location.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
if chat.parser_type == "jsonl":
|
|
172
|
+
# Leading lines are often session metadata (mode, snapshots) with
|
|
173
|
+
# no `cwd` field; scan forward (capped) for the first one that
|
|
174
|
+
# has it rather than assuming line 1.
|
|
175
|
+
with open(chat.path, "r") as f:
|
|
176
|
+
for _, line in zip(range(200), f):
|
|
177
|
+
line = line.strip()
|
|
178
|
+
if not line:
|
|
179
|
+
continue
|
|
180
|
+
try:
|
|
181
|
+
data = json.loads(line)
|
|
182
|
+
except json.JSONDecodeError:
|
|
183
|
+
continue
|
|
184
|
+
cwd = data.get("cwd")
|
|
185
|
+
if cwd:
|
|
186
|
+
return Path(cwd)
|
|
187
|
+
elif chat.parser_type == "markdown":
|
|
188
|
+
return chat.path.parent
|
|
189
|
+
elif chat.parser_type == "json":
|
|
190
|
+
with open(chat.path, "r") as f:
|
|
191
|
+
content = f.read(10000)
|
|
192
|
+
match = re.search(r"Current Workspace Directory \((.*?)\)", content)
|
|
193
|
+
if match:
|
|
194
|
+
return Path(match.group(1))
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_chat_last_active(chat: DiscoveredChat) -> Optional[datetime]:
|
|
201
|
+
"""Best-effort true last-active timestamp for a chat log.
|
|
202
|
+
|
|
203
|
+
Scans per-message timestamps rather than trusting file mtime, which can
|
|
204
|
+
be misleading (e.g. a metadata-only stub rewritten without new
|
|
205
|
+
messages). Returns None when the format isn't understood yet, in which
|
|
206
|
+
case callers should fall back to file mtime.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
if chat.parser_type == "jsonl":
|
|
210
|
+
last_ts = None
|
|
211
|
+
with open(chat.path, "r") as f:
|
|
212
|
+
for line in f:
|
|
213
|
+
line = line.strip()
|
|
214
|
+
if not line:
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
data = json.loads(line)
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
continue
|
|
220
|
+
ts = data.get("timestamp")
|
|
221
|
+
if not ts:
|
|
222
|
+
continue
|
|
223
|
+
try:
|
|
224
|
+
parsed = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
225
|
+
except ValueError:
|
|
226
|
+
continue
|
|
227
|
+
if last_ts is None or parsed > last_ts:
|
|
228
|
+
last_ts = parsed
|
|
229
|
+
return last_ts
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
return None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Data models for agent CLI registry and discovery."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AgentCLIInfo:
|
|
10
|
+
id: str
|
|
11
|
+
name: str
|
|
12
|
+
binary: str
|
|
13
|
+
description: str
|
|
14
|
+
config_paths: List[Path] = field(default_factory=list)
|
|
15
|
+
mcp_support: bool = True
|
|
16
|
+
mcp_command_example: str = ""
|
|
17
|
+
plugin_support: bool = False
|
|
18
|
+
plugin_path: Optional[Path] = None
|
|
19
|
+
skills_path: Optional[Path] = None
|
|
20
|
+
plugin_template: str = ""
|
|
21
|
+
chat_log_patterns: List[str] = field(default_factory=list)
|
|
22
|
+
chat_parser_type: str = "json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DiscoveredChat:
|
|
27
|
+
"""Represents a discovered agent chat log file."""
|
|
28
|
+
|
|
29
|
+
agent_id: str
|
|
30
|
+
path: Path
|
|
31
|
+
parser_type: str
|
|
File without changes
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Agent CLI Detection Registry for Task Agent.
|
|
2
|
+
|
|
3
|
+
Provides a unified registry of popular agent CLIs, detection logic, global/project
|
|
4
|
+
MCP configuration paths, and plugin installation helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import shutil
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AgentCLIInfo:
|
|
15
|
+
id: str
|
|
16
|
+
name: str
|
|
17
|
+
binary: str
|
|
18
|
+
description: str
|
|
19
|
+
config_paths: List[Path] = field(default_factory=list)
|
|
20
|
+
mcp_support: bool = True
|
|
21
|
+
mcp_command_example: str = ""
|
|
22
|
+
plugin_support: bool = False
|
|
23
|
+
plugin_path: Optional[Path] = None
|
|
24
|
+
skills_path: Optional[Path] = None
|
|
25
|
+
plugin_template: str = ""
|
|
26
|
+
chat_log_patterns: List[str] = field(default_factory=list)
|
|
27
|
+
chat_parser_type: str = "json"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_agent_cli_registry() -> Dict[str, AgentCLIInfo]:
|
|
31
|
+
home = Path.home()
|
|
32
|
+
return {
|
|
33
|
+
"claude": AgentCLIInfo(
|
|
34
|
+
id="claude",
|
|
35
|
+
name="Claude Code",
|
|
36
|
+
binary="claude",
|
|
37
|
+
description="Anthropic's agentic coding CLI tool",
|
|
38
|
+
config_paths=[home / ".claude.json", home / ".claude" / "config.json"],
|
|
39
|
+
mcp_support=True,
|
|
40
|
+
mcp_command_example="ta init-mcp --claude",
|
|
41
|
+
plugin_support=True,
|
|
42
|
+
plugin_path=home / ".claude" / "plugins",
|
|
43
|
+
skills_path=home / ".claude" / "commands",
|
|
44
|
+
plugin_template="claude-code",
|
|
45
|
+
chat_log_patterns=[
|
|
46
|
+
"~/.claude/projects/**/*.jsonl",
|
|
47
|
+
"~/.claude/history.jsonl",
|
|
48
|
+
],
|
|
49
|
+
chat_parser_type="jsonl",
|
|
50
|
+
),
|
|
51
|
+
"agy": AgentCLIInfo(
|
|
52
|
+
id="agy",
|
|
53
|
+
name="Antigravity CLI",
|
|
54
|
+
binary="agy",
|
|
55
|
+
description="Google DeepMind Antigravity CLI",
|
|
56
|
+
config_paths=[home / ".gemini" / "antigravity-cli" / "mcp_config.json"],
|
|
57
|
+
mcp_support=True,
|
|
58
|
+
mcp_command_example="ta init-mcp --agy",
|
|
59
|
+
plugin_support=True,
|
|
60
|
+
plugin_path=home / ".gemini" / "antigravity-cli" / "plugins",
|
|
61
|
+
skills_path=home / ".gemini" / "config" / "skills",
|
|
62
|
+
plugin_template="antigravity",
|
|
63
|
+
chat_log_patterns=[
|
|
64
|
+
"~/.gemini/tmp/**/chats/session-*.json",
|
|
65
|
+
"~/.gemini/antigravity-cli/brain/**/transcript.jsonl",
|
|
66
|
+
"~/.gemini/antigravity-cli/brain/**/transcript_full.jsonl",
|
|
67
|
+
],
|
|
68
|
+
chat_parser_type="json",
|
|
69
|
+
),
|
|
70
|
+
"opencode": AgentCLIInfo(
|
|
71
|
+
id="opencode",
|
|
72
|
+
name="OpenCode",
|
|
73
|
+
binary="opencode",
|
|
74
|
+
description="Open-source agentic coding workspace and TUI",
|
|
75
|
+
config_paths=[home / ".config" / "opencode" / "opencode.json"],
|
|
76
|
+
mcp_support=True,
|
|
77
|
+
mcp_command_example="ta init-mcp --opencode",
|
|
78
|
+
plugin_support=True,
|
|
79
|
+
plugin_path=home / ".config" / "opencode" / "plugins",
|
|
80
|
+
skills_path=home / ".config" / "opencode" / "skills",
|
|
81
|
+
plugin_template="opencode",
|
|
82
|
+
chat_log_patterns=[
|
|
83
|
+
"~/.local/share/opencode/storage/**/*.json",
|
|
84
|
+
"~/.local/share/opencode/sessions/*.json",
|
|
85
|
+
"~/.config/opencode/chats/*.json",
|
|
86
|
+
],
|
|
87
|
+
chat_parser_type="json",
|
|
88
|
+
),
|
|
89
|
+
"copilot": AgentCLIInfo(
|
|
90
|
+
id="copilot",
|
|
91
|
+
name="GitHub Copilot CLI",
|
|
92
|
+
binary="copilot",
|
|
93
|
+
description="GitHub Copilot CLI agent",
|
|
94
|
+
config_paths=[home / ".config" / "github-copilot" / "config.json"],
|
|
95
|
+
mcp_support=True,
|
|
96
|
+
mcp_command_example="ta init-mcp --copilot",
|
|
97
|
+
plugin_support=True,
|
|
98
|
+
plugin_path=home / ".config" / "github-copilot" / "plugins",
|
|
99
|
+
skills_path=home / ".config" / "github-copilot" / "skills",
|
|
100
|
+
plugin_template="copilot",
|
|
101
|
+
),
|
|
102
|
+
"grok": AgentCLIInfo(
|
|
103
|
+
id="grok",
|
|
104
|
+
name="Grok Build",
|
|
105
|
+
binary="grok",
|
|
106
|
+
description="xAI Grok coding assistant CLI",
|
|
107
|
+
config_paths=[home / ".config" / "grok" / "config.json"],
|
|
108
|
+
mcp_support=True,
|
|
109
|
+
mcp_command_example="ta init-mcp --agent grok",
|
|
110
|
+
plugin_support=True,
|
|
111
|
+
plugin_path=home / ".config" / "grok" / "plugins",
|
|
112
|
+
skills_path=home / ".config" / "grok" / "skills",
|
|
113
|
+
plugin_template="grok",
|
|
114
|
+
),
|
|
115
|
+
"cursor": AgentCLIInfo(
|
|
116
|
+
id="cursor",
|
|
117
|
+
name="Cursor CLI",
|
|
118
|
+
binary="cursor",
|
|
119
|
+
description="Cursor AI editor CLI interface",
|
|
120
|
+
config_paths=[
|
|
121
|
+
home / ".cursor" / "mcp.json",
|
|
122
|
+
home / ".config" / "Cursor" / "mcp.json",
|
|
123
|
+
],
|
|
124
|
+
mcp_support=True,
|
|
125
|
+
mcp_command_example="ta init-mcp --print",
|
|
126
|
+
plugin_support=True,
|
|
127
|
+
plugin_path=home / ".cursor" / "plugins",
|
|
128
|
+
skills_path=home / ".cursor" / "skills",
|
|
129
|
+
plugin_template="cursor",
|
|
130
|
+
),
|
|
131
|
+
"windsurf": AgentCLIInfo(
|
|
132
|
+
id="windsurf",
|
|
133
|
+
name="Windsurf",
|
|
134
|
+
binary="windsurf",
|
|
135
|
+
description="Codeium Windsurf AI IDE CLI",
|
|
136
|
+
config_paths=[home / ".codeium" / "windsurf" / "mcp_config.json"],
|
|
137
|
+
mcp_support=True,
|
|
138
|
+
mcp_command_example="ta init-mcp --print",
|
|
139
|
+
plugin_support=True,
|
|
140
|
+
plugin_path=home / ".codeium" / "windsurf" / "plugins",
|
|
141
|
+
skills_path=home / ".codeium" / "windsurf" / "skills",
|
|
142
|
+
plugin_template="windsurf",
|
|
143
|
+
),
|
|
144
|
+
"aider": AgentCLIInfo(
|
|
145
|
+
id="aider",
|
|
146
|
+
name="Aider",
|
|
147
|
+
binary="aider",
|
|
148
|
+
description="AI pair programming in your terminal",
|
|
149
|
+
config_paths=[home / ".aider.conf.yml"],
|
|
150
|
+
mcp_support=False,
|
|
151
|
+
plugin_support=False,
|
|
152
|
+
chat_log_patterns=[
|
|
153
|
+
"**/.aider.chat.history.md",
|
|
154
|
+
"~/.aider.chat.history.md",
|
|
155
|
+
],
|
|
156
|
+
chat_parser_type="markdown",
|
|
157
|
+
),
|
|
158
|
+
"codex": AgentCLIInfo(
|
|
159
|
+
id="codex",
|
|
160
|
+
name="Codex CLI / ADK Worker",
|
|
161
|
+
binary="codex",
|
|
162
|
+
description="OpenAI Codex CLI harness",
|
|
163
|
+
config_paths=[home / ".codex" / "config.json"],
|
|
164
|
+
mcp_support=True,
|
|
165
|
+
plugin_support=True,
|
|
166
|
+
plugin_path=home / ".codex" / "plugins",
|
|
167
|
+
skills_path=home / ".codex" / "skills",
|
|
168
|
+
plugin_template="codex",
|
|
169
|
+
),
|
|
170
|
+
"continue": AgentCLIInfo(
|
|
171
|
+
id="continue",
|
|
172
|
+
name="Continue",
|
|
173
|
+
binary="continue",
|
|
174
|
+
description="Open-source AI code assistant",
|
|
175
|
+
config_paths=[home / ".continue" / "config.json"],
|
|
176
|
+
mcp_support=True,
|
|
177
|
+
plugin_support=True,
|
|
178
|
+
plugin_path=home / ".continue" / "plugins",
|
|
179
|
+
skills_path=home / ".continue" / "skills",
|
|
180
|
+
plugin_template="continue",
|
|
181
|
+
),
|
|
182
|
+
"cline": AgentCLIInfo(
|
|
183
|
+
id="cline",
|
|
184
|
+
name="Cline",
|
|
185
|
+
binary="cline",
|
|
186
|
+
description="Autonomous coding agent extension & CLI",
|
|
187
|
+
config_paths=[home / ".cline" / "mcp_settings.json"],
|
|
188
|
+
mcp_support=True,
|
|
189
|
+
plugin_support=True,
|
|
190
|
+
chat_log_patterns=[
|
|
191
|
+
"~/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/tasks/*/ui_messages.json",
|
|
192
|
+
"~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/*/ui_messages.json",
|
|
193
|
+
],
|
|
194
|
+
chat_parser_type="json",
|
|
195
|
+
),
|
|
196
|
+
"roo": AgentCLIInfo(
|
|
197
|
+
id="roo",
|
|
198
|
+
name="Roo Code",
|
|
199
|
+
binary="roo",
|
|
200
|
+
description="Roo Code AI coding assistant",
|
|
201
|
+
config_paths=[home / ".roo" / "mcp_settings.json"],
|
|
202
|
+
mcp_support=True,
|
|
203
|
+
plugin_support=True,
|
|
204
|
+
chat_log_patterns=[
|
|
205
|
+
"~/.vscode-server/data/User/globalStorage/rooveterinaryinc.roo-cline/tasks/*/ui_messages.json",
|
|
206
|
+
"~/.vscode-server-insiders/data/User/globalStorage/rooveterinaryinc.roo-cline/tasks/*/ui_messages.json",
|
|
207
|
+
"~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/tasks/*/ui_messages.json",
|
|
208
|
+
"~/.config/Code - Insiders/User/globalStorage/rooveterinaryinc.roo-cline/tasks/*/ui_messages.json",
|
|
209
|
+
],
|
|
210
|
+
chat_parser_type="json",
|
|
211
|
+
),
|
|
212
|
+
"goose": AgentCLIInfo(
|
|
213
|
+
id="goose",
|
|
214
|
+
name="Goose",
|
|
215
|
+
binary="goose",
|
|
216
|
+
description="Block's open-source AI agent framework",
|
|
217
|
+
config_paths=[home / ".config" / "goose" / "config.yaml"],
|
|
218
|
+
mcp_support=True,
|
|
219
|
+
plugin_support=False,
|
|
220
|
+
),
|
|
221
|
+
"sgpt": AgentCLIInfo(
|
|
222
|
+
id="sgpt",
|
|
223
|
+
name="ShellGPT",
|
|
224
|
+
binary="sgpt",
|
|
225
|
+
description="Command-line productivity tool powered by AI",
|
|
226
|
+
config_paths=[home / ".config" / "shell_gpt" / ".sgptrc"],
|
|
227
|
+
mcp_support=False,
|
|
228
|
+
plugin_support=False,
|
|
229
|
+
),
|
|
230
|
+
"interpreter": AgentCLIInfo(
|
|
231
|
+
id="interpreter",
|
|
232
|
+
name="Open Interpreter",
|
|
233
|
+
binary="interpreter",
|
|
234
|
+
description="Natural language interface to computer capabilities",
|
|
235
|
+
config_paths=[home / ".config" / "open-interpreter" / "config.yaml"],
|
|
236
|
+
mcp_support=False,
|
|
237
|
+
plugin_support=False,
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def inspect_agent_cli(agent_id: str) -> dict:
|
|
243
|
+
"""Inspect local installation and MCP registration status for an agent CLI."""
|
|
244
|
+
registry = get_agent_cli_registry()
|
|
245
|
+
if agent_id not in registry:
|
|
246
|
+
raise ValueError(f"Unknown agent CLI: '{agent_id}'")
|
|
247
|
+
|
|
248
|
+
info = registry[agent_id]
|
|
249
|
+
installed = shutil.which(info.binary) is not None
|
|
250
|
+
mcp_registered = False
|
|
251
|
+
|
|
252
|
+
for config_path in info.config_paths:
|
|
253
|
+
if config_path.is_file():
|
|
254
|
+
try:
|
|
255
|
+
content = config_path.read_text(encoding="utf-8")
|
|
256
|
+
if "task_agent" in content or "task-agent" in content:
|
|
257
|
+
mcp_registered = True
|
|
258
|
+
break
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
plugin_installed = False
|
|
263
|
+
if info.plugin_path and info.plugin_path.exists():
|
|
264
|
+
try:
|
|
265
|
+
if any("task-agent" in p.name for p in info.plugin_path.iterdir()):
|
|
266
|
+
plugin_installed = True
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"id": info.id,
|
|
272
|
+
"name": info.name,
|
|
273
|
+
"binary": info.binary,
|
|
274
|
+
"description": info.description,
|
|
275
|
+
"installed": installed,
|
|
276
|
+
"mcp_support": info.mcp_support,
|
|
277
|
+
"mcp_registered": mcp_registered,
|
|
278
|
+
"plugin_support": info.plugin_support,
|
|
279
|
+
"plugin_installed": plugin_installed,
|
|
280
|
+
"mcp_command_example": info.mcp_command_example,
|
|
281
|
+
"chat_log_patterns": info.chat_log_patterns,
|
|
282
|
+
"chat_parser_type": info.chat_parser_type,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def inspect_all_agent_clis() -> List[dict]:
|
|
287
|
+
"""Inspect local installation status for all registered agent CLIs."""
|
|
288
|
+
registry = get_agent_cli_registry()
|
|
289
|
+
return [inspect_agent_cli(agent_id) for agent_id in registry]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_disabled_agent_plugins() -> List[str]:
|
|
293
|
+
"""Load list of explicitly disabled agent plugins from task-agent user config."""
|
|
294
|
+
import json
|
|
295
|
+
|
|
296
|
+
config_file = Path.home() / ".config" / "task-agent" / "config.json"
|
|
297
|
+
if config_file.is_file():
|
|
298
|
+
try:
|
|
299
|
+
data = json.loads(config_file.read_text(encoding="utf-8"))
|
|
300
|
+
disabled = data.get("plugins", {}).get("disabled_agents", [])
|
|
301
|
+
if isinstance(disabled, list):
|
|
302
|
+
return [str(item) for item in disabled]
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def set_agent_plugin_enabled(agent_id: str, enabled: bool) -> None:
|
|
309
|
+
"""Enable or disable a specific agent plugin in task-agent user config."""
|
|
310
|
+
import json
|
|
311
|
+
|
|
312
|
+
config_dir = Path.home() / ".config" / "task-agent"
|
|
313
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
config_file = config_dir / "config.json"
|
|
315
|
+
|
|
316
|
+
data: dict = {}
|
|
317
|
+
if config_file.is_file():
|
|
318
|
+
try:
|
|
319
|
+
data = json.loads(config_file.read_text(encoding="utf-8"))
|
|
320
|
+
except Exception:
|
|
321
|
+
data = {}
|
|
322
|
+
|
|
323
|
+
plugins_cfg = data.setdefault("plugins", {})
|
|
324
|
+
disabled = plugins_cfg.setdefault("disabled_agents", [])
|
|
325
|
+
|
|
326
|
+
if enabled and agent_id in disabled:
|
|
327
|
+
disabled.remove(agent_id)
|
|
328
|
+
elif not enabled and agent_id not in disabled:
|
|
329
|
+
disabled.append(agent_id)
|
|
330
|
+
|
|
331
|
+
config_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def is_agent_plugin_enabled(
|
|
335
|
+
agent_id: str, disabled_list: Optional[List[str]] = None
|
|
336
|
+
) -> bool:
|
|
337
|
+
"""Check if an agent plugin is enabled (enabled by default unless explicitly disabled)."""
|
|
338
|
+
if disabled_list is None:
|
|
339
|
+
disabled_list = get_disabled_agent_plugins()
|
|
340
|
+
return agent_id not in disabled_list
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tests for agent chat log pattern registration and discovery."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from multi_agent_registry import (
|
|
8
|
+
AgentCLIInfo,
|
|
9
|
+
DiscoveredChat,
|
|
10
|
+
discover_agent_chats,
|
|
11
|
+
get_agent_cli_registry,
|
|
12
|
+
inspect_agent_cli,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_agent_cli_registry_has_chat_log_patterns():
|
|
17
|
+
registry = get_agent_cli_registry()
|
|
18
|
+
|
|
19
|
+
expected_agents = ["agy", "roo", "cline", "aider", "claude", "opencode"]
|
|
20
|
+
for agent_id in expected_agents:
|
|
21
|
+
assert agent_id in registry
|
|
22
|
+
info = registry[agent_id]
|
|
23
|
+
assert isinstance(info.chat_log_patterns, list)
|
|
24
|
+
assert len(info.chat_log_patterns) > 0
|
|
25
|
+
assert isinstance(info.chat_parser_type, str)
|
|
26
|
+
assert len(info.chat_parser_type) > 0
|
|
27
|
+
|
|
28
|
+
assert registry["claude"].chat_parser_type == "jsonl"
|
|
29
|
+
assert registry["aider"].chat_parser_type == "markdown"
|
|
30
|
+
assert registry["agy"].chat_parser_type == "json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_agent_cli_info_defaults():
|
|
34
|
+
info = AgentCLIInfo(
|
|
35
|
+
id="test",
|
|
36
|
+
name="Test Agent",
|
|
37
|
+
binary="test",
|
|
38
|
+
description="Test description",
|
|
39
|
+
)
|
|
40
|
+
assert info.chat_log_patterns == []
|
|
41
|
+
assert info.chat_parser_type == "json"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_inspect_agent_cli_chat_info():
|
|
45
|
+
info = inspect_agent_cli("claude")
|
|
46
|
+
assert "chat_log_patterns" in info
|
|
47
|
+
assert "chat_parser_type" in info
|
|
48
|
+
assert info["chat_parser_type"] == "jsonl"
|
|
49
|
+
assert len(info["chat_log_patterns"]) > 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_discover_agent_chats_unknown_agent():
|
|
53
|
+
with pytest.raises(ValueError, match="Unknown agent CLI"):
|
|
54
|
+
discover_agent_chats("nonexistent_agent_xyz")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_discover_agent_chats_all_agents():
|
|
58
|
+
chats = discover_agent_chats()
|
|
59
|
+
assert isinstance(chats, list)
|
|
60
|
+
for chat in chats:
|
|
61
|
+
assert isinstance(chat, DiscoveredChat)
|
|
62
|
+
assert isinstance(chat.agent_id, str)
|
|
63
|
+
assert isinstance(chat.path, Path)
|
|
64
|
+
assert isinstance(chat.parser_type, str)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_discover_agent_chats_specific_agent():
|
|
68
|
+
chats = discover_agent_chats("claude")
|
|
69
|
+
assert isinstance(chats, list)
|
|
70
|
+
for chat in chats:
|
|
71
|
+
assert chat.agent_id == "claude"
|
|
72
|
+
assert chat.parser_type == "jsonl"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_discover_agent_chats_repo_scoping_with_gwt(tmp_path: Path):
|
|
76
|
+
host_root = tmp_path / "my_project"
|
|
77
|
+
host_root.mkdir()
|
|
78
|
+
gwt_dir = host_root / ".gwt" / "feature-branch"
|
|
79
|
+
gwt_dir.mkdir(parents=True)
|
|
80
|
+
|
|
81
|
+
history_file = host_root / ".aider.chat.history.md"
|
|
82
|
+
history_file.write_text("# Aider Chat History\n", encoding="utf-8")
|
|
83
|
+
|
|
84
|
+
chats = discover_agent_chats("aider", project_dir=gwt_dir)
|
|
85
|
+
found_paths = [chat.path.resolve() for chat in chats]
|
|
86
|
+
assert history_file.resolve() in found_paths
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_discover_agent_chats_home_expansion(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
90
|
+
fake_home = tmp_path / "home"
|
|
91
|
+
fake_home.mkdir()
|
|
92
|
+
monkeypatch.setenv("HOME", str(fake_home))
|
|
93
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: fake_home)
|
|
94
|
+
|
|
95
|
+
claude_project_dir = fake_home / ".claude" / "projects" / "my-repo"
|
|
96
|
+
claude_project_dir.mkdir(parents=True)
|
|
97
|
+
chat_file = claude_project_dir / "session.jsonl"
|
|
98
|
+
chat_file.write_text('{"role": "user", "content": "hello"}\n', encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
chats = discover_agent_chats("claude", project_dir=tmp_path)
|
|
101
|
+
found_chats = [c for c in chats if c.agent_id == "claude"]
|
|
102
|
+
assert len(found_chats) > 0
|
|
103
|
+
assert any(c.path.resolve() == chat_file.resolve() for c in found_chats)
|
|
104
|
+
matched = [c for c in found_chats if c.path.resolve() == chat_file.resolve()][0]
|
|
105
|
+
assert matched.parser_type == "jsonl"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "markdown-it-py"
|
|
7
|
+
version = "4.2.0"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "mdurl" },
|
|
11
|
+
]
|
|
12
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
13
|
+
wheels = [
|
|
14
|
+
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "mdurl"
|
|
19
|
+
version = "0.1.2"
|
|
20
|
+
source = { registry = "https://pypi.org/simple" }
|
|
21
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
22
|
+
wheels = [
|
|
23
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "multi-agent-registry"
|
|
28
|
+
version = "0.2.0"
|
|
29
|
+
source = { editable = "." }
|
|
30
|
+
dependencies = [
|
|
31
|
+
{ name = "verkit" },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[package.metadata]
|
|
35
|
+
requires-dist = [{ name = "verkit", specifier = ">=0.1.4" }]
|
|
36
|
+
|
|
37
|
+
[[package]]
|
|
38
|
+
name = "pygments"
|
|
39
|
+
version = "2.21.0"
|
|
40
|
+
source = { registry = "https://pypi.org/simple" }
|
|
41
|
+
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
|
42
|
+
wheels = [
|
|
43
|
+
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[[package]]
|
|
47
|
+
name = "rich"
|
|
48
|
+
version = "15.0.0"
|
|
49
|
+
source = { registry = "https://pypi.org/simple" }
|
|
50
|
+
dependencies = [
|
|
51
|
+
{ name = "markdown-it-py" },
|
|
52
|
+
{ name = "pygments" },
|
|
53
|
+
]
|
|
54
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
55
|
+
wheels = [
|
|
56
|
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[package]]
|
|
60
|
+
name = "verkit"
|
|
61
|
+
version = "0.1.4"
|
|
62
|
+
source = { registry = "https://pypi.org/simple" }
|
|
63
|
+
dependencies = [
|
|
64
|
+
{ name = "rich" },
|
|
65
|
+
]
|
|
66
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a8/68/70d0727c0feff38a35881f0db362c93ebdf6dc4b14b024b3b4434f1795de/verkit-0.1.4.tar.gz", hash = "sha256:2ec6bf18124736783517c16858ac3799a21ba89e4fcc324a3ad22930c0b52cc8", size = 9034, upload-time = "2026-07-31T14:56:38.01Z" }
|
|
67
|
+
wheels = [
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/d2/ee/6eadd91f262d01b316722e768b549248b1a660ac20029b67bc2dc2e97bf0/verkit-0.1.4-py3-none-any.whl", hash = "sha256:bce42636d1bfb22e20e63bfccf74df8349836c17930ef1c3d3bd836e7a170cbb", size = 9149, upload-time = "2026-07-31T14:56:36.889Z" },
|
|
69
|
+
]
|