llmcode-cli 1.0.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.
- llm_code/__init__.py +2 -0
- llm_code/analysis/__init__.py +6 -0
- llm_code/analysis/cache.py +33 -0
- llm_code/analysis/engine.py +256 -0
- llm_code/analysis/go_rules.py +114 -0
- llm_code/analysis/js_rules.py +84 -0
- llm_code/analysis/python_rules.py +311 -0
- llm_code/analysis/rules.py +140 -0
- llm_code/analysis/rust_rules.py +108 -0
- llm_code/analysis/universal_rules.py +111 -0
- llm_code/api/__init__.py +0 -0
- llm_code/api/client.py +90 -0
- llm_code/api/errors.py +73 -0
- llm_code/api/openai_compat.py +390 -0
- llm_code/api/provider.py +35 -0
- llm_code/api/sse.py +52 -0
- llm_code/api/types.py +140 -0
- llm_code/cli/__init__.py +0 -0
- llm_code/cli/commands.py +70 -0
- llm_code/cli/image.py +122 -0
- llm_code/cli/render.py +214 -0
- llm_code/cli/status_line.py +79 -0
- llm_code/cli/streaming.py +92 -0
- llm_code/cli/tui_main.py +220 -0
- llm_code/computer_use/__init__.py +11 -0
- llm_code/computer_use/app_detect.py +49 -0
- llm_code/computer_use/app_tier.py +57 -0
- llm_code/computer_use/coordinator.py +99 -0
- llm_code/computer_use/input_control.py +71 -0
- llm_code/computer_use/screenshot.py +93 -0
- llm_code/cron/__init__.py +13 -0
- llm_code/cron/parser.py +145 -0
- llm_code/cron/scheduler.py +135 -0
- llm_code/cron/storage.py +126 -0
- llm_code/enterprise/__init__.py +1 -0
- llm_code/enterprise/audit.py +59 -0
- llm_code/enterprise/auth.py +26 -0
- llm_code/enterprise/oidc.py +95 -0
- llm_code/enterprise/rbac.py +65 -0
- llm_code/harness/__init__.py +5 -0
- llm_code/harness/config.py +33 -0
- llm_code/harness/engine.py +129 -0
- llm_code/harness/guides.py +41 -0
- llm_code/harness/sensors.py +68 -0
- llm_code/harness/templates.py +84 -0
- llm_code/hida/__init__.py +1 -0
- llm_code/hida/classifier.py +187 -0
- llm_code/hida/engine.py +49 -0
- llm_code/hida/profiles.py +95 -0
- llm_code/hida/types.py +28 -0
- llm_code/ide/__init__.py +1 -0
- llm_code/ide/bridge.py +80 -0
- llm_code/ide/detector.py +76 -0
- llm_code/ide/server.py +169 -0
- llm_code/logging.py +29 -0
- llm_code/lsp/__init__.py +0 -0
- llm_code/lsp/client.py +298 -0
- llm_code/lsp/detector.py +42 -0
- llm_code/lsp/manager.py +56 -0
- llm_code/lsp/tools.py +288 -0
- llm_code/marketplace/__init__.py +0 -0
- llm_code/marketplace/builtin_registry.py +102 -0
- llm_code/marketplace/installer.py +162 -0
- llm_code/marketplace/plugin.py +78 -0
- llm_code/marketplace/registry.py +360 -0
- llm_code/mcp/__init__.py +0 -0
- llm_code/mcp/bridge.py +87 -0
- llm_code/mcp/client.py +117 -0
- llm_code/mcp/health.py +120 -0
- llm_code/mcp/manager.py +214 -0
- llm_code/mcp/oauth.py +219 -0
- llm_code/mcp/transport.py +254 -0
- llm_code/mcp/types.py +53 -0
- llm_code/remote/__init__.py +0 -0
- llm_code/remote/client.py +136 -0
- llm_code/remote/protocol.py +22 -0
- llm_code/remote/server.py +275 -0
- llm_code/remote/ssh_proxy.py +56 -0
- llm_code/runtime/__init__.py +0 -0
- llm_code/runtime/auto_commit.py +56 -0
- llm_code/runtime/auto_diagnose.py +62 -0
- llm_code/runtime/checkpoint.py +70 -0
- llm_code/runtime/checkpoint_recovery.py +142 -0
- llm_code/runtime/compaction.py +35 -0
- llm_code/runtime/compressor.py +415 -0
- llm_code/runtime/config.py +533 -0
- llm_code/runtime/context.py +49 -0
- llm_code/runtime/conversation.py +921 -0
- llm_code/runtime/cost_tracker.py +126 -0
- llm_code/runtime/dream.py +127 -0
- llm_code/runtime/file_protection.py +150 -0
- llm_code/runtime/hardware.py +85 -0
- llm_code/runtime/hooks.py +223 -0
- llm_code/runtime/indexer.py +230 -0
- llm_code/runtime/knowledge_compiler.py +232 -0
- llm_code/runtime/memory.py +132 -0
- llm_code/runtime/memory_layers.py +467 -0
- llm_code/runtime/memory_lint.py +252 -0
- llm_code/runtime/model_aliases.py +37 -0
- llm_code/runtime/ollama.py +93 -0
- llm_code/runtime/overlay.py +124 -0
- llm_code/runtime/permissions.py +200 -0
- llm_code/runtime/plan.py +45 -0
- llm_code/runtime/prompt.py +238 -0
- llm_code/runtime/repo_map.py +174 -0
- llm_code/runtime/sandbox.py +116 -0
- llm_code/runtime/session.py +268 -0
- llm_code/runtime/skill_resolver.py +61 -0
- llm_code/runtime/skills.py +133 -0
- llm_code/runtime/speculative.py +75 -0
- llm_code/runtime/streaming_executor.py +216 -0
- llm_code/runtime/telemetry.py +196 -0
- llm_code/runtime/token_budget.py +26 -0
- llm_code/runtime/vcr.py +142 -0
- llm_code/runtime/vision.py +102 -0
- llm_code/swarm/__init__.py +1 -0
- llm_code/swarm/backend_subprocess.py +108 -0
- llm_code/swarm/backend_tmux.py +103 -0
- llm_code/swarm/backend_worktree.py +306 -0
- llm_code/swarm/checkpoint.py +74 -0
- llm_code/swarm/coordinator.py +236 -0
- llm_code/swarm/mailbox.py +88 -0
- llm_code/swarm/manager.py +202 -0
- llm_code/swarm/memory_sync.py +80 -0
- llm_code/swarm/recovery.py +21 -0
- llm_code/swarm/team.py +67 -0
- llm_code/swarm/types.py +31 -0
- llm_code/task/__init__.py +16 -0
- llm_code/task/diagnostics.py +93 -0
- llm_code/task/manager.py +162 -0
- llm_code/task/types.py +112 -0
- llm_code/task/verifier.py +104 -0
- llm_code/tools/__init__.py +0 -0
- llm_code/tools/agent.py +145 -0
- llm_code/tools/agent_roles.py +82 -0
- llm_code/tools/base.py +94 -0
- llm_code/tools/bash.py +565 -0
- llm_code/tools/computer_use_tools.py +278 -0
- llm_code/tools/coordinator_tool.py +75 -0
- llm_code/tools/cron_create.py +90 -0
- llm_code/tools/cron_delete.py +49 -0
- llm_code/tools/cron_list.py +51 -0
- llm_code/tools/deferred.py +92 -0
- llm_code/tools/dump.py +116 -0
- llm_code/tools/edit_file.py +282 -0
- llm_code/tools/git_tools.py +531 -0
- llm_code/tools/glob_search.py +112 -0
- llm_code/tools/grep_search.py +144 -0
- llm_code/tools/ide_diagnostics.py +59 -0
- llm_code/tools/ide_open.py +58 -0
- llm_code/tools/ide_selection.py +52 -0
- llm_code/tools/memory_tools.py +138 -0
- llm_code/tools/multi_edit.py +143 -0
- llm_code/tools/notebook_edit.py +107 -0
- llm_code/tools/notebook_read.py +81 -0
- llm_code/tools/parsing.py +63 -0
- llm_code/tools/read_file.py +154 -0
- llm_code/tools/registry.py +58 -0
- llm_code/tools/search_backends/__init__.py +56 -0
- llm_code/tools/search_backends/brave.py +56 -0
- llm_code/tools/search_backends/duckduckgo.py +129 -0
- llm_code/tools/search_backends/searxng.py +71 -0
- llm_code/tools/search_backends/tavily.py +73 -0
- llm_code/tools/swarm_create.py +109 -0
- llm_code/tools/swarm_delete.py +95 -0
- llm_code/tools/swarm_list.py +44 -0
- llm_code/tools/swarm_message.py +109 -0
- llm_code/tools/task_close.py +79 -0
- llm_code/tools/task_plan.py +79 -0
- llm_code/tools/task_verify.py +90 -0
- llm_code/tools/tool_search.py +65 -0
- llm_code/tools/web_common.py +258 -0
- llm_code/tools/web_fetch.py +223 -0
- llm_code/tools/web_search.py +280 -0
- llm_code/tools/write_file.py +118 -0
- llm_code/tui/__init__.py +1 -0
- llm_code/tui/app.py +2432 -0
- llm_code/tui/chat_view.py +82 -0
- llm_code/tui/chat_widgets.py +309 -0
- llm_code/tui/header_bar.py +46 -0
- llm_code/tui/input_bar.py +349 -0
- llm_code/tui/keybindings.py +142 -0
- llm_code/tui/marketplace.py +210 -0
- llm_code/tui/status_bar.py +72 -0
- llm_code/tui/theme.py +96 -0
- llm_code/utils/__init__.py +0 -0
- llm_code/utils/diff.py +111 -0
- llm_code/utils/errors.py +70 -0
- llm_code/utils/hyperlink.py +73 -0
- llm_code/utils/notebook.py +179 -0
- llm_code/utils/search.py +69 -0
- llm_code/utils/text_normalize.py +28 -0
- llm_code/utils/version_check.py +62 -0
- llm_code/vim/__init__.py +4 -0
- llm_code/vim/engine.py +51 -0
- llm_code/vim/motions.py +172 -0
- llm_code/vim/operators.py +183 -0
- llm_code/vim/text_objects.py +139 -0
- llm_code/vim/transitions.py +279 -0
- llm_code/vim/types.py +68 -0
- llm_code/voice/__init__.py +1 -0
- llm_code/voice/languages.py +43 -0
- llm_code/voice/recorder.py +136 -0
- llm_code/voice/stt.py +36 -0
- llm_code/voice/stt_anthropic.py +66 -0
- llm_code/voice/stt_google.py +32 -0
- llm_code/voice/stt_whisper.py +52 -0
- llmcode_cli-1.0.0.dist-info/METADATA +524 -0
- llmcode_cli-1.0.0.dist-info/RECORD +212 -0
- llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
- llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
- llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Built-in registry of known Claude Code plugins (official + community)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
OFFICIAL_PLUGINS = [
|
|
5
|
+
{"name": "superpowers", "desc": "Core skills: TDD, debugging, collaboration, plans", "skills": 28, "repo": "obra/superpowers"},
|
|
6
|
+
{"name": "data-engineering", "desc": "Data warehouse exploration, pipeline audit, SQL", "skills": 46, "repo": ""},
|
|
7
|
+
{"name": "searchfit-seo", "desc": "AI-powered SEO toolkit — audit, content strategy", "skills": 22, "repo": ""},
|
|
8
|
+
{"name": "chrome-devtools-mcp", "desc": "Browser automation, debugging, performance analysis", "skills": 8, "repo": "anthropics/claude-code-chrome-devtools-mcp"},
|
|
9
|
+
{"name": "figma", "desc": "Figma MCP server + design-to-code skills", "skills": 7, "repo": ""},
|
|
10
|
+
{"name": "frontend-design", "desc": "UI/UX implementation skill for web frontends", "skills": 3, "repo": ""},
|
|
11
|
+
{"name": "skill-creator", "desc": "Create, improve, and measure skill performance", "skills": 3, "repo": ""},
|
|
12
|
+
{"name": "remember", "desc": "Continuous memory — extract and persist context", "skills": 2, "repo": ""},
|
|
13
|
+
{"name": "claude-code-setup", "desc": "Analyze codebases, recommend Claude Code automations", "skills": 1, "repo": ""},
|
|
14
|
+
{"name": "claude-md-management", "desc": "Audit and improve CLAUDE.md files", "skills": 1, "repo": ""},
|
|
15
|
+
{"name": "agent-sdk-dev", "desc": "Claude Agent SDK development tools", "skills": 0, "repo": ""},
|
|
16
|
+
{"name": "code-review", "desc": "Automated code review with specialized agents", "skills": 0, "repo": ""},
|
|
17
|
+
{"name": "code-simplifier", "desc": "Simplify and refine code for clarity", "skills": 0, "repo": ""},
|
|
18
|
+
{"name": "commit-commands", "desc": "Streamline git workflow — commit, push, PR", "skills": 0, "repo": ""},
|
|
19
|
+
{"name": "context7", "desc": "Context7 MCP for up-to-date documentation lookup", "skills": 0, "repo": ""},
|
|
20
|
+
{"name": "explanatory-output-style", "desc": "Educational insights about implementation choices", "skills": 0, "repo": ""},
|
|
21
|
+
{"name": "feature-dev", "desc": "Feature development workflow with specialized agents", "skills": 0, "repo": ""},
|
|
22
|
+
{"name": "playwright", "desc": "Browser automation and E2E testing (Microsoft)", "skills": 0, "repo": ""},
|
|
23
|
+
{"name": "pr-review-toolkit", "desc": "Comprehensive PR review with specialized agents", "skills": 0, "repo": ""},
|
|
24
|
+
{"name": "ralph-loop", "desc": "Continuous self-referential AI loops", "skills": 0, "repo": ""},
|
|
25
|
+
{"name": "security-guidance", "desc": "Security reminder hooks for safe coding", "skills": 0, "repo": ""},
|
|
26
|
+
{"name": "semgrep", "desc": "Semgrep MCP for static analysis and SAST", "skills": 0, "repo": ""},
|
|
27
|
+
{"name": "supabase", "desc": "Supabase MCP for database and auth operations", "skills": 0, "repo": ""},
|
|
28
|
+
{"name": "clangd-lsp", "desc": "C/C++ language server integration", "skills": 0, "repo": ""},
|
|
29
|
+
{"name": "gopls-lsp", "desc": "Go language server integration", "skills": 0, "repo": ""},
|
|
30
|
+
{"name": "pyright-lsp", "desc": "Python language server integration", "skills": 0, "repo": ""},
|
|
31
|
+
{"name": "rust-analyzer-lsp", "desc": "Rust language server integration", "skills": 0, "repo": ""},
|
|
32
|
+
{"name": "typescript-lsp", "desc": "TypeScript language server integration", "skills": 0, "repo": ""},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
COMMUNITY_PLUGINS = [
|
|
36
|
+
{"name": "ai-integration-architect", "desc": "Design and scaffold AI integration into enterprise systems", "skills": 1, "repo": ""},
|
|
37
|
+
{"name": "claude-md-optimizer", "desc": "Optimize oversized CLAUDE.md using progressive disclosure", "skills": 1, "repo": ""},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_all_known_plugins() -> list[dict]:
|
|
42
|
+
"""Return all known plugins (official + community) sorted by skill count."""
|
|
43
|
+
all_plugins = []
|
|
44
|
+
for p in OFFICIAL_PLUGINS:
|
|
45
|
+
all_plugins.append({**p, "source": "official"})
|
|
46
|
+
for p in COMMUNITY_PLUGINS:
|
|
47
|
+
all_plugins.append({**p, "source": "community"})
|
|
48
|
+
all_plugins.sort(key=lambda x: (-x["skills"], x["name"]))
|
|
49
|
+
return all_plugins
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def search_clawhub_skills(query: str = "", limit: int = 30) -> list[tuple[str, str]]:
|
|
53
|
+
"""Search ClawHub.ai skill marketplace (44,000+ skills)."""
|
|
54
|
+
import httpx
|
|
55
|
+
|
|
56
|
+
# If no query, fetch popular categories to get a good mix
|
|
57
|
+
queries = [query] if query else ["code", "test", "review", "debug", "deploy", "security", "api", "frontend"]
|
|
58
|
+
|
|
59
|
+
results: list[tuple[str, str]] = []
|
|
60
|
+
seen_slugs: set[str] = set()
|
|
61
|
+
|
|
62
|
+
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
63
|
+
for q in queries:
|
|
64
|
+
if len(results) >= limit:
|
|
65
|
+
break
|
|
66
|
+
try:
|
|
67
|
+
resp = await client.get(
|
|
68
|
+
"https://clawhub.ai/api/search",
|
|
69
|
+
params={"q": q, "limit": min(10, limit - len(results))},
|
|
70
|
+
)
|
|
71
|
+
resp.raise_for_status()
|
|
72
|
+
data = resp.json()
|
|
73
|
+
for item in data.get("results", []):
|
|
74
|
+
slug = item.get("slug", "")
|
|
75
|
+
if slug and slug not in seen_slugs:
|
|
76
|
+
seen_slugs.add(slug)
|
|
77
|
+
name = item.get("displayName") or slug
|
|
78
|
+
summary = item.get("summary", "")[:60]
|
|
79
|
+
results.append((slug, f"{name} — {summary}"))
|
|
80
|
+
except Exception:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
return results[:limit]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def search_clawhub_plugins(query: str, limit: int = 30) -> list[tuple[str, str]]:
|
|
87
|
+
"""Search ClawHub.ai plugin marketplace."""
|
|
88
|
+
import httpx
|
|
89
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
90
|
+
resp = await client.get(
|
|
91
|
+
"https://clawhub.ai/api/search",
|
|
92
|
+
params={"q": f"plugin {query}", "limit": limit},
|
|
93
|
+
)
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
data = resp.json()
|
|
96
|
+
results = []
|
|
97
|
+
for item in data.get("results", []):
|
|
98
|
+
name = item.get("displayName") or item.get("slug", "")
|
|
99
|
+
slug = item.get("slug", "")
|
|
100
|
+
summary = item.get("summary", "")[:70]
|
|
101
|
+
results.append((slug, f"{name} — {summary}"))
|
|
102
|
+
return results
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Plugin installer — local copy, npm, and GitHub install strategies."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from llm_code.marketplace.plugin import InstalledPlugin, PluginManifest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# State file format:
|
|
14
|
+
# {
|
|
15
|
+
# "plugin-name": {"enabled": true, "installed_from": "local"}
|
|
16
|
+
# }
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PluginInstaller:
|
|
20
|
+
"""Manages installation, removal, and enumeration of plugins."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, install_dir: Path) -> None:
|
|
23
|
+
self._install_dir = install_dir
|
|
24
|
+
self._install_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
self._state_path = self._install_dir / "state.json"
|
|
26
|
+
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
# State helpers
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def _read_state(self) -> dict[str, dict[str, Any]]:
|
|
32
|
+
if not self._state_path.exists():
|
|
33
|
+
return {}
|
|
34
|
+
return json.loads(self._state_path.read_text(encoding="utf-8"))
|
|
35
|
+
|
|
36
|
+
def _write_state(self, state: dict[str, dict[str, Any]]) -> None:
|
|
37
|
+
self._state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Install strategies
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def install_from_local(self, source: Path) -> Path:
|
|
44
|
+
"""Copy a local plugin directory into the install directory.
|
|
45
|
+
|
|
46
|
+
Returns the destination path.
|
|
47
|
+
"""
|
|
48
|
+
manifest = PluginManifest.from_path(source)
|
|
49
|
+
dest = self._install_dir / manifest.name
|
|
50
|
+
if dest.exists():
|
|
51
|
+
shutil.rmtree(dest)
|
|
52
|
+
shutil.copytree(source, dest)
|
|
53
|
+
|
|
54
|
+
state = self._read_state()
|
|
55
|
+
state[manifest.name] = {"enabled": True, "installed_from": "local"}
|
|
56
|
+
self._write_state(state)
|
|
57
|
+
|
|
58
|
+
return dest
|
|
59
|
+
|
|
60
|
+
async def install_from_npm(self, package: str, version: str = "latest") -> Path:
|
|
61
|
+
"""Install a plugin package via npm --prefix (uses execvp, not shell).
|
|
62
|
+
|
|
63
|
+
Returns the destination path.
|
|
64
|
+
"""
|
|
65
|
+
dest = self._install_dir / package.replace("/", "__")
|
|
66
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
pkg_spec = f"{package}@{version}" if version != "latest" else package
|
|
68
|
+
|
|
69
|
+
proc = await asyncio.create_subprocess_exec(
|
|
70
|
+
"npm", "install", "--prefix", str(dest), pkg_spec,
|
|
71
|
+
stdout=asyncio.subprocess.PIPE,
|
|
72
|
+
stderr=asyncio.subprocess.PIPE,
|
|
73
|
+
)
|
|
74
|
+
await proc.communicate()
|
|
75
|
+
|
|
76
|
+
state = self._read_state()
|
|
77
|
+
state[package] = {"enabled": True, "installed_from": "npm"}
|
|
78
|
+
self._write_state(state)
|
|
79
|
+
|
|
80
|
+
return dest
|
|
81
|
+
|
|
82
|
+
async def install_from_github(self, repo: str, ref: str = "main") -> Path:
|
|
83
|
+
"""Clone a GitHub repository as a plugin using git clone (uses execvp, not shell).
|
|
84
|
+
|
|
85
|
+
Returns the destination path.
|
|
86
|
+
"""
|
|
87
|
+
name = repo.replace("/", "__")
|
|
88
|
+
dest = self._install_dir / name
|
|
89
|
+
if dest.exists():
|
|
90
|
+
shutil.rmtree(dest)
|
|
91
|
+
|
|
92
|
+
url = f"https://github.com/{repo}.git"
|
|
93
|
+
proc = await asyncio.create_subprocess_exec(
|
|
94
|
+
"git", "clone", "--depth", "1", "--branch", ref, url, str(dest),
|
|
95
|
+
stdout=asyncio.subprocess.PIPE,
|
|
96
|
+
stderr=asyncio.subprocess.PIPE,
|
|
97
|
+
)
|
|
98
|
+
await proc.communicate()
|
|
99
|
+
|
|
100
|
+
state = self._read_state()
|
|
101
|
+
state[name] = {"enabled": True, "installed_from": "github"}
|
|
102
|
+
self._write_state(state)
|
|
103
|
+
|
|
104
|
+
return dest
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
# Management
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def uninstall(self, name: str) -> None:
|
|
111
|
+
"""Remove an installed plugin by name."""
|
|
112
|
+
dest = self._install_dir / name
|
|
113
|
+
if dest.exists():
|
|
114
|
+
shutil.rmtree(dest)
|
|
115
|
+
|
|
116
|
+
state = self._read_state()
|
|
117
|
+
state.pop(name, None)
|
|
118
|
+
self._write_state(state)
|
|
119
|
+
|
|
120
|
+
def list_installed(self) -> list[InstalledPlugin]:
|
|
121
|
+
"""Return all installed plugins, merging directory scan with state.json."""
|
|
122
|
+
state = self._read_state()
|
|
123
|
+
plugins: list[InstalledPlugin] = []
|
|
124
|
+
|
|
125
|
+
for entry in sorted(self._install_dir.iterdir()):
|
|
126
|
+
if not entry.is_dir():
|
|
127
|
+
continue
|
|
128
|
+
if entry.name == "state.json":
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
manifest = PluginManifest.from_path(entry)
|
|
132
|
+
except FileNotFoundError:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
entry_state = state.get(manifest.name, {})
|
|
136
|
+
enabled = bool(entry_state.get("enabled", True))
|
|
137
|
+
installed_from = str(entry_state.get("installed_from", "local"))
|
|
138
|
+
|
|
139
|
+
plugins.append(
|
|
140
|
+
InstalledPlugin(
|
|
141
|
+
manifest=manifest,
|
|
142
|
+
path=entry,
|
|
143
|
+
enabled=enabled,
|
|
144
|
+
installed_from=installed_from,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return plugins
|
|
149
|
+
|
|
150
|
+
def enable(self, name: str) -> None:
|
|
151
|
+
"""Mark a plugin as enabled in state.json."""
|
|
152
|
+
state = self._read_state()
|
|
153
|
+
entry = state.setdefault(name, {})
|
|
154
|
+
state[name] = {**entry, "enabled": True}
|
|
155
|
+
self._write_state(state)
|
|
156
|
+
|
|
157
|
+
def disable(self, name: str) -> None:
|
|
158
|
+
"""Mark a plugin as disabled in state.json."""
|
|
159
|
+
state = self._read_state()
|
|
160
|
+
entry = state.setdefault(name, {})
|
|
161
|
+
state[name] = {**entry, "enabled": False}
|
|
162
|
+
self._write_state(state)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Plugin manifest and installed-plugin models for the marketplace subsystem."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class PluginManifest:
|
|
12
|
+
"""Immutable representation of a parsed .claude-plugin/plugin.json manifest."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
version: str
|
|
16
|
+
description: str
|
|
17
|
+
author: dict[str, Any] | None = None
|
|
18
|
+
homepage: str | None = None
|
|
19
|
+
repository: str | None = None
|
|
20
|
+
keywords: tuple[str, ...] = ()
|
|
21
|
+
commands: tuple[str, ...] = ()
|
|
22
|
+
agents: tuple[str, ...] | None = None
|
|
23
|
+
skills: str | tuple[str, ...] | None = None
|
|
24
|
+
hooks: dict[str, Any] | None = None
|
|
25
|
+
mcp_servers: tuple[dict[str, Any], ...] | None = None
|
|
26
|
+
lsp_servers: tuple[dict[str, Any], ...] | None = None
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_path(cls, plugin_dir: Path) -> "PluginManifest":
|
|
30
|
+
"""Read .claude-plugin/plugin.json from plugin_dir and return a PluginManifest.
|
|
31
|
+
|
|
32
|
+
Raises FileNotFoundError if plugin_dir does not exist or plugin.json is missing.
|
|
33
|
+
"""
|
|
34
|
+
if not plugin_dir.exists():
|
|
35
|
+
raise FileNotFoundError(f"Plugin directory not found: {plugin_dir}")
|
|
36
|
+
|
|
37
|
+
manifest_path = plugin_dir / ".claude-plugin" / "plugin.json"
|
|
38
|
+
if not manifest_path.exists():
|
|
39
|
+
raise FileNotFoundError(f"Plugin manifest not found: {manifest_path}")
|
|
40
|
+
|
|
41
|
+
data: dict[str, Any] = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
42
|
+
|
|
43
|
+
# Map camelCase JSON keys to snake_case fields
|
|
44
|
+
mcp_raw = data.get("mcpServers")
|
|
45
|
+
lsp_raw = data.get("lspServers")
|
|
46
|
+
|
|
47
|
+
keywords_raw = data.get("keywords", [])
|
|
48
|
+
commands_raw = data.get("commands", [])
|
|
49
|
+
agents_raw = data.get("agents")
|
|
50
|
+
skills_raw = data.get("skills")
|
|
51
|
+
hooks_raw = data.get("hooks")
|
|
52
|
+
|
|
53
|
+
return cls(
|
|
54
|
+
name=data["name"],
|
|
55
|
+
version=data["version"],
|
|
56
|
+
description=data["description"],
|
|
57
|
+
author=data.get("author"),
|
|
58
|
+
homepage=data.get("homepage"),
|
|
59
|
+
repository=data.get("repository"),
|
|
60
|
+
keywords=tuple(keywords_raw) if keywords_raw else (),
|
|
61
|
+
commands=tuple(commands_raw) if commands_raw else (),
|
|
62
|
+
agents=tuple(agents_raw) if agents_raw is not None else None,
|
|
63
|
+
skills=skills_raw if isinstance(skills_raw, str) else (tuple(skills_raw) if skills_raw is not None else None),
|
|
64
|
+
hooks=hooks_raw,
|
|
65
|
+
mcp_servers=tuple(mcp_raw) if mcp_raw is not None else None,
|
|
66
|
+
lsp_servers=tuple(lsp_raw) if lsp_raw is not None else None,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class InstalledPlugin:
|
|
72
|
+
"""Immutable snapshot of an installed plugin with runtime state."""
|
|
73
|
+
|
|
74
|
+
manifest: PluginManifest
|
|
75
|
+
path: Path
|
|
76
|
+
enabled: bool
|
|
77
|
+
scope: str = "user"
|
|
78
|
+
installed_from: str = "local"
|