agentweld 0.1.0__py3-none-any.whl

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.
Files changed (46) hide show
  1. agentweld/__init__.py +3 -0
  2. agentweld/cli/__init__.py +0 -0
  3. agentweld/cli/add.py +142 -0
  4. agentweld/cli/generate.py +164 -0
  5. agentweld/cli/init.py +197 -0
  6. agentweld/cli/inspect.py +171 -0
  7. agentweld/cli/main.py +22 -0
  8. agentweld/cli/preview.py +122 -0
  9. agentweld/composition/__init__.py +1 -0
  10. agentweld/composition/composer.py +85 -0
  11. agentweld/config/__init__.py +6 -0
  12. agentweld/config/loader.py +117 -0
  13. agentweld/config/writer.py +235 -0
  14. agentweld/curation/__init__.py +1 -0
  15. agentweld/curation/engine.py +28 -0
  16. agentweld/curation/quality.py +77 -0
  17. agentweld/curation/rules.py +67 -0
  18. agentweld/generators/__init__.py +13 -0
  19. agentweld/generators/agent_card.py +76 -0
  20. agentweld/generators/base.py +27 -0
  21. agentweld/generators/readme.py +70 -0
  22. agentweld/generators/runner.py +87 -0
  23. agentweld/generators/system_prompt.py +81 -0
  24. agentweld/generators/templates/readme.md.j2 +18 -0
  25. agentweld/generators/templates/system_prompt.md.j2 +16 -0
  26. agentweld/generators/tool_manifest.py +63 -0
  27. agentweld/models/__init__.py +0 -0
  28. agentweld/models/artifacts.py +61 -0
  29. agentweld/models/composed.py +32 -0
  30. agentweld/models/config.py +162 -0
  31. agentweld/models/tool.py +99 -0
  32. agentweld/plugins/__init__.py +5 -0
  33. agentweld/plugins/loader.py +55 -0
  34. agentweld/sources/__init__.py +15 -0
  35. agentweld/sources/base.py +49 -0
  36. agentweld/sources/mcp_http.py +129 -0
  37. agentweld/sources/mcp_stdio.py +156 -0
  38. agentweld/sources/registry.py +120 -0
  39. agentweld/utils/__init__.py +0 -0
  40. agentweld/utils/console.py +105 -0
  41. agentweld/utils/errors.py +33 -0
  42. agentweld-0.1.0.dist-info/METADATA +330 -0
  43. agentweld-0.1.0.dist-info/RECORD +46 -0
  44. agentweld-0.1.0.dist-info/WHEEL +4 -0
  45. agentweld-0.1.0.dist-info/entry_points.txt +2 -0
  46. agentweld-0.1.0.dist-info/licenses/LICENSE +21 -0
agentweld/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """agentweld — Turn any MCP server into a curated, composable, A2A-ready agent."""
2
+
3
+ __version__ = "0.1.0"
File without changes
agentweld/cli/add.py ADDED
@@ -0,0 +1,142 @@
1
+ """agentweld add — add a new MCP source to an existing project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ import anyio
10
+ import typer
11
+
12
+ from agentweld.config.loader import load_config
13
+ from agentweld.config.writer import add_source
14
+ from agentweld.models.config import SourceConfig
15
+ from agentweld.sources.registry import get_adapter
16
+ from agentweld.utils.console import console, make_tools_table
17
+ from agentweld.utils.errors import AgentweldError, ConfigNotFoundError, SourceConnectionError
18
+
19
+ app = typer.Typer(
20
+ help="Add a new MCP source to an existing agentweld project.",
21
+ invoke_without_command=True,
22
+ )
23
+
24
+
25
+ @app.command()
26
+ def add(
27
+ source: str = typer.Argument(..., help="MCP server command (stdio) or URL (http)"),
28
+ from_: str = typer.Option("mcp", "--from", help="Source type"),
29
+ trust: bool = typer.Option(False, "--trust", help="Trust and execute the stdio command"),
30
+ config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to agentweld.yaml"),
31
+ ) -> None:
32
+ """Load existing agentweld.yaml, introspect new source, and append it."""
33
+
34
+ # Determine transport
35
+ transport: Literal["stdio", "streamable-http"] = (
36
+ "streamable-http" if source.startswith("http") else "stdio"
37
+ )
38
+
39
+ # Safety gate for stdio
40
+ if transport == "stdio":
41
+ if not trust:
42
+ console.print(
43
+ "[bold red]ERROR:[/] Spawning a stdio MCP server executes arbitrary code. "
44
+ "Re-run with [bold]--trust[/] to confirm.",
45
+ highlight=False,
46
+ )
47
+ raise typer.Exit(code=1)
48
+ console.print(
49
+ f"[bold yellow]WARNING:[/] --trust flag set. Spawning subprocess: [bold]{source}[/]",
50
+ highlight=False,
51
+ )
52
+
53
+ # Load existing config to find the yaml path
54
+ try:
55
+ cfg = load_config(config_path)
56
+ except ConfigNotFoundError as e:
57
+ console.print(f"[red]Config not found:[/] {e}")
58
+ raise typer.Exit(code=1)
59
+
60
+ # Resolve the actual yaml file path
61
+ yaml_path = _resolve_yaml_path(config_path)
62
+
63
+ # Check if source already exists
64
+ source_id = _derive_source_id(source)
65
+ existing_ids = {s.id for s in cfg.sources}
66
+ if source_id in existing_ids:
67
+ console.print(
68
+ f"[red]ERROR:[/] Source '[bold]{source_id}[/]' already exists in {yaml_path}. "
69
+ "Use a different --name or edit the file manually.",
70
+ highlight=False,
71
+ )
72
+ raise typer.Exit(code=1)
73
+
74
+ # Build SourceConfig
75
+ source_config = SourceConfig(
76
+ id=source_id,
77
+ type="mcp_server",
78
+ transport=transport,
79
+ command=source if transport == "stdio" else None,
80
+ url=source if transport != "stdio" else None,
81
+ )
82
+
83
+ # Introspect
84
+ console.print(f"[cyan]Connecting to[/] {source}...")
85
+ adapter = get_adapter(transport)
86
+ try:
87
+ tools = anyio.run(adapter.introspect, source_config)
88
+ except SourceConnectionError as e:
89
+ console.print(f"[red]Connection failed:[/] {e}")
90
+ raise typer.Exit(code=1)
91
+ except AgentweldError as e:
92
+ console.print(f"[red]Error:[/] {e}")
93
+ raise typer.Exit(code=1)
94
+
95
+ console.print(f"[green]Discovered {len(tools)} tools.[/]")
96
+ tool_rows = [
97
+ {
98
+ "name": t.name,
99
+ "source_id": t.source_id,
100
+ "description": t.description_curated,
101
+ "quality_score": t.quality_score,
102
+ }
103
+ for t in tools
104
+ ]
105
+ console.print(make_tools_table(tool_rows))
106
+
107
+ # Append to yaml
108
+ try:
109
+ add_source(source_config, yaml_path)
110
+ except ValueError as e:
111
+ console.print(f"[red]Failed to add source:[/] {e}")
112
+ raise typer.Exit(code=1)
113
+
114
+ console.print(f"[green]Added source '[bold]{source_id}[/]' to[/] {yaml_path}")
115
+
116
+
117
+ def _derive_source_id(source: str) -> str:
118
+ """Derive a short source ID from a command or URL."""
119
+ parts = source.replace("https://", "").replace("http://", "").split()
120
+ token = parts[-1] if parts else source
121
+ slug = re.sub(r"[^a-z0-9]", "-", token.lower())
122
+ slug = re.sub(r"-+", "-", slug).strip("-")
123
+ segments = [s for s in slug.split("-") if len(s) > 2]
124
+ return segments[-1] if segments else slug[:20]
125
+
126
+
127
+ def _resolve_yaml_path(config_path: Path | None) -> Path:
128
+ """Resolve path to agentweld.yaml, searching from cwd if not specified."""
129
+ if config_path is not None:
130
+ return Path(config_path)
131
+
132
+ current = Path.cwd()
133
+ while True:
134
+ candidate = current / "agentweld.yaml"
135
+ if candidate.exists():
136
+ return candidate
137
+ parent = current.parent
138
+ if parent == current:
139
+ break
140
+ current = parent
141
+
142
+ return Path.cwd() / "agentweld.yaml"
@@ -0,0 +1,164 @@
1
+ """agentweld generate — run the full pipeline and emit artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import anyio
8
+ import typer
9
+
10
+ from agentweld.composition.composer import ComposedToolSet, Composer
11
+ from agentweld.config.loader import load_config
12
+ from agentweld.curation.engine import CurationEngine
13
+ from agentweld.generators.runner import run_generators
14
+ from agentweld.models.config import AgentweldConfig, SourceConfig
15
+ from agentweld.models.tool import ToolDefinition
16
+ from agentweld.sources.registry import get_adapter
17
+ from agentweld.utils.console import console
18
+ from agentweld.utils.errors import (
19
+ AgentweldError,
20
+ ConfigNotFoundError,
21
+ QualityGateError,
22
+ SourceConnectionError,
23
+ )
24
+
25
+ app = typer.Typer(help="Run the full pipeline and generate artifacts.", invoke_without_command=True)
26
+
27
+
28
+ @app.command()
29
+ def generate(
30
+ force: bool = typer.Option(False, "--force", help="Overwrite existing artifacts"),
31
+ only: list[str] = typer.Option([], "--only", help="Only generate specific artifacts"),
32
+ config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to agentweld.yaml"),
33
+ output_dir: Path | None = typer.Option(
34
+ None, "--output-dir", "-o", help="Override output directory"
35
+ ),
36
+ ) -> None:
37
+ """Full pipeline: load config → introspect → curate → compose → generate artifacts."""
38
+
39
+ # 1. Load config
40
+ try:
41
+ cfg = load_config(config_path)
42
+ except ConfigNotFoundError as e:
43
+ console.print(f"[red]Config not found:[/] {e}")
44
+ console.print("[muted]Run [bold]agentweld init[/] to create an agentweld.yaml first.[/]")
45
+ raise typer.Exit(code=1)
46
+
47
+ if not cfg.sources:
48
+ console.print("[yellow]No sources configured. Nothing to generate.[/]")
49
+ raise typer.Exit(code=0)
50
+
51
+ # 2. Introspect all sources concurrently
52
+ console.print("[cyan]Introspecting sources...[/]")
53
+ all_tools: list[ToolDefinition] = []
54
+ errors: dict[str, str] = {}
55
+
56
+ async def _introspect_all() -> None:
57
+ async def _introspect_one(src_cfg: SourceConfig) -> None:
58
+ transport = src_cfg.transport or "stdio"
59
+ try:
60
+ adapter = get_adapter(transport)
61
+ tools = await adapter.introspect(src_cfg)
62
+ all_tools.extend(tools)
63
+ console.print(f" [green]✓[/] {src_cfg.id}: {len(tools)} tool(s)")
64
+ except SourceConnectionError as e:
65
+ errors[src_cfg.id] = str(e)
66
+ console.print(f" [red]✗[/] {src_cfg.id}: {e}")
67
+
68
+ async with anyio.create_task_group() as tg:
69
+ for src_cfg in cfg.sources:
70
+ tg.start_soon(_introspect_one, src_cfg)
71
+
72
+ anyio.run(_introspect_all)
73
+
74
+ if errors and not force:
75
+ console.print(
76
+ f"[red]ERROR:[/] {len(errors)} source(s) failed to connect. "
77
+ "Use [bold]--force[/] to skip failed sources.",
78
+ highlight=False,
79
+ )
80
+ raise typer.Exit(code=1)
81
+
82
+ if not all_tools:
83
+ console.print("[yellow]No tools discovered. Nothing to generate.[/]")
84
+ raise typer.Exit(code=0)
85
+
86
+ console.print(f"[cyan]Total tools discovered:[/] {len(all_tools)}")
87
+
88
+ # 3. Curate
89
+ console.print("[cyan]Running curation engine...[/]")
90
+ try:
91
+ engine = CurationEngine(cfg)
92
+ curated_tools = engine.run(all_tools)
93
+ except AgentweldError as e:
94
+ console.print(f"[red]Curation error:[/] {e}")
95
+ raise typer.Exit(code=1)
96
+
97
+ # 4. Quality gate
98
+ if not force:
99
+ try:
100
+ _check_quality_gate(curated_tools, cfg)
101
+ except QualityGateError as e:
102
+ console.print(f"[red]Quality gate failed:[/] {e}")
103
+ raise typer.Exit(code=1)
104
+
105
+ # 5. Compose
106
+ console.print("[cyan]Composing tool namespace...[/]")
107
+ composed: ComposedToolSet | None = None
108
+ try:
109
+ composer = Composer(cfg)
110
+ composed = composer.compose(curated_tools)
111
+ except AgentweldError as e:
112
+ console.print(f"[red]Composition error:[/] {e}")
113
+ raise typer.Exit(code=1)
114
+
115
+ # 6. Generate artifacts
116
+ out_dir = output_dir or Path(cfg.generate.output_dir)
117
+ if not force and out_dir.exists() and any(out_dir.iterdir()):
118
+ console.print(
119
+ f"[red]ERROR:[/] Output directory [bold]{out_dir}[/] already exists and is non-empty. "
120
+ "Use [bold]--force[/] to overwrite.",
121
+ highlight=False,
122
+ )
123
+ raise typer.Exit(code=1)
124
+
125
+ console.print(f"[cyan]Generating artifacts in[/] {out_dir}...")
126
+ try:
127
+ artifacts = run_generators(
128
+ cfg=cfg,
129
+ tools=curated_tools,
130
+ composed=composed,
131
+ output_dir=out_dir,
132
+ only=only or None,
133
+ force=force,
134
+ )
135
+ _print_artifact_summary(artifacts, out_dir)
136
+ except AgentweldError as e:
137
+ console.print(f"[red]Generator error:[/] {e}")
138
+ raise typer.Exit(code=1)
139
+
140
+
141
+ def _check_quality_gate(tools: list[ToolDefinition], cfg: AgentweldConfig) -> None:
142
+ """Raise QualityGateError if any tool is below the block_below threshold."""
143
+ block_below = cfg.quality.block_below
144
+ blocking = [t for t in tools if t.quality_score is not None and t.quality_score < block_below]
145
+ if blocking:
146
+ names = ", ".join(t.name for t in blocking[:5])
147
+ if len(blocking) > 5:
148
+ names += f" ... and {len(blocking) - 5} more"
149
+ raise QualityGateError(
150
+ f"{len(blocking)} tool(s) below quality threshold "
151
+ f"({block_below:.2f}): {names}. "
152
+ "Fix the tools or run with --force to bypass."
153
+ )
154
+
155
+
156
+ def _print_artifact_summary(artifacts: list[Path], output_dir: Path) -> None:
157
+ """Print a summary of generated artifact files."""
158
+ console.print(f"\n[green]Generated {len(artifacts)} artifact(s) in[/] {output_dir}:")
159
+ for path in sorted(artifacts):
160
+ try:
161
+ rel = path.relative_to(output_dir)
162
+ except ValueError:
163
+ rel = path
164
+ console.print(f" [muted]•[/] {rel}")
agentweld/cli/init.py ADDED
@@ -0,0 +1,197 @@
1
+ """agentweld init — scaffold a new project from an MCP source."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ import anyio
11
+ import typer
12
+
13
+ from agentweld.config.writer import write_new
14
+ from agentweld.models.config import (
15
+ AgentConfig,
16
+ AgentweldConfig,
17
+ CompositionConfig,
18
+ EnrichmentConfig,
19
+ GenerateConfig,
20
+ MetaConfig,
21
+ QualityConfig,
22
+ SourceConfig,
23
+ ToolsConfig,
24
+ )
25
+ from agentweld.sources.registry import get_adapter
26
+ from agentweld.utils.console import console, make_tools_table
27
+ from agentweld.utils.errors import AgentweldError, SourceConnectionError
28
+
29
+ app = typer.Typer(
30
+ help="Initialize a new agentweld project from an MCP source.",
31
+ invoke_without_command=True,
32
+ )
33
+
34
+
35
+ @app.command()
36
+ def init(
37
+ source: str = typer.Argument(..., help="MCP server command (stdio) or URL (http)"),
38
+ from_: str = typer.Option("mcp", "--from", help="Source type"),
39
+ trust: bool = typer.Option(False, "--trust", help="Trust and execute the stdio command"),
40
+ output: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"),
41
+ name: str | None = typer.Option(None, "--name", "-n", help="Agent name"),
42
+ ) -> None:
43
+ """Connect to MCP server, introspect tools, scaffold agentweld.yaml."""
44
+
45
+ # Determine transport
46
+ transport: Literal["stdio", "streamable-http"] = (
47
+ "streamable-http" if source.startswith("http") else "stdio"
48
+ )
49
+
50
+ # Safety gate for stdio
51
+ if transport == "stdio":
52
+ if not trust:
53
+ console.print(
54
+ "[bold red]ERROR:[/] Spawning a stdio MCP server executes arbitrary code. "
55
+ "Re-run with [bold]--trust[/] to confirm.",
56
+ highlight=False,
57
+ )
58
+ raise typer.Exit(code=1)
59
+ console.print(
60
+ f"[bold yellow]WARNING:[/] --trust flag set. Spawning subprocess: [bold]{source}[/]",
61
+ highlight=False,
62
+ )
63
+
64
+ # Build a minimal SourceConfig
65
+ source_id = _derive_source_id(source)
66
+ source_config = SourceConfig(
67
+ id=source_id,
68
+ type="mcp_server",
69
+ transport=transport,
70
+ command=source if transport == "stdio" else None,
71
+ url=source if transport != "stdio" else None,
72
+ )
73
+
74
+ # Introspect
75
+ console.print(f"[cyan]Connecting to[/] {source}...")
76
+ adapter = get_adapter(transport)
77
+ try:
78
+ tools = anyio.run(adapter.introspect, source_config)
79
+ except SourceConnectionError as e:
80
+ console.print(f"[red]Connection failed:[/] {e}")
81
+ raise typer.Exit(code=1)
82
+ except AgentweldError as e:
83
+ console.print(f"[red]Error:[/] {e}")
84
+ raise typer.Exit(code=1)
85
+
86
+ console.print(f"[green]Discovered {len(tools)} tools.[/]")
87
+ tool_rows = [
88
+ {
89
+ "name": t.name,
90
+ "source_id": t.source_id,
91
+ "description": t.description_curated,
92
+ "quality_score": t.quality_score,
93
+ }
94
+ for t in tools
95
+ ]
96
+ console.print(make_tools_table(tool_rows))
97
+
98
+ # Build config
99
+ agent_name = name or _derive_agent_name(source)
100
+ config = _build_initial_config(agent_name, source_config)
101
+
102
+ # Write agentweld.yaml
103
+ output.mkdir(parents=True, exist_ok=True)
104
+ yaml_path = output / "agentweld.yaml"
105
+ write_new(config, yaml_path)
106
+ console.print(f"[green]Created[/] {yaml_path}")
107
+
108
+
109
+ def _derive_source_id(source: str) -> str:
110
+ """Derive a short source ID from a command or URL.
111
+
112
+ Examples:
113
+ "npx @modelcontextprotocol/server-github" -> "github"
114
+ "docker run -i --rm -e TOKEN ghcr.io/github/github-mcp-server" -> "github"
115
+ "https://api.example.com/mcp" -> "example"
116
+ """
117
+ parts = source.replace("https://", "").replace("http://", "").split()
118
+ # For docker commands, find the image argument (last non-flag token after "run")
119
+ if parts and parts[0] == "docker":
120
+ image = _extract_docker_image(parts)
121
+ token = image.split("/")[-1] if image else parts[-1]
122
+ else:
123
+ token = parts[-1] if parts else source
124
+ slug = re.sub(r"[^a-z0-9]", "-", token.lower())
125
+ slug = re.sub(r"-+", "-", slug).strip("-")
126
+ segments = [s for s in slug.split("-") if len(s) > 2]
127
+ # For docker images and similar "name-type-suffix" patterns, prefer the first
128
+ # meaningful segment (e.g. "github-mcp-server" -> "github").
129
+ # For npx packages like "server-github", prefer the last (-> "github").
130
+ if parts and parts[0] == "docker":
131
+ return segments[0] if segments else slug[:20]
132
+ return segments[-1] if segments else slug[:20]
133
+
134
+
135
+ def _extract_docker_image(parts: list[str]) -> str:
136
+ """Return the image name from a 'docker run ...' token list.
137
+
138
+ Skips flags (starting with '-') and their values for known value-taking flags.
139
+ """
140
+ _VALUE_FLAGS = {
141
+ "-e",
142
+ "--env",
143
+ "-p",
144
+ "--publish",
145
+ "--name",
146
+ "-v",
147
+ "--volume",
148
+ "--network",
149
+ "-u",
150
+ "--user",
151
+ "--entrypoint",
152
+ "-w",
153
+ "--workdir",
154
+ }
155
+ skip_next = False
156
+ past_run = False
157
+ for token in parts:
158
+ if token == "run":
159
+ past_run = True
160
+ continue
161
+ if not past_run:
162
+ continue
163
+ if skip_next:
164
+ skip_next = False
165
+ continue
166
+ if token in _VALUE_FLAGS:
167
+ skip_next = True
168
+ continue
169
+ if token.startswith("-"):
170
+ continue
171
+ # First non-flag token after 'run' is the image
172
+ return token
173
+ return ""
174
+
175
+
176
+ def _derive_agent_name(source: str) -> str:
177
+ sid = _derive_source_id(source)
178
+ return f"{sid.title()} Agent"
179
+
180
+
181
+ def _build_initial_config(agent_name: str, source: SourceConfig) -> AgentweldConfig:
182
+ now = datetime.datetime.now(tz=datetime.UTC).replace(microsecond=0)
183
+ return AgentweldConfig(
184
+ meta=MetaConfig(created_at=now, updated_at=now),
185
+ agent=AgentConfig(
186
+ name=agent_name,
187
+ description=f"Agent powered by {source.id}",
188
+ version="0.1.0",
189
+ ),
190
+ sources=[source],
191
+ tools=ToolsConfig(),
192
+ quality=QualityConfig(),
193
+ enrichment=EnrichmentConfig(),
194
+ composition=CompositionConfig(),
195
+ a2a=None,
196
+ generate=GenerateConfig(),
197
+ )
@@ -0,0 +1,171 @@
1
+ """agentweld inspect — inspect tools from configured MCP sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from pathlib import Path
7
+
8
+ import anyio
9
+ import typer
10
+
11
+ from agentweld.config.loader import load_config
12
+ from agentweld.models.config import AgentweldConfig, SourceConfig
13
+ from agentweld.models.tool import ToolDefinition
14
+ from agentweld.sources.registry import get_adapter
15
+ from agentweld.utils.console import console, make_sources_table, make_tools_table
16
+ from agentweld.utils.errors import ConfigNotFoundError, SourceConnectionError
17
+
18
+ # STUB: replace with real import after phase-4/5 merge
19
+ try:
20
+ from agentweld.composition.composer import ComposedToolSet, Composer
21
+ from agentweld.curation.engine import CurationEngine
22
+ except ImportError:
23
+ CurationEngine = None # type: ignore[assignment,misc]
24
+ Composer = None # type: ignore[assignment,misc]
25
+ ComposedToolSet = None # type: ignore[assignment,misc]
26
+
27
+ app = typer.Typer(help="Inspect tools from configured MCP sources.", invoke_without_command=True)
28
+
29
+
30
+ @app.command()
31
+ def inspect(
32
+ source: bool = typer.Option(False, "--source", help="Show raw tools from each source"),
33
+ final: bool = typer.Option(False, "--final", help="Show post-curation tools"),
34
+ conflicts: bool = typer.Option(False, "--conflicts", help="Show naming conflicts"),
35
+ config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to agentweld.yaml"),
36
+ ) -> None:
37
+ """Inspect and display tools from all configured MCP sources."""
38
+
39
+ try:
40
+ cfg = load_config(config_path)
41
+ except ConfigNotFoundError as e:
42
+ console.print(f"[red]Config not found:[/] {e}")
43
+ raise typer.Exit(code=1)
44
+
45
+ if not cfg.sources:
46
+ console.print("[yellow]No sources configured in agentweld.yaml.[/]")
47
+ raise typer.Exit(code=0)
48
+
49
+ # Introspect all sources concurrently
50
+ all_tools: list[ToolDefinition] = []
51
+ tools_by_source: dict[str, list[ToolDefinition]] = {}
52
+ errors: dict[str, str] = {}
53
+
54
+ async def _introspect_all() -> None:
55
+ async def _introspect_one(src_cfg: SourceConfig) -> None:
56
+ transport = src_cfg.transport or "stdio"
57
+ try:
58
+ adapter = get_adapter(transport)
59
+ tools = await adapter.introspect(src_cfg)
60
+ tools_by_source[src_cfg.id] = tools
61
+ all_tools.extend(tools)
62
+ except SourceConnectionError as e:
63
+ errors[src_cfg.id] = str(e)
64
+ tools_by_source[src_cfg.id] = []
65
+
66
+ async with anyio.create_task_group() as tg:
67
+ for src_cfg in cfg.sources:
68
+ tg.start_soon(_introspect_one, src_cfg)
69
+
70
+ anyio.run(_introspect_all)
71
+
72
+ # Report connection errors
73
+ for src_id, err in errors.items():
74
+ console.print(f"[red]ERROR[/] [{src_id}]: {err}")
75
+
76
+ if not source and not final and not conflicts:
77
+ # Default: show summary table
78
+ _show_summary(tools_by_source)
79
+ return
80
+
81
+ if source:
82
+ _show_raw_tools(tools_by_source)
83
+
84
+ if conflicts:
85
+ _show_conflicts(all_tools)
86
+
87
+ if final:
88
+ _show_final_tools(all_tools, cfg)
89
+
90
+
91
+ def _show_summary(tools_by_source: dict[str, list[ToolDefinition]]) -> None:
92
+ """Display a summary table: one row per source."""
93
+ rows = []
94
+ for src_id, tools in tools_by_source.items():
95
+ scored = [t for t in tools if t.quality_score is not None]
96
+ scores = [t.quality_score for t in scored if t.quality_score is not None]
97
+ avg_quality: float | None = sum(scores) / len(scores) if scores else None
98
+ rows.append({"source": src_id, "tools": len(tools), "avg_quality": avg_quality})
99
+ console.print(make_sources_table(rows))
100
+
101
+
102
+ def _show_raw_tools(tools_by_source: dict[str, list[ToolDefinition]]) -> None:
103
+ """Display raw (pre-curation) tools per source."""
104
+ for src_id, tools in tools_by_source.items():
105
+ console.print(f"\n[bold cyan]Source: {src_id}[/] ({len(tools)} tools)")
106
+ if tools:
107
+ tool_rows = [
108
+ {
109
+ "name": t.name,
110
+ "source_id": t.source_id,
111
+ "description": t.description_original,
112
+ "quality_score": t.quality_score,
113
+ }
114
+ for t in tools
115
+ ]
116
+ console.print(make_tools_table(tool_rows, show_quality=True))
117
+ else:
118
+ console.print("[muted] (no tools)[/]")
119
+
120
+
121
+ def _show_conflicts(all_tools: list[ToolDefinition]) -> None:
122
+ """Detect and display tools with duplicate names across sources."""
123
+ name_to_tools: dict[str, list[ToolDefinition]] = defaultdict(list)
124
+ for t in all_tools:
125
+ name_to_tools[t.name].append(t)
126
+
127
+ conflicts_found = {name: tools for name, tools in name_to_tools.items() if len(tools) > 1}
128
+
129
+ if not conflicts_found:
130
+ console.print("[green]No naming conflicts detected.[/]")
131
+ return
132
+
133
+ console.print(f"[bold yellow]Found {len(conflicts_found)} naming conflict(s):[/]")
134
+ for name, tools in conflicts_found.items():
135
+ sources_list = ", ".join(t.source_id for t in tools)
136
+ console.print(f" [bold]{name}[/] — appears in: {sources_list}")
137
+
138
+
139
+ def _show_final_tools(all_tools: list[ToolDefinition], cfg: AgentweldConfig) -> None:
140
+ """Display post-curation tools (requires Phase 4 CurationEngine)."""
141
+ if CurationEngine is None:
142
+ console.print(
143
+ "[yellow]WARNING:[/] CurationEngine not available (Phase 4 not merged). "
144
+ "Showing raw tools instead.",
145
+ highlight=False,
146
+ )
147
+ tool_rows = [
148
+ {
149
+ "name": t.name,
150
+ "source_id": t.source_id,
151
+ "description": t.description_curated,
152
+ "quality_score": t.quality_score,
153
+ }
154
+ for t in all_tools
155
+ ]
156
+ console.print(make_tools_table(tool_rows, show_quality=True))
157
+ return
158
+
159
+ engine = CurationEngine(cfg)
160
+ curated = engine.run(all_tools)
161
+ tool_rows = [
162
+ {
163
+ "name": t.name,
164
+ "source_id": t.source_id,
165
+ "description": t.description_curated,
166
+ "quality_score": t.quality_score,
167
+ }
168
+ for t in curated
169
+ ]
170
+ console.print(f"\n[bold cyan]Post-curation tools[/] ({len(curated)} tools)")
171
+ console.print(make_tools_table(tool_rows, show_quality=True))