Skip to content

Commit 6134d5b

Browse files
author
James Zhu
committed
Fix tool lookup and imports in launch command
1 parent 83f3990 commit 6134d5b

13 files changed

Lines changed: 1704 additions & 372 deletions

code_assistant_manager/cli/app.py

Lines changed: 1302 additions & 117 deletions
Large diffs are not rendered by default.

code_assistant_manager/cli/commands.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,7 @@
4040
UninstallContext,
4141
uninstall,
4242
)
43-
from code_assistant_manager.config import ConfigManager
44-
from code_assistant_manager.menu.base import Colors
45-
from code_assistant_manager.tools import (
46-
display_all_tool_endpoints,
47-
display_tool_endpoints,
48-
get_registered_tools,
49-
)
43+
# Lazy imports moved inside functions to improve startup time
5044

5145
logger = logging.getLogger(__name__)
5246

@@ -56,7 +50,13 @@
5650
# ============================================================================
5751

5852

59-
def _get_config_manager(ctx: Context) -> ConfigManager:
53+
54+
def _get_config_manager(ctx: Context):
55+
# Lazy import
56+
from code_assistant_manager.config import ConfigManager
57+
58+
# Return type hint for clarity
59+
6060
"""Get or create ConfigManager from context."""
6161
try:
6262
config_path = None
@@ -80,6 +80,12 @@ def upgrade(
8080
"""Upgrade CLI tools (alias: u). If not installed, will install.
8181
If installed, will try to upgrade."""
8282
from code_assistant_manager.cli.upgrade import handle_upgrade_command
83+
from code_assistant_manager.config import ConfigManager
84+
from code_assistant_manager.tools import (
85+
display_all_tool_endpoints,
86+
display_tool_endpoints,
87+
get_registered_tools,
88+
)
8389

8490
logger.debug(f"Upgrade command called with target: {target}")
8591
config_path = ctx.obj.get("config_path")
@@ -125,6 +131,10 @@ def doctor(
125131
config: Optional[str] = CONFIG_FILE_OPTION,
126132
):
127133
"""Run diagnostic checks on the code-assistant-manager installation (alias: d)"""
134+
135+
# Lazy imports
136+
from code_assistant_manager.config import ConfigManager
137+
from code_assistant_manager.tools import display_all_tool_endpoints, display_tool_endpoints
128138
# Initialize context object
129139
ctx.ensure_object(dict)
130140
ctx.obj["config_path"] = config
@@ -173,6 +183,10 @@ def doctor(
173183

174184
def launch_alias(ctx: Context, tool_name: str = TOOL_NAME_OPTION):
175185
"""Alias for 'launch' command."""
186+
187+
# Lazy imports
188+
from code_assistant_manager.config import ConfigManager
189+
from code_assistant_manager.tools import get_registered_tools
176190
# Initialize context object
177191
ctx.ensure_object(dict)
178192
ctx.obj["config_path"] = None
@@ -420,6 +434,10 @@ def validate_config(
420434
verbose: bool = VALIDATE_VERBOSE_OPTION,
421435
):
422436
"""Validate the configuration file for syntax and semantic errors."""
437+
438+
# Lazy imports
439+
from code_assistant_manager.config import ConfigManager
440+
from code_assistant_manager.menu.base import Colors
423441
try:
424442
cm = ConfigManager(config)
425443
typer.echo(

code_assistant_manager/cli/plugins/plugin_management_commands.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
from code_assistant_manager.menu.base import Colors
1212
from code_assistant_manager.plugins import (
13-
BUILTIN_PLUGIN_REPOS,
1413
VALID_APP_TYPES,
1514
PluginManager,
1615
PluginRepo,

code_assistant_manager/fetching/parsers.py

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,28 @@ def parse_from_file(
3939

4040
# Find the repo root by looking for .git directory
4141
repo_root = skill_dir
42-
for parent in skill_dir.parents:
43-
if (parent / '.git').exists():
44-
repo_root = parent
45-
break
42+
# Check current dir first
43+
if (skill_dir / '.git').exists():
44+
repo_root = skill_dir
4645
else:
47-
# Fallback to the original logic if .git not found
48-
repo_root = skill_dir.parents[-2]
46+
for parent in skill_dir.parents:
47+
if (parent / '.git').exists():
48+
repo_root = parent
49+
break
50+
else:
51+
# Fallback to the original logic if .git not found
52+
try:
53+
repo_root = skill_dir.parents[-2]
54+
except IndexError:
55+
repo_root = skill_dir.parent
4956

5057
# Get relative path from repo root to skill directory
5158
try:
52-
source_directory = str(skill_dir.relative_to(repo_root))
59+
repo_relative_path = str(skill_dir.relative_to(repo_root))
5360
except ValueError:
54-
source_directory = directory
61+
repo_relative_path = directory
62+
63+
source_directory = repo_relative_path
5564

5665
# If skills_path is set, source_directory should be relative to skills_path
5766
if repo_config.path:
@@ -64,9 +73,16 @@ def parse_from_file(
6473
# If we can't make it relative, keep the full path but warn
6574
logger.warning(f"Skill directory {skill_dir} is not under skills_path {repo_config.path}")
6675

76+
# Determine directory name for key
77+
if skill_dir == repo_root:
78+
# Use repo name for root skills if directory would be "."
79+
directory = repo_config.name if repo_config.name else "."
80+
else:
81+
directory = skill_dir.name
82+
6783
# Create skill entity
6884
skill = Skill(
69-
key=self.create_entity_key(repo_config, directory),
85+
key=self.create_entity_key(repo_config, source_directory),
7086
name=meta.get("name", directory),
7187
description=meta.get("description", ""),
7288
directory=directory,
@@ -75,7 +91,7 @@ def parse_from_file(
7591
repo_name=repo_config.name,
7692
repo_branch=repo_config.branch,
7793
skills_path=repo_config.path,
78-
readme_url=f"https://github.com/{repo_config.owner}/{repo_config.name}/tree/{repo_config.branch}/{source_directory}",
94+
readme_url=f"https://github.com/{repo_config.owner}/{repo_config.name}/tree/{repo_config.branch}/{repo_relative_path}",
7995
source_directory=source_directory,
8096
)
8197

@@ -91,23 +107,44 @@ def create_entity_key(self, repo_config: RepoConfig, entity_name: str) -> str:
91107

92108
def _parse_metadata(self, skill_md: Path) -> dict:
93109
"""Parse skill metadata from SKILL.md."""
94-
# This is a simplified version - in reality this would parse YAML frontmatter
95-
# from the existing handler logic
96110
meta = {"name": "", "description": ""}
97111

98112
try:
99113
with open(skill_md, 'r', encoding='utf-8') as f:
100114
content = f.read()
101115

102-
# Simple parsing - look for name and description
116+
# Parse frontmatter manually to avoid dependencies if needed,
117+
# or just to handle simple key: value pairs
103118
lines = content.split('\n')
104-
for line in lines[:10]: # Check first 10 lines
105-
line = line.strip()
106-
if line.startswith('# '):
107-
meta["name"] = line[2:].strip()
108-
elif line.startswith('description:') or line.startswith('Description:'):
109-
meta["description"] = line.split(':', 1)[1].strip()
110-
break
119+
frontmatter = []
120+
in_frontmatter = False
121+
122+
for line in lines:
123+
if line.strip() == '---':
124+
if not in_frontmatter:
125+
in_frontmatter = True
126+
continue
127+
else:
128+
break
129+
if in_frontmatter:
130+
frontmatter.append(line)
131+
132+
if frontmatter:
133+
for line in frontmatter:
134+
if ':' in line:
135+
key, value = line.split(':', 1)
136+
key = key.strip()
137+
value = value.strip().strip('"').strip("'")
138+
if key in meta or key == "name": # Allow updating name and description
139+
meta[key] = value
140+
141+
# Fallback to H1 title if name not found in frontmatter
142+
if not meta["name"]:
143+
for line in lines:
144+
if line.strip().startswith('# '):
145+
meta["name"] = line.strip()[2:].strip()
146+
break
147+
111148
except Exception as e:
112149
logger.warning(f"Failed to parse metadata from {skill_md}: {e}")
113150

code_assistant_manager/plugins/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
)
2323
from .gemini import GeminiPluginHandler
2424
from .manager import (
25-
BUILTIN_PLUGIN_REPOS,
2625
PLUGIN_HANDLERS,
2726
VALID_APP_TYPES,
2827
PluginManager,
@@ -55,5 +54,4 @@
5554
# Constants
5655
"PLUGIN_HANDLERS",
5756
"VALID_APP_TYPES",
58-
"BUILTIN_PLUGIN_REPOS",
5957
]

code_assistant_manager/plugins/manager.py

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ def _load_builtin_plugin_repos() -> Dict[str, PluginRepo]:
2121
2222
Returns bundled repos as PluginRepo objects for fallback.
2323
"""
24-
from .fetch import fetch_repo_info
25-
2624
package_dir = Path(__file__).parent.parent
2725
repos_file = package_dir / "plugin_repos.json"
2826

@@ -32,23 +30,9 @@ def _load_builtin_plugin_repos() -> Dict[str, PluginRepo]:
3230
with open(repos_file, "r", encoding="utf-8") as f:
3331
data = json.load(f)
3432
for key, repo_data in data.items():
35-
# For marketplace repos, fetch the actual name from the repo's marketplace.json
33+
# Use configured name or fallback to key
3634
name = repo_data.get("name", key)
37-
if repo_data.get("type") == "marketplace" and repo_data.get("repoOwner") and repo_data.get("repoName"):
38-
try:
39-
# Fetch the actual name from the repo's marketplace.json
40-
repo_info = fetch_repo_info(
41-
repo_data["repoOwner"],
42-
repo_data["repoName"],
43-
repo_data.get("repoBranch", "main")
44-
)
45-
if repo_info and repo_info.name:
46-
name = repo_info.name
47-
logger.debug(f"Using fetched name '{name}' for {key} from marketplace.json")
48-
except Exception as e:
49-
logger.warning(f"Failed to fetch name for {key} from marketplace.json: {e}")
50-
# Fall back to the configured name
51-
35+
5236
repos[key] = PluginRepo(
5337
name=name,
5438
description=repo_data.get("description", ""),
@@ -76,8 +60,6 @@ def _load_plugin_repos_from_config(config_dir: Optional[Path] = None) -> Dict[st
7660
Returns:
7761
Dictionary of PluginRepo objects
7862
"""
79-
from .fetch import fetch_repo_info
80-
8163
loader = RepoConfigLoader(config_dir)
8264
bundled_fallback_dict = _load_builtin_plugin_repos()
8365

@@ -102,22 +84,8 @@ def _load_plugin_repos_from_config(config_dir: Optional[Path] = None) -> Dict[st
10284
# Convert back to PluginRepo objects
10385
repos: Dict[str, PluginRepo] = {}
10486
for key, repo_data in repos_dict.items():
105-
# For marketplace repos, fetch the actual name from the repo's marketplace.json
87+
# Use configured name or fallback to key
10688
name = repo_data.get("name", key)
107-
if repo_data.get("type") == "marketplace" and repo_data.get("repoOwner") and repo_data.get("repoName"):
108-
try:
109-
# Fetch the actual name from the repo's marketplace.json
110-
repo_info = fetch_repo_info(
111-
repo_data["repoOwner"],
112-
repo_data["repoName"],
113-
repo_data.get("repoBranch", "main")
114-
)
115-
if repo_info and repo_info.name:
116-
name = repo_info.name
117-
logger.debug(f"Using fetched name '{name}' for {key} from marketplace.json")
118-
except Exception as e:
119-
logger.warning(f"Failed to fetch name for {key} from marketplace.json: {e}")
120-
# Fall back to the configured name
12189

12290
repos[key] = PluginRepo(
12391
name=name,
@@ -145,8 +113,16 @@ def _load_plugin_repos_from_config(config_dir: Optional[Path] = None) -> Dict[st
145113
# Valid app types that support plugins
146114
VALID_APP_TYPES = list(PLUGIN_HANDLERS.keys())
147115

148-
# Built-in plugin repositories
149-
BUILTIN_PLUGIN_REPOS = _load_plugin_repos_from_config()
116+
# Cache for plugin repositories
117+
_CACHED_PLUGIN_REPOS: Optional[Dict[str, PluginRepo]] = None
118+
119+
120+
def _get_plugin_repos_lazy() -> Dict[str, PluginRepo]:
121+
"""Get all plugin repos (lazy loaded)."""
122+
global _CACHED_PLUGIN_REPOS
123+
if _CACHED_PLUGIN_REPOS is None:
124+
_CACHED_PLUGIN_REPOS = _load_plugin_repos_from_config()
125+
return _CACHED_PLUGIN_REPOS
150126

151127

152128
def get_handler(app_type: str) -> BasePluginHandler:
@@ -328,13 +304,14 @@ def remove_marketplace(self, name: str) -> None:
328304

329305
def get_builtin_repos(self) -> Dict[str, PluginRepo]:
330306
"""Get all built-in plugin repositories."""
331-
return BUILTIN_PLUGIN_REPOS.copy()
307+
return _get_plugin_repos_lazy().copy()
332308

333309
def get_builtin_repo(self, name: str) -> Optional[PluginRepo]:
334310
"""Get a built-in plugin repository by name (supports aliases)."""
335-
resolved = self._resolve_repo_name(name, self.get_builtin_repos())
311+
repos = self.get_builtin_repos()
312+
resolved = self._resolve_repo_name(name, repos)
336313
if resolved:
337-
return BUILTIN_PLUGIN_REPOS.get(resolved)
314+
return repos.get(resolved)
338315
return None
339316

340317
# ==================== User Plugin Repos ====================
@@ -496,7 +473,7 @@ def install(
496473
else:
497474
raise ValueError(
498475
f"Invalid source: {source}. Must be a local path, GitHub repo (owner/repo), "
499-
f"or built-in repo name: {list(BUILTIN_PLUGIN_REPOS.keys())}"
476+
f"or built-in repo name: {list(_get_plugin_repos_lazy().keys())}"
500477
)
501478

502479
# Save to registry

code_assistant_manager/tools/registry.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
import time
34
from pathlib import Path
45
from typing import Dict, List, Optional
56

@@ -9,7 +10,7 @@
910

1011

1112
class ToolRegistry:
12-
"""Registry for external CLI tools loaded from tools.yaml with lazy loading."""
13+
"""Registry for external CLI tools loaded from tools.yaml with lazy loading and caching."""
1314

1415
def __init__(self, config_path: Optional[Path] = None):
1516
env_override = os.environ.get("CODE_ASSISTANT_MANAGER_TOOLS_FILE")
@@ -23,6 +24,8 @@ def __init__(self, config_path: Optional[Path] = None):
2324
Path(__file__).resolve().parent.parent.parent / "tools.yaml"
2425
)
2526
self._tools = None # Lazy load on first access
27+
self._cache_time = None
28+
self._cache_ttl = 30 # Cache for 30 seconds
2629

2730
def _load(self) -> Dict[str, dict]:
2831
"""Load tools from packaged resources first, then fall back to file path.
@@ -81,11 +84,14 @@ def _load(self) -> Dict[str, dict]:
8184
return tools if isinstance(tools, dict) else {}
8285

8386
def _ensure_loaded(self):
84-
if self._tools is None:
87+
# Check if cache is still valid
88+
if self._tools is None or self._cache_time is None or (time.time() - self._cache_time) > self._cache_ttl:
8589
self._tools = self._load()
90+
self._cache_time = time.time()
8691

8792
def reload(self):
8893
self._tools = self._load()
94+
self._cache_time = time.time()
8995

9096
def get_tool(self, tool_key: str) -> dict:
9197
self._ensure_loaded()

0 commit comments

Comments
 (0)