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,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]
@@ -0,0 +1,5 @@
1
+ """Command-line interface for ForgeCLI."""
2
+
3
+ from forgecli.cli.main import app
4
+
5
+ __all__ = ["app"]
@@ -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
+ ]