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.
@@ -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,7 @@
1
+ .venv
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .ruff_cache/
@@ -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
@@ -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
+ ]