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.
- forgecli/__init__.py +4 -0
- forgecli/build/__init__.py +135 -0
- forgecli/build/apply.py +218 -0
- forgecli/build/caveman_optimize.py +37 -0
- forgecli/build/diff_extract.py +218 -0
- forgecli/build/llm.py +167 -0
- forgecli/build/optimize.py +37 -0
- forgecli/build/pipeline.py +76 -0
- forgecli/build/retrieval.py +157 -0
- forgecli/build/summarize.py +76 -0
- forgecli/build/test_run.py +75 -0
- forgecli/builder/__init__.py +11 -0
- forgecli/builder/builder.py +53 -0
- forgecli/builder/editor.py +36 -0
- forgecli/builder/formatter.py +31 -0
- forgecli/cli/__init__.py +5 -0
- forgecli/cli/bootstrap.py +281 -0
- forgecli/cli/commands_graph.py +149 -0
- forgecli/cli/commands_wrappers.py +80 -0
- forgecli/cli/daemon.py +744 -0
- forgecli/cli/main.py +234 -0
- forgecli/cli/ui.py +56 -0
- forgecli/config/__init__.py +28 -0
- forgecli/config/loader.py +85 -0
- forgecli/config/settings.py +206 -0
- forgecli/config/writer.py +90 -0
- forgecli/core/__init__.py +35 -0
- forgecli/core/container.py +68 -0
- forgecli/core/context.py +54 -0
- forgecli/core/credentials.py +114 -0
- forgecli/core/errors.py +27 -0
- forgecli/core/events.py +67 -0
- forgecli/core/logging.py +47 -0
- forgecli/core/models.py +214 -0
- forgecli/core/plugins.py +54 -0
- forgecli/core/service.py +22 -0
- forgecli/docs/__init__.py +5 -0
- forgecli/docs/generator.py +115 -0
- forgecli/engine/__init__.py +105 -0
- forgecli/engine/context.py +163 -0
- forgecli/engine/defaults.py +89 -0
- forgecli/engine/events.py +185 -0
- forgecli/engine/execution.py +519 -0
- forgecli/engine/plugins.py +145 -0
- forgecli/engine/runner.py +158 -0
- forgecli/engine/stages/__init__.py +33 -0
- forgecli/engine/stages/caveman_optimizer.py +47 -0
- forgecli/engine/stages/context_optimizer.py +44 -0
- forgecli/engine/stages/execution_engine_stage.py +102 -0
- forgecli/engine/stages/git_engine.py +30 -0
- forgecli/engine/stages/intent_analyzer.py +38 -0
- forgecli/engine/stages/model_router.py +65 -0
- forgecli/engine/stages/planning_engine.py +45 -0
- forgecli/engine/stages/repository_analyzer.py +54 -0
- forgecli/engine/stages/validation_engine.py +87 -0
- forgecli/git/__init__.py +9 -0
- forgecli/git/repo.py +55 -0
- forgecli/git/service.py +45 -0
- forgecli/graph/__init__.py +56 -0
- forgecli/graph/backend_graphify.py +448 -0
- forgecli/graph/edge.py +19 -0
- forgecli/graph/graph.py +85 -0
- forgecli/graph/graphify.py +405 -0
- forgecli/graph/indexer.py +82 -0
- forgecli/graph/node.py +47 -0
- forgecli/graph/repository.py +164 -0
- forgecli/memory/__init__.py +12 -0
- forgecli/memory/cache.py +61 -0
- forgecli/memory/history.py +136 -0
- forgecli/memory/store.py +85 -0
- forgecli/optimizer/__init__.py +13 -0
- forgecli/optimizer/caveman/__init__.py +169 -0
- forgecli/optimizer/caveman/cli.py +62 -0
- forgecli/optimizer/caveman/decorator.py +67 -0
- forgecli/optimizer/caveman/factory.py +51 -0
- forgecli/optimizer/caveman/ruleset.py +156 -0
- forgecli/optimizer/caveman/state.py +50 -0
- forgecli/optimizer/chunker.py +85 -0
- forgecli/optimizer/optimizer.py +81 -0
- forgecli/optimizer/ponytail/__init__.py +181 -0
- forgecli/optimizer/ponytail/cli.py +159 -0
- forgecli/optimizer/ponytail/decorator.py +70 -0
- forgecli/optimizer/ponytail/factory.py +51 -0
- forgecli/optimizer/ponytail/ruleset.py +168 -0
- forgecli/optimizer/ponytail/state.py +51 -0
- forgecli/optimizer/ranker.py +37 -0
- forgecli/optimizer/summarizer.py +44 -0
- forgecli/orchestrator/__init__.py +706 -0
- forgecli/planner/__init__.py +56 -0
- forgecli/planner/agent.py +61 -0
- forgecli/planner/plan.py +65 -0
- forgecli/planner/planner.py +17 -0
- forgecli/planner/render.py +267 -0
- forgecli/planner/serialize.py +104 -0
- forgecli/planner/software.py +832 -0
- forgecli/platform/__init__.py +91 -0
- forgecli/platform/core.py +201 -0
- forgecli/platform/deps.py +361 -0
- forgecli/platform/paths.py +234 -0
- forgecli/platform/shell.py +176 -0
- forgecli/platform/update.py +253 -0
- forgecli/plugins/__init__.py +249 -0
- forgecli/prompts/__init__.py +11 -0
- forgecli/prompts/loader.py +28 -0
- forgecli/prompts/registry.py +32 -0
- forgecli/prompts/renderer.py +23 -0
- forgecli/providers/__init__.py +39 -0
- forgecli/providers/anthropic.py +207 -0
- forgecli/providers/base.py +204 -0
- forgecli/providers/builtin.py +26 -0
- forgecli/providers/conversation.py +74 -0
- forgecli/providers/google.py +290 -0
- forgecli/providers/http_base.py +207 -0
- forgecli/providers/mock.py +111 -0
- forgecli/providers/openai.py +206 -0
- forgecli/providers/openai_compatible.py +787 -0
- forgecli/providers/router.py +340 -0
- forgecli/providers/router_state.py +89 -0
- forgecli/review/__init__.py +45 -0
- forgecli/review/analyzer.py +124 -0
- forgecli/review/analyzers/__init__.py +1 -0
- forgecli/review/analyzers/architecture.py +255 -0
- forgecli/review/analyzers/complexity.py +162 -0
- forgecli/review/analyzers/dead_code.py +255 -0
- forgecli/review/analyzers/duplicates.py +161 -0
- forgecli/review/analyzers/performance.py +161 -0
- forgecli/review/analyzers/security.py +244 -0
- forgecli/review/finding.py +68 -0
- forgecli/review/report.py +321 -0
- forgecli/review/repository.py +130 -0
- forgecli/review/suggestions.py +98 -0
- forgecli/runtime/__init__.py +6 -0
- forgecli/runtime/cache_store.py +75 -0
- forgecli/runtime/mcp_config.py +118 -0
- forgecli/runtime/prepare.py +203 -0
- forgecli/runtime/wrappers.py +150 -0
- forgecli/sdk/__init__.py +132 -0
- forgecli/sdk/events.py +206 -0
- forgecli/sdk/interfaces.py +282 -0
- forgecli/sdk/loader.py +239 -0
- forgecli/sdk/manager.py +678 -0
- forgecli/sdk/manifest.py +395 -0
- forgecli/sdk/sandbox.py +247 -0
- forgecli/sdk/version.py +313 -0
- forgecli/templates/__init__.py +9 -0
- forgecli/templates/engine.py +37 -0
- forgecli/templates/registry.py +32 -0
- forgecli/utils/__init__.py +19 -0
- forgecli/utils/fs.py +121 -0
- forgecli/utils/ids.py +11 -0
- forgecli/utils/io.py +27 -0
- forgecli/utils/paths.py +78 -0
- forgecli/utils/stats.py +179 -0
- forgecli/utils/timing.py +25 -0
- forgeoptimizer-0.1.0.dist-info/METADATA +177 -0
- forgeoptimizer-0.1.0.dist-info/RECORD +159 -0
- forgeoptimizer-0.1.0.dist-info/WHEEL +4 -0
- forgeoptimizer-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|
forgecli/sdk/__init__.py
ADDED
|
@@ -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
|
+
]
|