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,31 @@
|
|
|
1
|
+
"""Source code formatting dispatcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from forgecli.core.service import Service
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Formatter(Service):
|
|
12
|
+
"""Dispatch code formatting to a language-specific backend.
|
|
13
|
+
|
|
14
|
+
The default implementation is a no-op placeholder; concrete
|
|
15
|
+
formatters (ruff, black, prettier, gofmt) will be plugged in here
|
|
16
|
+
without changing call sites.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name = "builder.formatter"
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, backend: str = "auto") -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._backend = backend
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def backend(self) -> str:
|
|
27
|
+
return self._backend
|
|
28
|
+
|
|
29
|
+
def format(self, paths: Iterable[Path]) -> list[Path]:
|
|
30
|
+
"""Format ``paths``; placeholder returns the input untouched."""
|
|
31
|
+
return [Path(p) for p in paths]
|
forgecli/cli/__init__.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Build the :class:`AppContext` for a CLI invocation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from forgecli.config.loader import ConfigLoader
|
|
10
|
+
from forgecli.core.context import AppContext
|
|
11
|
+
from forgecli.core.logging import configure_logging, get_logger
|
|
12
|
+
from forgecli.providers.anthropic import AnthropicProvider
|
|
13
|
+
from forgecli.providers.base import ProviderRegistry, default_registry
|
|
14
|
+
from forgecli.providers.google import GeminiProvider
|
|
15
|
+
from forgecli.providers.mock import MockProvider
|
|
16
|
+
from forgecli.providers.openai import OpenAIProvider
|
|
17
|
+
from forgecli.providers.router_state import load_state as load_router_state
|
|
18
|
+
from forgecli.utils.paths import ProjectPaths
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
21
|
+
from forgecli.core.container import Container
|
|
22
|
+
from forgecli.optimizer.summarizer import Summarizer
|
|
23
|
+
|
|
24
|
+
log = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def bootstrap_context(
|
|
28
|
+
*,
|
|
29
|
+
config_path: Path | None = None,
|
|
30
|
+
cwd: Path | str | None = None,
|
|
31
|
+
extras: dict[str, Any] | None = None,
|
|
32
|
+
) -> AppContext:
|
|
33
|
+
"""Build an :class:`AppContext` with default services registered."""
|
|
34
|
+
if isinstance(cwd, str):
|
|
35
|
+
cwd = Path(cwd)
|
|
36
|
+
import click
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
click_ctx = click.get_current_context(silent=True)
|
|
40
|
+
except Exception:
|
|
41
|
+
click_ctx = None
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
click_ctx is not None
|
|
45
|
+
and click_ctx.obj is not None
|
|
46
|
+
and isinstance(click_ctx.obj, AppContext)
|
|
47
|
+
):
|
|
48
|
+
return click_ctx.obj
|
|
49
|
+
|
|
50
|
+
paths = ProjectPaths.from_env(cwd=cwd).ensure()
|
|
51
|
+
|
|
52
|
+
verbose = (extras or {}).get("verbose", False)
|
|
53
|
+
level = "DEBUG" if verbose else "INFO"
|
|
54
|
+
configure_logging(level=level)
|
|
55
|
+
|
|
56
|
+
loader = ConfigLoader(config_path) if config_path else ConfigLoader()
|
|
57
|
+
|
|
58
|
+
from forgecli.providers.openai_compatible import (
|
|
59
|
+
CohereProvider,
|
|
60
|
+
FireworksProvider,
|
|
61
|
+
GroqProvider,
|
|
62
|
+
LMStudioProvider,
|
|
63
|
+
MiniMaxProvider,
|
|
64
|
+
MistralProvider,
|
|
65
|
+
NvidiaProvider,
|
|
66
|
+
OllamaProvider,
|
|
67
|
+
OpenRouterProvider,
|
|
68
|
+
TogetherProvider,
|
|
69
|
+
VllmProvider,
|
|
70
|
+
XaiProvider,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
provider_registry: ProviderRegistry = default_registry
|
|
74
|
+
for name, cls in (
|
|
75
|
+
("mock", MockProvider),
|
|
76
|
+
("openai", OpenAIProvider),
|
|
77
|
+
("anthropic", AnthropicProvider),
|
|
78
|
+
("google", GeminiProvider),
|
|
79
|
+
("openrouter", OpenRouterProvider),
|
|
80
|
+
("groq", GroqProvider),
|
|
81
|
+
("mistral", MistralProvider),
|
|
82
|
+
("minimax", MiniMaxProvider),
|
|
83
|
+
("xai", XaiProvider),
|
|
84
|
+
("together", TogetherProvider),
|
|
85
|
+
("fireworks", FireworksProvider),
|
|
86
|
+
("cohere", CohereProvider),
|
|
87
|
+
("nvidia", NvidiaProvider),
|
|
88
|
+
("ollama", OllamaProvider),
|
|
89
|
+
("lmstudio", LMStudioProvider),
|
|
90
|
+
("vllm", VllmProvider),
|
|
91
|
+
):
|
|
92
|
+
provider_registry.register(name, cls)
|
|
93
|
+
|
|
94
|
+
container = _build_container(provider_registry, paths)
|
|
95
|
+
context = AppContext(paths=paths, loader=loader, container=container)
|
|
96
|
+
merged = dict(extras or {})
|
|
97
|
+
merged.setdefault(
|
|
98
|
+
"optimizer.state",
|
|
99
|
+
_load_optimizer_state(paths),
|
|
100
|
+
)
|
|
101
|
+
merged.setdefault(
|
|
102
|
+
"router.state",
|
|
103
|
+
load_router_state(paths.data_dir / "router.json"),
|
|
104
|
+
)
|
|
105
|
+
context.extras.update(merged)
|
|
106
|
+
if click_ctx is not None:
|
|
107
|
+
click_ctx.obj = context
|
|
108
|
+
return context
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _load_optimizer_state(paths: ProjectPaths):
|
|
112
|
+
"""Read the persisted optimizer state from ``data_dir/optimizer.json``."""
|
|
113
|
+
from forgecli.optimizer.ponytail import Intensity
|
|
114
|
+
from forgecli.optimizer.ponytail.state import OptimizerState
|
|
115
|
+
|
|
116
|
+
state_path = paths.data_dir / "optimizer.json"
|
|
117
|
+
if not state_path.exists():
|
|
118
|
+
return OptimizerState()
|
|
119
|
+
try:
|
|
120
|
+
payload = json.loads(state_path.read_text(encoding="utf-8"))
|
|
121
|
+
except (OSError, json.JSONDecodeError):
|
|
122
|
+
return OptimizerState()
|
|
123
|
+
|
|
124
|
+
state = OptimizerState()
|
|
125
|
+
intensity = payload.get("intensity")
|
|
126
|
+
if isinstance(intensity, str):
|
|
127
|
+
try:
|
|
128
|
+
state.intensity = Intensity.parse(intensity)
|
|
129
|
+
except ValueError:
|
|
130
|
+
state.intensity = Intensity.LITE
|
|
131
|
+
backend = payload.get("backend")
|
|
132
|
+
if isinstance(backend, str):
|
|
133
|
+
state.backend = backend
|
|
134
|
+
binary = payload.get("binary")
|
|
135
|
+
if isinstance(binary, str):
|
|
136
|
+
state.binary = binary
|
|
137
|
+
return state
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _build_container(
|
|
141
|
+
provider_registry: ProviderRegistry,
|
|
142
|
+
paths: ProjectPaths,
|
|
143
|
+
):
|
|
144
|
+
"""Register default services in the DI container."""
|
|
145
|
+
from forgecli.core.container import Container
|
|
146
|
+
from forgecli.graph.backend_graphify import GraphifyRepositoryGraph
|
|
147
|
+
from forgecli.graph.graph import CodeGraph
|
|
148
|
+
from forgecli.graph.indexer import Indexer
|
|
149
|
+
from forgecli.graph.repository import RepositoryGraph
|
|
150
|
+
from forgecli.memory.store import MemoryStore
|
|
151
|
+
from forgecli.optimizer.chunker import Chunker
|
|
152
|
+
from forgecli.optimizer.optimizer import ContextOptimizer
|
|
153
|
+
from forgecli.optimizer.ponytail import PromptOptimizer
|
|
154
|
+
from forgecli.optimizer.ponytail.factory import build_optimizer
|
|
155
|
+
from forgecli.optimizer.ranker import Ranker
|
|
156
|
+
from forgecli.optimizer.summarizer import Summarizer
|
|
157
|
+
from forgecli.prompts.loader import PromptLoader
|
|
158
|
+
from forgecli.prompts.registry import PromptRegistry
|
|
159
|
+
from forgecli.prompts.renderer import PromptRenderer
|
|
160
|
+
from forgecli.providers.router import ModelRouter
|
|
161
|
+
from forgecli.templates.engine import TemplateEngine
|
|
162
|
+
from forgecli.templates.registry import TemplateRegistry
|
|
163
|
+
|
|
164
|
+
container = Container()
|
|
165
|
+
|
|
166
|
+
container.register_instance(ProviderRegistry, provider_registry)
|
|
167
|
+
container.register_instance(ProjectPaths, paths)
|
|
168
|
+
container.register_instance(CodeGraph, CodeGraph())
|
|
169
|
+
|
|
170
|
+
container.register(MemoryStore, lambda c: MemoryStore(paths.data_dir / "history.db"))
|
|
171
|
+
container.register(Summarizer, lambda c: _build_default_summarizer(c))
|
|
172
|
+
container.register(
|
|
173
|
+
ContextOptimizer,
|
|
174
|
+
lambda c: ContextOptimizer(
|
|
175
|
+
chunker=Chunker(),
|
|
176
|
+
ranker=Ranker(),
|
|
177
|
+
summarizer=c.resolve(Summarizer) if c.has(Summarizer) else None,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
container.register(
|
|
181
|
+
Indexer,
|
|
182
|
+
lambda c: Indexer(
|
|
183
|
+
graph=c.resolve(CodeGraph),
|
|
184
|
+
root=paths.cwd,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
container.register(
|
|
188
|
+
RepositoryGraph, # type: ignore[type-abstract]
|
|
189
|
+
lambda _c: GraphifyRepositoryGraph(root=paths.cwd),
|
|
190
|
+
)
|
|
191
|
+
container.register(
|
|
192
|
+
PromptOptimizer, # type: ignore[type-abstract]
|
|
193
|
+
lambda _c: build_optimizer(_load_optimizer_state(paths), ConfigLoader().load()),
|
|
194
|
+
)
|
|
195
|
+
container.register(
|
|
196
|
+
ModelRouter,
|
|
197
|
+
lambda c: ModelRouter(registry=c.resolve(ProviderRegistry)),
|
|
198
|
+
)
|
|
199
|
+
container.register(PromptRenderer, lambda _c: PromptRenderer())
|
|
200
|
+
container.register(PromptLoader, lambda _c: PromptLoader(paths.prompts_dir))
|
|
201
|
+
container.register(PromptRegistry, lambda _c: PromptRegistry())
|
|
202
|
+
container.register(TemplateEngine, lambda c: TemplateEngine(renderer=c.resolve(PromptRenderer)))
|
|
203
|
+
container.register(TemplateRegistry, lambda _c: TemplateRegistry())
|
|
204
|
+
|
|
205
|
+
return container
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _build_default_summarizer(container: Container) -> Summarizer:
|
|
209
|
+
"""Wire the default summarizer against the mock provider.
|
|
210
|
+
|
|
211
|
+
Real provider wiring will live in a separate composition function and
|
|
212
|
+
will be selected via configuration.
|
|
213
|
+
"""
|
|
214
|
+
from forgecli.optimizer.summarizer import Summarizer
|
|
215
|
+
from forgecli.providers.mock import MockProvider, MockProviderConfig
|
|
216
|
+
|
|
217
|
+
provider = MockProvider(MockProviderConfig())
|
|
218
|
+
return Summarizer(provider=provider)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def resolve_provider_and_decision(
|
|
222
|
+
*,
|
|
223
|
+
live: bool,
|
|
224
|
+
cwd: Path | str,
|
|
225
|
+
) -> tuple[Any, Any]:
|
|
226
|
+
"""Unified provider and route decision resolver for all AI commands.
|
|
227
|
+
|
|
228
|
+
If live is True and no provider has credentials (which resolves to mock),
|
|
229
|
+
it displays the error requesting configuration and exits with code 1.
|
|
230
|
+
"""
|
|
231
|
+
import typer
|
|
232
|
+
|
|
233
|
+
from forgecli.providers.base import ProviderRegistry
|
|
234
|
+
from forgecli.providers.mock import MockProvider, MockProviderConfig
|
|
235
|
+
from forgecli.providers.router import (
|
|
236
|
+
ModelRouter,
|
|
237
|
+
RouteDecision,
|
|
238
|
+
SelectionMode,
|
|
239
|
+
_provider_has_credentials,
|
|
240
|
+
)
|
|
241
|
+
from forgecli.providers.router_state import load_state as load_router_state
|
|
242
|
+
|
|
243
|
+
if isinstance(cwd, str):
|
|
244
|
+
cwd = Path(cwd)
|
|
245
|
+
|
|
246
|
+
app_context = bootstrap_context(cwd=cwd)
|
|
247
|
+
state = load_router_state(app_context.paths.data_dir / "router.json")
|
|
248
|
+
router = app_context.container.resolve(ModelRouter)
|
|
249
|
+
decision = router.select(state.choice)
|
|
250
|
+
|
|
251
|
+
if not live:
|
|
252
|
+
provider: Any = MockProvider(MockProviderConfig())
|
|
253
|
+
decision = RouteDecision(
|
|
254
|
+
provider_name="mock",
|
|
255
|
+
model=decision.model,
|
|
256
|
+
mode=SelectionMode.FALLBACK,
|
|
257
|
+
)
|
|
258
|
+
return provider, decision
|
|
259
|
+
|
|
260
|
+
if decision.provider_name == "mock" or not _provider_has_credentials(decision.provider_name):
|
|
261
|
+
from forgecli.cli.ui import error
|
|
262
|
+
|
|
263
|
+
error(
|
|
264
|
+
"No AI provider configured.\n\n"
|
|
265
|
+
"Run:\n"
|
|
266
|
+
" forge provider use <provider>\n"
|
|
267
|
+
" forge model use <model>\n\n"
|
|
268
|
+
"Then retry."
|
|
269
|
+
)
|
|
270
|
+
raise typer.Exit(code=1)
|
|
271
|
+
|
|
272
|
+
registry = app_context.container.resolve(ProviderRegistry)
|
|
273
|
+
if not registry.has(decision.provider_name):
|
|
274
|
+
from forgecli.cli.ui import error
|
|
275
|
+
|
|
276
|
+
error(f"Unknown provider '{decision.provider_name}'.")
|
|
277
|
+
raise typer.Exit(code=1)
|
|
278
|
+
|
|
279
|
+
provider_cls = registry.get(decision.provider_name)
|
|
280
|
+
provider = provider_cls() # type: ignore[call-arg]
|
|
281
|
+
return provider, decision
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""``forge graph`` subcommand group: build.
|
|
2
|
+
|
|
3
|
+
These commands integrate the external Graphify CLI behind the
|
|
4
|
+
:mod:`forgecli.graph.repository` abstraction. When Graphify is not
|
|
5
|
+
installed the commands print an installation hint instead of failing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from forgecli.cli.ui import (
|
|
16
|
+
error,
|
|
17
|
+
get_console,
|
|
18
|
+
info,
|
|
19
|
+
success,
|
|
20
|
+
)
|
|
21
|
+
from forgecli.graph.backend_graphify import GraphifyRepositoryGraph
|
|
22
|
+
from forgecli.utils.fs import has_supported_source_files
|
|
23
|
+
from forgecli.utils.paths import to_privacy_path
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
help="Build the codebase graph.",
|
|
27
|
+
no_args_is_help=True,
|
|
28
|
+
rich_markup_mode="rich",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_backend(path: Path) -> GraphifyRepositoryGraph:
|
|
33
|
+
return GraphifyRepositoryGraph(root=path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _require_graphify(backend: GraphifyRepositoryGraph) -> None:
|
|
37
|
+
if not await backend.is_available():
|
|
38
|
+
raise typer.Exit(code=1) from None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def setup_graphify_credentials(path: Path) -> str | None:
|
|
42
|
+
"""Read the active provider, load its API key, set env vars, and return provider name if configured."""
|
|
43
|
+
from forgecli.cli.bootstrap import bootstrap_context
|
|
44
|
+
from forgecli.core.credentials import get_api_key
|
|
45
|
+
from forgecli.providers.router import _PROVIDER_ENV_VARS, ModelRouter
|
|
46
|
+
from forgecli.providers.router_state import load_state as load_router_state
|
|
47
|
+
|
|
48
|
+
app_context = bootstrap_context(cwd=path)
|
|
49
|
+
state = load_router_state(app_context.paths.data_dir / "router.json")
|
|
50
|
+
router = app_context.container.resolve(ModelRouter)
|
|
51
|
+
decision = router.select(state.choice)
|
|
52
|
+
|
|
53
|
+
provider_name = decision.provider_name
|
|
54
|
+
if provider_name == "mock":
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Try env vars first
|
|
58
|
+
env_vars = _PROVIDER_ENV_VARS.get(provider_name, ())
|
|
59
|
+
for ev in env_vars:
|
|
60
|
+
if os.environ.get(ev):
|
|
61
|
+
return provider_name
|
|
62
|
+
|
|
63
|
+
# Try keychain/credential manager
|
|
64
|
+
api_key = get_api_key(provider_name)
|
|
65
|
+
if api_key:
|
|
66
|
+
for ev in env_vars:
|
|
67
|
+
os.environ[ev] = api_key
|
|
68
|
+
return provider_name
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("build")
|
|
74
|
+
def build_cmd(
|
|
75
|
+
path: str = typer.Option(".", "--path", "-p", help="Project root to index."),
|
|
76
|
+
force: bool = typer.Option(
|
|
77
|
+
False,
|
|
78
|
+
"--force",
|
|
79
|
+
help="Overwrite graph.json even if the rebuild has fewer nodes.",
|
|
80
|
+
),
|
|
81
|
+
no_cluster: bool = typer.Option(False, "--no-cluster", help="Skip Leiden clustering."),
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Build (or rebuild) the codebase graph for ``path``."""
|
|
84
|
+
import asyncio
|
|
85
|
+
|
|
86
|
+
path_obj = Path(path).resolve()
|
|
87
|
+
|
|
88
|
+
# 1. Exit cleanly if no supported source files found
|
|
89
|
+
if not has_supported_source_files(path_obj):
|
|
90
|
+
get_console().print("No supported source files found. Nothing to build.")
|
|
91
|
+
raise typer.Exit(code=0)
|
|
92
|
+
|
|
93
|
+
# 2. Check credentials first. Never continue without a valid provider configuration.
|
|
94
|
+
active_provider = setup_graphify_credentials(path_obj)
|
|
95
|
+
if not active_provider:
|
|
96
|
+
get_console().print(
|
|
97
|
+
"❌ API key required.\n\n"
|
|
98
|
+
"Forge Graph requires an AI provider API key before a knowledge graph can be built."
|
|
99
|
+
)
|
|
100
|
+
raise typer.Exit(code=1)
|
|
101
|
+
|
|
102
|
+
backend = _build_backend(path_obj)
|
|
103
|
+
|
|
104
|
+
async def _run() -> None:
|
|
105
|
+
if not await backend.is_available():
|
|
106
|
+
get_console().print(await backend.install_hint())
|
|
107
|
+
raise typer.Exit(code=1)
|
|
108
|
+
|
|
109
|
+
info(f"Building graph for [accent]{to_privacy_path(backend.root)}[/accent] ...")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
import json
|
|
113
|
+
import time
|
|
114
|
+
|
|
115
|
+
start_time = time.perf_counter()
|
|
116
|
+
result = await backend.build(force=force, no_cluster=no_cluster)
|
|
117
|
+
build_duration = time.perf_counter() - start_time
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
build_time_file = backend.root / "graphify-out" / "build_time.json"
|
|
121
|
+
build_time_file.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
with open(build_time_file, "w", encoding="utf-8") as f:
|
|
123
|
+
json.dump({"build_time": build_duration}, f)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
snapshot = result.snapshot
|
|
128
|
+
get_console().print(
|
|
129
|
+
f" nodes: [bold]{len(snapshot.nodes)}[/bold]\n"
|
|
130
|
+
f" edges: [bold]{len(snapshot.edges)}[/bold]\n"
|
|
131
|
+
f" communities:[bold]{len(snapshot.communities)}[/bold]"
|
|
132
|
+
)
|
|
133
|
+
for label, value in result.artifacts.items():
|
|
134
|
+
get_console().print(f" [muted]{label}:[/muted] {to_privacy_path(value)}")
|
|
135
|
+
success("Graph built.")
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
error(f"Graph build failed: {exc}")
|
|
138
|
+
raise typer.Exit(code=1) from exc
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
asyncio.run(_run())
|
|
142
|
+
except typer.Exit:
|
|
143
|
+
raise
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
error(f"Graph build failed: {exc}")
|
|
146
|
+
raise typer.Exit(code=1) from exc
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Wrapper commands: forge claude | codex | cursor | opencode | commandcode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from forgecli.runtime.wrappers import launch_wrapper
|
|
10
|
+
|
|
11
|
+
_WRAPPER_SETTINGS = {
|
|
12
|
+
"allow_extra_args": True,
|
|
13
|
+
"allow_interspersed_args": True,
|
|
14
|
+
"ignore_unknown_options": True,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def claude_cmd(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
path: str = typer.Option(".", "--path", "-p", help="Project root."),
|
|
21
|
+
refresh: bool = typer.Option(False, "--refresh", help="Bypass cached Forge context."),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Launch Claude Code with Forge prompt + token optimization."""
|
|
24
|
+
launch_wrapper("claude", list(ctx.args), path=Path(path), force_prepare=refresh)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def codex_cmd(
|
|
28
|
+
ctx: typer.Context,
|
|
29
|
+
path: str = typer.Option(".", "--path", "-p", help="Project root."),
|
|
30
|
+
refresh: bool = typer.Option(False, "--refresh", help="Bypass cached Forge context."),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Launch Codex CLI with Forge prompt + token optimization."""
|
|
33
|
+
launch_wrapper("codex", list(ctx.args), path=Path(path), force_prepare=refresh)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cursor_cmd(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
path: str = typer.Option(".", "--path", "-p", help="Project root."),
|
|
39
|
+
refresh: bool = typer.Option(False, "--refresh", help="Bypass cached Forge context."),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Launch Cursor CLI with Forge prompt + token optimization."""
|
|
42
|
+
launch_wrapper("cursor", list(ctx.args), path=Path(path), force_prepare=refresh)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def opencode_cmd(
|
|
46
|
+
ctx: typer.Context,
|
|
47
|
+
path: str = typer.Option(".", "--path", "-p", help="Project root."),
|
|
48
|
+
refresh: bool = typer.Option(False, "--refresh", help="Bypass cached Forge context."),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Launch OpenCode CLI with Forge prompt + token optimization."""
|
|
51
|
+
launch_wrapper("opencode", list(ctx.args), path=Path(path), force_prepare=refresh)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def commandcode_cmd(
|
|
55
|
+
ctx: typer.Context,
|
|
56
|
+
path: str = typer.Option(".", "--path", "-p", help="Project root."),
|
|
57
|
+
refresh: bool = typer.Option(False, "--refresh", help="Bypass cached Forge context."),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Launch CommandCode CLI with Forge prompt + token optimization."""
|
|
60
|
+
launch_wrapper("commandcode", list(ctx.args), path=Path(path), force_prepare=refresh)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def antigravity_cmd(
|
|
64
|
+
ctx: typer.Context,
|
|
65
|
+
path: str = typer.Option(".", "--path", "-p", help="Project root."),
|
|
66
|
+
refresh: bool = typer.Option(False, "--refresh", help="Bypass cached Forge context."),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Launch Antigravity CLI with Forge prompt + token optimization."""
|
|
69
|
+
launch_wrapper("antigravity", list(ctx.args), path=Path(path), force_prepare=refresh)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"_WRAPPER_SETTINGS",
|
|
74
|
+
"antigravity_cmd",
|
|
75
|
+
"claude_cmd",
|
|
76
|
+
"codex_cmd",
|
|
77
|
+
"commandcode_cmd",
|
|
78
|
+
"cursor_cmd",
|
|
79
|
+
"opencode_cmd",
|
|
80
|
+
]
|