forgeoptimizer 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 (159) hide show
  1. forgecli/__init__.py +4 -0
  2. forgecli/build/__init__.py +135 -0
  3. forgecli/build/apply.py +218 -0
  4. forgecli/build/caveman_optimize.py +37 -0
  5. forgecli/build/diff_extract.py +218 -0
  6. forgecli/build/llm.py +167 -0
  7. forgecli/build/optimize.py +37 -0
  8. forgecli/build/pipeline.py +76 -0
  9. forgecli/build/retrieval.py +157 -0
  10. forgecli/build/summarize.py +76 -0
  11. forgecli/build/test_run.py +75 -0
  12. forgecli/builder/__init__.py +11 -0
  13. forgecli/builder/builder.py +53 -0
  14. forgecli/builder/editor.py +36 -0
  15. forgecli/builder/formatter.py +31 -0
  16. forgecli/cli/__init__.py +5 -0
  17. forgecli/cli/bootstrap.py +281 -0
  18. forgecli/cli/commands_graph.py +149 -0
  19. forgecli/cli/commands_wrappers.py +80 -0
  20. forgecli/cli/daemon.py +744 -0
  21. forgecli/cli/main.py +234 -0
  22. forgecli/cli/ui.py +56 -0
  23. forgecli/config/__init__.py +28 -0
  24. forgecli/config/loader.py +85 -0
  25. forgecli/config/settings.py +206 -0
  26. forgecli/config/writer.py +90 -0
  27. forgecli/core/__init__.py +35 -0
  28. forgecli/core/container.py +68 -0
  29. forgecli/core/context.py +54 -0
  30. forgecli/core/credentials.py +114 -0
  31. forgecli/core/errors.py +27 -0
  32. forgecli/core/events.py +67 -0
  33. forgecli/core/logging.py +47 -0
  34. forgecli/core/models.py +214 -0
  35. forgecli/core/plugins.py +54 -0
  36. forgecli/core/service.py +22 -0
  37. forgecli/docs/__init__.py +5 -0
  38. forgecli/docs/generator.py +115 -0
  39. forgecli/engine/__init__.py +105 -0
  40. forgecli/engine/context.py +163 -0
  41. forgecli/engine/defaults.py +89 -0
  42. forgecli/engine/events.py +185 -0
  43. forgecli/engine/execution.py +519 -0
  44. forgecli/engine/plugins.py +145 -0
  45. forgecli/engine/runner.py +158 -0
  46. forgecli/engine/stages/__init__.py +33 -0
  47. forgecli/engine/stages/caveman_optimizer.py +47 -0
  48. forgecli/engine/stages/context_optimizer.py +44 -0
  49. forgecli/engine/stages/execution_engine_stage.py +102 -0
  50. forgecli/engine/stages/git_engine.py +30 -0
  51. forgecli/engine/stages/intent_analyzer.py +38 -0
  52. forgecli/engine/stages/model_router.py +65 -0
  53. forgecli/engine/stages/planning_engine.py +45 -0
  54. forgecli/engine/stages/repository_analyzer.py +54 -0
  55. forgecli/engine/stages/validation_engine.py +87 -0
  56. forgecli/git/__init__.py +9 -0
  57. forgecli/git/repo.py +55 -0
  58. forgecli/git/service.py +45 -0
  59. forgecli/graph/__init__.py +56 -0
  60. forgecli/graph/backend_graphify.py +448 -0
  61. forgecli/graph/edge.py +19 -0
  62. forgecli/graph/graph.py +85 -0
  63. forgecli/graph/graphify.py +405 -0
  64. forgecli/graph/indexer.py +82 -0
  65. forgecli/graph/node.py +47 -0
  66. forgecli/graph/repository.py +164 -0
  67. forgecli/memory/__init__.py +12 -0
  68. forgecli/memory/cache.py +61 -0
  69. forgecli/memory/history.py +136 -0
  70. forgecli/memory/store.py +85 -0
  71. forgecli/optimizer/__init__.py +13 -0
  72. forgecli/optimizer/caveman/__init__.py +169 -0
  73. forgecli/optimizer/caveman/cli.py +62 -0
  74. forgecli/optimizer/caveman/decorator.py +67 -0
  75. forgecli/optimizer/caveman/factory.py +51 -0
  76. forgecli/optimizer/caveman/ruleset.py +156 -0
  77. forgecli/optimizer/caveman/state.py +50 -0
  78. forgecli/optimizer/chunker.py +85 -0
  79. forgecli/optimizer/optimizer.py +81 -0
  80. forgecli/optimizer/ponytail/__init__.py +181 -0
  81. forgecli/optimizer/ponytail/cli.py +159 -0
  82. forgecli/optimizer/ponytail/decorator.py +70 -0
  83. forgecli/optimizer/ponytail/factory.py +51 -0
  84. forgecli/optimizer/ponytail/ruleset.py +168 -0
  85. forgecli/optimizer/ponytail/state.py +51 -0
  86. forgecli/optimizer/ranker.py +37 -0
  87. forgecli/optimizer/summarizer.py +44 -0
  88. forgecli/orchestrator/__init__.py +706 -0
  89. forgecli/planner/__init__.py +56 -0
  90. forgecli/planner/agent.py +61 -0
  91. forgecli/planner/plan.py +65 -0
  92. forgecli/planner/planner.py +17 -0
  93. forgecli/planner/render.py +267 -0
  94. forgecli/planner/serialize.py +104 -0
  95. forgecli/planner/software.py +832 -0
  96. forgecli/platform/__init__.py +91 -0
  97. forgecli/platform/core.py +201 -0
  98. forgecli/platform/deps.py +361 -0
  99. forgecli/platform/paths.py +234 -0
  100. forgecli/platform/shell.py +176 -0
  101. forgecli/platform/update.py +253 -0
  102. forgecli/plugins/__init__.py +249 -0
  103. forgecli/prompts/__init__.py +11 -0
  104. forgecli/prompts/loader.py +28 -0
  105. forgecli/prompts/registry.py +32 -0
  106. forgecli/prompts/renderer.py +23 -0
  107. forgecli/providers/__init__.py +39 -0
  108. forgecli/providers/anthropic.py +207 -0
  109. forgecli/providers/base.py +204 -0
  110. forgecli/providers/builtin.py +26 -0
  111. forgecli/providers/conversation.py +74 -0
  112. forgecli/providers/google.py +290 -0
  113. forgecli/providers/http_base.py +207 -0
  114. forgecli/providers/mock.py +111 -0
  115. forgecli/providers/openai.py +206 -0
  116. forgecli/providers/openai_compatible.py +787 -0
  117. forgecli/providers/router.py +340 -0
  118. forgecli/providers/router_state.py +89 -0
  119. forgecli/review/__init__.py +45 -0
  120. forgecli/review/analyzer.py +124 -0
  121. forgecli/review/analyzers/__init__.py +1 -0
  122. forgecli/review/analyzers/architecture.py +255 -0
  123. forgecli/review/analyzers/complexity.py +162 -0
  124. forgecli/review/analyzers/dead_code.py +255 -0
  125. forgecli/review/analyzers/duplicates.py +161 -0
  126. forgecli/review/analyzers/performance.py +161 -0
  127. forgecli/review/analyzers/security.py +244 -0
  128. forgecli/review/finding.py +68 -0
  129. forgecli/review/report.py +321 -0
  130. forgecli/review/repository.py +130 -0
  131. forgecli/review/suggestions.py +98 -0
  132. forgecli/runtime/__init__.py +6 -0
  133. forgecli/runtime/cache_store.py +75 -0
  134. forgecli/runtime/mcp_config.py +118 -0
  135. forgecli/runtime/prepare.py +203 -0
  136. forgecli/runtime/wrappers.py +150 -0
  137. forgecli/sdk/__init__.py +132 -0
  138. forgecli/sdk/events.py +206 -0
  139. forgecli/sdk/interfaces.py +282 -0
  140. forgecli/sdk/loader.py +239 -0
  141. forgecli/sdk/manager.py +678 -0
  142. forgecli/sdk/manifest.py +395 -0
  143. forgecli/sdk/sandbox.py +247 -0
  144. forgecli/sdk/version.py +313 -0
  145. forgecli/templates/__init__.py +9 -0
  146. forgecli/templates/engine.py +37 -0
  147. forgecli/templates/registry.py +32 -0
  148. forgecli/utils/__init__.py +19 -0
  149. forgecli/utils/fs.py +121 -0
  150. forgecli/utils/ids.py +11 -0
  151. forgecli/utils/io.py +27 -0
  152. forgecli/utils/paths.py +78 -0
  153. forgecli/utils/stats.py +179 -0
  154. forgecli/utils/timing.py +25 -0
  155. forgeoptimizer-0.1.0.dist-info/METADATA +177 -0
  156. forgeoptimizer-0.1.0.dist-info/RECORD +159 -0
  157. forgeoptimizer-0.1.0.dist-info/WHEEL +4 -0
  158. forgeoptimizer-0.1.0.dist-info/entry_points.txt +2 -0
  159. forgeoptimizer-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,118 @@
1
+ """Automated configuration and detection of Forge MCP server for AI CLIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from forgecli.cli.ui import info, success
10
+
11
+
12
+ def configure_mcp_for_all(repo_root: Path) -> None:
13
+ """Detect and automatically configure Forge MCP server for Claude, Cursor, and local project."""
14
+ configure_claude_mcp()
15
+ configure_cursor_mcp(repo_root)
16
+ configure_project_local_mcp(repo_root)
17
+
18
+
19
+ def configure_claude_mcp() -> None:
20
+ """Ensure ~/.claude.json is configured with the forge MCP server."""
21
+ claude_config_path = Path.home() / ".claude.json"
22
+ forge_mcp_entry = {"command": "forge", "args": ["mcp"]}
23
+
24
+ try:
25
+ data: dict = {}
26
+ if claude_config_path.exists():
27
+ content = claude_config_path.read_text(encoding="utf-8").strip()
28
+ if content:
29
+ try:
30
+ data = json.loads(content)
31
+ except json.JSONDecodeError:
32
+ info("Could not parse ~/.claude.json as valid JSON. Skipping auto-config.")
33
+ return
34
+
35
+ if "mcpServers" not in data or not isinstance(data["mcpServers"], dict):
36
+ data["mcpServers"] = {}
37
+
38
+ if "forge" not in data["mcpServers"]:
39
+ data["mcpServers"]["forge"] = forge_mcp_entry
40
+ claude_config_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
41
+ success("✨ Configured Forge MCP server in ~/.claude.json for Claude Code.")
42
+ except Exception as e:
43
+ info(f"Note: Could not auto-configure Claude Code MCP: {e}")
44
+ info("You can manually configure Claude Code using: claude mcp add forge -- forge mcp")
45
+
46
+
47
+ def configure_cursor_mcp(repo_root: Path) -> None:
48
+ """Ensure Cursor is configured with the forge MCP server globally and project-locally."""
49
+ # 1. Global ~/.cursor/mcp.json
50
+ cursor_global_path = Path.home() / ".cursor" / "mcp.json"
51
+ forge_mcp_entry = {"command": "forge", "args": ["mcp"]}
52
+
53
+ try:
54
+ cursor_global_path.parent.mkdir(parents=True, exist_ok=True)
55
+ data: dict = {}
56
+ if cursor_global_path.exists():
57
+ content = cursor_global_path.read_text(encoding="utf-8").strip()
58
+ if content:
59
+ with contextlib.suppress(json.JSONDecodeError):
60
+ data = json.loads(content)
61
+
62
+ if "mcpServers" not in data or not isinstance(data["mcpServers"], dict):
63
+ data["mcpServers"] = {}
64
+
65
+ if "forge" not in data["mcpServers"]:
66
+ data["mcpServers"]["forge"] = forge_mcp_entry
67
+ cursor_global_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
68
+ success("✨ Configured Forge MCP server in ~/.cursor/mcp.json for Cursor.")
69
+ except Exception as e:
70
+ info(f"Note: Could not auto-configure Cursor global MCP: {e}")
71
+
72
+ # 2. Project-local .cursor/mcp.json
73
+ cursor_local_dir = repo_root / ".cursor"
74
+ cursor_local_path = cursor_local_dir / "mcp.json"
75
+ try:
76
+ cursor_local_dir.mkdir(parents=True, exist_ok=True)
77
+ data = {}
78
+ if cursor_local_path.exists():
79
+ content = cursor_local_path.read_text(encoding="utf-8").strip()
80
+ if content:
81
+ with contextlib.suppress(json.JSONDecodeError):
82
+ data = json.loads(content)
83
+
84
+ if "mcpServers" not in data or not isinstance(data["mcpServers"], dict):
85
+ data["mcpServers"] = {}
86
+
87
+ if "forge" not in data["mcpServers"]:
88
+ data["mcpServers"]["forge"] = forge_mcp_entry
89
+ cursor_local_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
90
+ success(
91
+ f"✨ Configured Forge MCP server in {repo_root.name}/.cursor/mcp.json for Cursor."
92
+ )
93
+ except Exception as e:
94
+ info(f"Note: Could not auto-configure Cursor local MCP: {e}")
95
+
96
+
97
+ def configure_project_local_mcp(repo_root: Path) -> None:
98
+ """Create a project-local .mcp.json file for tools that support auto-discovery."""
99
+ mcp_local_path = repo_root / ".mcp.json"
100
+ forge_mcp_entry = {"command": "forge", "args": ["mcp"]}
101
+
102
+ try:
103
+ data: dict = {}
104
+ if mcp_local_path.exists():
105
+ content = mcp_local_path.read_text(encoding="utf-8").strip()
106
+ if content:
107
+ with contextlib.suppress(json.JSONDecodeError):
108
+ data = json.loads(content)
109
+
110
+ if "mcpServers" not in data or not isinstance(data["mcpServers"], dict):
111
+ data["mcpServers"] = {}
112
+
113
+ if "forge" not in data["mcpServers"]:
114
+ data["mcpServers"]["forge"] = forge_mcp_entry
115
+ mcp_local_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
116
+ success("✨ Created project-local .mcp.json for auto-discovery of Forge MCP server.")
117
+ except Exception as e:
118
+ info(f"Note: Could not write project-local .mcp.json: {e}")
@@ -0,0 +1,203 @@
1
+ """Fast repository context for AI wrapper commands — no full codebase indexing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from forgecli.optimizer.optimizer import ContextOptimizer
10
+ from forgecli.optimizer.ponytail import Intensity, PonytailRulesetOptimizer
11
+ from forgecli.providers.base import ChatMessage, ChatRequest, Role
12
+ from forgecli.runtime.cache_store import (
13
+ CachedRuntime,
14
+ load_runtime_cache,
15
+ repo_fingerprint,
16
+ save_runtime_cache,
17
+ )
18
+ from forgecli.utils.paths import ProjectPaths
19
+
20
+ _SKIP_DIRS = {
21
+ ".git",
22
+ ".venv",
23
+ "__pycache__",
24
+ "node_modules",
25
+ "dist",
26
+ "build",
27
+ ".forge",
28
+ "graphify-out",
29
+ ".mypy_cache",
30
+ ".pytest_cache",
31
+ ".ruff_cache",
32
+ }
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class PreparedRuntime:
37
+ """Optimized repository context ready for a wrapper CLI."""
38
+
39
+ root: Path
40
+ context_summary: str
41
+ context_file: Path
42
+ from_cache: bool
43
+
44
+ @classmethod
45
+ def from_cached(cls, root: Path, cached: CachedRuntime) -> PreparedRuntime:
46
+ return cls(
47
+ root=root,
48
+ context_summary=cached.context_summary,
49
+ context_file=Path(cached.context_file),
50
+ from_cache=True,
51
+ )
52
+
53
+
54
+ def resolve_repo_root(start: Path) -> Path:
55
+ """Return the nearest git repository root, or ``start`` when not in a repo."""
56
+ start = start.resolve()
57
+ for candidate in (start, *start.parents):
58
+ if (candidate / ".git").exists():
59
+ return candidate
60
+ return start
61
+
62
+
63
+ def _git_line(root: Path) -> str | None:
64
+ from forgecli.platform.shell import run
65
+
66
+ branch = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
67
+ if branch.ok and branch.stdout.strip():
68
+ return f"Branch: {branch.stdout.strip()}"
69
+ return None
70
+
71
+
72
+ def _scan_repo_light(root: Path) -> str:
73
+ """Build a small repo snapshot without indexing the full codebase."""
74
+ lines = [
75
+ f"Repository: {root.name}",
76
+ f"Root: {root}",
77
+ ]
78
+ git_info = _git_line(root)
79
+ if git_info:
80
+ lines.append(git_info)
81
+
82
+ layout: list[str] = []
83
+ try:
84
+ for entry in sorted(root.iterdir(), key=lambda p: p.name.lower())[:36]:
85
+ if entry.name in _SKIP_DIRS or entry.name.startswith("."):
86
+ continue
87
+ if entry.is_dir():
88
+ layout.append(f"{entry.name}/")
89
+ try:
90
+ for child in sorted(entry.iterdir(), key=lambda p: p.name.lower())[:6]:
91
+ if child.name in _SKIP_DIRS or child.name.startswith("."):
92
+ continue
93
+ suffix = "/" if child.is_dir() else ""
94
+ layout.append(f" {child.name}{suffix}")
95
+ except OSError:
96
+ pass
97
+ else:
98
+ layout.append(entry.name)
99
+ except OSError:
100
+ pass
101
+
102
+ if layout:
103
+ lines.extend(["", "Project layout (shallow scan):", *layout[:72]])
104
+
105
+ graph_marker = root / "graphify-out" / "graph.json"
106
+ if graph_marker.is_file():
107
+ lines.append("")
108
+ lines.append(
109
+ "Knowledge graph already on disk at graphify-out/. "
110
+ "Run `forge graph build` separately when you want a full refresh."
111
+ )
112
+
113
+ return "\n".join(lines)
114
+
115
+
116
+ def _optimize_prompt(context: str) -> str:
117
+ import asyncio
118
+
119
+ optimizer = PonytailRulesetOptimizer(intensity=Intensity.LITE)
120
+ request = ChatRequest(
121
+ model="runtime",
122
+ messages=[
123
+ ChatMessage(
124
+ role=Role.SYSTEM,
125
+ content="Prepare concise repository context for an AI coding assistant.",
126
+ ),
127
+ ChatMessage(
128
+ role=Role.USER,
129
+ content=(
130
+ "Optimize this repo snapshot for the next coding task. "
131
+ "Prefer reuse, minimal diffs, and existing patterns.\n\n"
132
+ f"{context}"
133
+ ),
134
+ ),
135
+ ],
136
+ )
137
+ optimized = asyncio.run(optimizer.optimize_chat(request))
138
+ user = next(
139
+ (message.content for message in optimized.request.messages if message.role is Role.USER),
140
+ context,
141
+ )
142
+ return user.strip() or context
143
+
144
+
145
+ def _optimize_tokens(text: str) -> str:
146
+ optimizer = ContextOptimizer(max_context_tokens=8_000)
147
+ result = optimizer.optimize(text)
148
+ if not result.chunks:
149
+ return text[:12_000]
150
+ merged = "\n".join(chunk.text for chunk in result.chunks[:24])
151
+ return merged[:12_000]
152
+
153
+
154
+ def prepare_runtime_sync(
155
+ start: Path,
156
+ *,
157
+ force: bool = False,
158
+ quiet: bool = False,
159
+ ) -> PreparedRuntime:
160
+ """Prepare lightweight optimized context — no full graph build."""
161
+ root = resolve_repo_root(start)
162
+ fingerprint = repo_fingerprint(root)
163
+
164
+ if not force:
165
+ cached = load_runtime_cache(fingerprint)
166
+ if cached is not None:
167
+ return PreparedRuntime.from_cached(root, cached)
168
+
169
+ raw_context = _scan_repo_light(root)
170
+ prompt_optimized = _optimize_prompt(raw_context)
171
+ token_optimized = _optimize_tokens(prompt_optimized)
172
+
173
+ # Optimization pipeline check: never allow optimized context to be larger than raw context
174
+ if len(token_optimized) > len(raw_context):
175
+ token_optimized = raw_context
176
+
177
+ cache_dir = ProjectPaths.from_env().cache_dir / "runtime" / "context"
178
+ cache_dir.mkdir(parents=True, exist_ok=True)
179
+ context_file = cache_dir / f"{fingerprint}.md"
180
+ context_file.write_text(token_optimized, encoding="utf-8")
181
+
182
+ save_runtime_cache(
183
+ fingerprint,
184
+ CachedRuntime(
185
+ context_summary=token_optimized,
186
+ context_file=str(context_file),
187
+ node_count=0,
188
+ edge_count=0,
189
+ created_at=time.time(),
190
+ ),
191
+ )
192
+
193
+ if not quiet:
194
+ from forgecli.cli.ui import info
195
+
196
+ info(f"Optimized context for [accent]{root.name}[/accent] (cached for reuse)")
197
+
198
+ return PreparedRuntime(
199
+ root=root,
200
+ context_summary=token_optimized,
201
+ context_file=context_file,
202
+ from_cache=False,
203
+ )
@@ -0,0 +1,150 @@
1
+ """Launch Claude Code, Codex, Cursor, OpenCode, and CommandCode CLI with Forge-optimized context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ import typer
11
+
12
+ from forgecli.cli.ui import error, get_console, info
13
+ from forgecli.platform.shell import which
14
+ from forgecli.runtime.prepare import prepare_runtime_sync, resolve_repo_root
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class WrapperSpec:
19
+ name: str
20
+ binary: str
21
+ install_hint: str
22
+
23
+
24
+ WRAPPERS: dict[str, WrapperSpec] = {
25
+ "claude": WrapperSpec(
26
+ name="Claude Code",
27
+ binary="claude",
28
+ install_hint="Install Claude Code: https://docs.anthropic.com/en/docs/claude-code",
29
+ ),
30
+ "codex": WrapperSpec(
31
+ name="Codex CLI",
32
+ binary="codex",
33
+ install_hint="Install OpenAI Codex CLI: https://developers.openai.com/codex/cli/",
34
+ ),
35
+ "cursor": WrapperSpec(
36
+ name="Cursor CLI",
37
+ binary="cursor",
38
+ install_hint="Install Cursor CLI: https://cursor.com/docs/cli/overview",
39
+ ),
40
+ "opencode": WrapperSpec(
41
+ name="OpenCode CLI",
42
+ binary="opencode",
43
+ install_hint="Install OpenCode CLI: https://opencode.ai/",
44
+ ),
45
+ "commandcode": WrapperSpec(
46
+ name="CommandCode CLI",
47
+ binary="commandcode",
48
+ install_hint="Install CommandCode CLI: https://commandcode.ai/",
49
+ ),
50
+ "antigravity": WrapperSpec(
51
+ name="Antigravity CLI",
52
+ binary="antigravity",
53
+ install_hint="Install Antigravity CLI",
54
+ ),
55
+ }
56
+
57
+
58
+ def launch_wrapper(
59
+ wrapper_id: str,
60
+ extra_args: list[str] | None = None,
61
+ *,
62
+ path: Path | None = None,
63
+ force_prepare: bool = False,
64
+ ) -> None:
65
+ """Prepare lightweight context and launch the selected AI CLI."""
66
+ spec = WRAPPERS.get(wrapper_id)
67
+ if spec is None:
68
+ error(f"Unknown wrapper: {wrapper_id}")
69
+ raise typer.Exit(code=1)
70
+
71
+ if extra_args:
72
+ error(
73
+ "Passing prompts or extra arguments to wrapper commands is not supported. Please run the command without arguments to launch the interactive CLI."
74
+ )
75
+ raise typer.Exit(code=1)
76
+
77
+ binary_path = which(spec.binary)
78
+ if binary_path is None and wrapper_id == "commandcode":
79
+ # Check alternative binary names for commandcode
80
+ binary_path = which("command-code")
81
+
82
+ if binary_path is None:
83
+ console = get_console()
84
+ console.print(f"[red]✗[/red] {spec.name} is not installed.")
85
+ console.print(spec.install_hint)
86
+ raise typer.Exit(code=1)
87
+
88
+ # 1. Detect current repository
89
+ cwd = (path or Path.cwd()).resolve()
90
+ repo_root = resolve_repo_root(cwd)
91
+
92
+ # 2. Ensure the daemon is running
93
+ import time
94
+
95
+ import httpx
96
+
97
+ from forgecli.cli.daemon import is_daemon_running, start_daemon_background
98
+
99
+ if not is_daemon_running():
100
+ info("Forge Runtime daemon is not running. Starting it in the background...")
101
+ start_daemon_background()
102
+ # Wait a moment for it to spin up
103
+ for _ in range(20):
104
+ if is_daemon_running():
105
+ break
106
+ time.sleep(0.2)
107
+
108
+ # 3. Optimize context, prompts, and tokens (aggressively reusing cache)
109
+ import time
110
+
111
+ from forgecli.utils.stats import record_wrapper_stats
112
+
113
+ start_time = time.perf_counter()
114
+ prepared = prepare_runtime_sync(repo_root, force=force_prepare, quiet=False)
115
+ prep_time = time.perf_counter() - start_time
116
+
117
+ record_wrapper_stats(wrapper_id, repo_root, prepared, prep_time)
118
+
119
+ if prepared.from_cache:
120
+ info("Reusing cached Forge context.")
121
+
122
+ # 4. Notify daemon to refresh/ensure context is registered
123
+ try:
124
+ with httpx.Client(timeout=2.0) as client:
125
+ client.get(f"http://127.0.0.1:16868/context?path={repo_root}")
126
+ except Exception:
127
+ pass
128
+
129
+ # 5. Automatically configure MCP server for launched CLIs
130
+ try:
131
+ from forgecli.runtime.mcp_config import configure_mcp_for_all
132
+
133
+ configure_mcp_for_all(repo_root)
134
+ except Exception:
135
+ pass
136
+
137
+ env = os.environ.copy()
138
+ env["FORGE_CONTEXT"] = prepared.context_summary
139
+ env["FORGE_CONTEXT_FILE"] = str(prepared.context_file)
140
+ env["FORGE_REPO_ROOT"] = str(prepared.root)
141
+
142
+ argv = [binary_path, *(extra_args or [])]
143
+ info(f"Launching [accent]{spec.name}[/accent] ...")
144
+
145
+ completed = subprocess.run(
146
+ argv,
147
+ cwd=str(prepared.root),
148
+ env=env,
149
+ )
150
+ raise typer.Exit(code=completed.returncode)
@@ -0,0 +1,132 @@
1
+ """The ForgeCLI Plugin SDK.
2
+
3
+ The SDK is the *contract* third-party developers use to extend
4
+ ForgeCLI. It provides:
5
+
6
+ * :mod:`forgecli.sdk.manifest` — typed plugin metadata (TOML).
7
+ * :mod:`forgecli.sdk.version` — semver parsing + dep resolution.
8
+ * :mod:`forgecli.sdk.loader` — discovery (filesystem + entry-point).
9
+ * :mod:`forgecli.sdk.events` — pub/sub bus + hook manager.
10
+ * :mod:`forgecli.sdk.sandbox` — restricted-exec for plugin callbacks.
11
+ * :mod:`forgecli.sdk.interfaces` — the 10 canonical plugin types.
12
+ * :mod:`forgecli.sdk.manager` — the :class:`PluginManager` façade
13
+ (install / enable / disable / uninstall / update / configure).
14
+
15
+ See :file:`PLUGINS.md` for the end-to-end developer guide.
16
+ """
17
+
18
+ from forgecli.sdk.events import (
19
+ HookManager,
20
+ PluginEvent,
21
+ PluginEventBus,
22
+ PluginEventKind,
23
+ PluginHook,
24
+ )
25
+ from forgecli.sdk.interfaces import (
26
+ AIProviderPlugin,
27
+ CodeGeneratorPlugin,
28
+ ContextOptimizerPlugin,
29
+ DeploymentProviderPlugin,
30
+ DocumentationGeneratorPlugin,
31
+ GitProviderPlugin,
32
+ HealthIssue,
33
+ HealthReport,
34
+ NotificationProviderPlugin,
35
+ ObservabilityProviderPlugin,
36
+ PluginConfigurable,
37
+ PluginHealthCheck,
38
+ RepositoryAnalyzerPlugin,
39
+ TestRunnerPlugin,
40
+ )
41
+ from forgecli.sdk.loader import (
42
+ LoadedPlugin,
43
+ default_plugins_dir,
44
+ discover_entry_points,
45
+ discover_filesystem,
46
+ load_filesystem,
47
+ )
48
+ from forgecli.sdk.loader import (
49
+ PluginManifestNotFoundError as PluginManifestNotFound,
50
+ )
51
+ from forgecli.sdk.manager import (
52
+ PluginAlreadyInstalledError,
53
+ PluginCompatibilityError,
54
+ PluginError,
55
+ PluginManager,
56
+ PluginNotFoundError,
57
+ PluginRegistryState,
58
+ PluginState,
59
+ )
60
+ from forgecli.sdk.manifest import (
61
+ Compatibility,
62
+ EntryPoint,
63
+ EntryPointKind,
64
+ Permission,
65
+ PluginManifest,
66
+ is_valid_plugin_name,
67
+ )
68
+ from forgecli.sdk.sandbox import Sandbox, ScopedBuiltins, run_sandboxed, sandbox
69
+ from forgecli.sdk.version import (
70
+ DependencyCycleError,
71
+ Op,
72
+ Requirement,
73
+ Spec,
74
+ UnsatisfiableRequirementError,
75
+ Version,
76
+ VersionParseError,
77
+ resolve,
78
+ )
79
+
80
+ __all__ = [
81
+ "AIProviderPlugin",
82
+ "CodeGeneratorPlugin",
83
+ "Compatibility",
84
+ "ContextOptimizerPlugin",
85
+ "DependencyCycleError",
86
+ "DeploymentProviderPlugin",
87
+ "DocumentationGeneratorPlugin",
88
+ "EntryPoint",
89
+ "EntryPointKind",
90
+ "GitProviderPlugin",
91
+ "HealthIssue",
92
+ "HealthReport",
93
+ "HookManager",
94
+ "LoadedPlugin",
95
+ "NotificationProviderPlugin",
96
+ "ObservabilityProviderPlugin",
97
+ "Op",
98
+ "Permission",
99
+ "PluginAlreadyInstalledError",
100
+ "PluginCompatibilityError",
101
+ "PluginConfigurable",
102
+ "PluginError",
103
+ "PluginEvent",
104
+ "PluginEventBus",
105
+ "PluginEventKind",
106
+ "PluginHealthCheck",
107
+ "PluginHook",
108
+ "PluginManager",
109
+ "PluginManifest",
110
+ "PluginManifestNotFound",
111
+ "PluginManifestNotFoundError",
112
+ "PluginNotFoundError",
113
+ "PluginRegistryState",
114
+ "PluginState",
115
+ "RepositoryAnalyzerPlugin",
116
+ "Requirement",
117
+ "Sandbox",
118
+ "ScopedBuiltins",
119
+ "Spec",
120
+ "TestRunnerPlugin",
121
+ "UnsatisfiableRequirementError",
122
+ "Version",
123
+ "VersionParseError",
124
+ "default_plugins_dir",
125
+ "discover_entry_points",
126
+ "discover_filesystem",
127
+ "is_valid_plugin_name",
128
+ "load_filesystem",
129
+ "resolve",
130
+ "run_sandboxed",
131
+ "sandbox",
132
+ ]