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
forgecli/sdk/loader.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Plugin discovery and on-disk loading.
|
|
2
|
+
|
|
3
|
+
The SDK supports two complementary sources:
|
|
4
|
+
|
|
5
|
+
* **Filesystem-based** plugins. A directory under
|
|
6
|
+
``$config_dir/plugins/<name>`` containing
|
|
7
|
+
``forgecli-plugin.toml`` and a Python package. The loader reads
|
|
8
|
+
the manifest, then imports the entry-point callables on enable.
|
|
9
|
+
* **Entry-point-based** plugins. A regular Python package with a
|
|
10
|
+
``[project.entry-points."forgecli.plugins"]`` table. The loader
|
|
11
|
+
walks the active distribution set via :mod:`importlib.metadata`.
|
|
12
|
+
|
|
13
|
+
Both sources return a :class:`LoadedPlugin` (manifest + the
|
|
14
|
+
resolved entry-point callables + source path). The
|
|
15
|
+
:class:`PluginManager` consumes these.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
import importlib.metadata as importlib_metadata
|
|
22
|
+
import importlib.util
|
|
23
|
+
import sys
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from types import ModuleType
|
|
28
|
+
|
|
29
|
+
from forgecli.platform.paths import config_dir
|
|
30
|
+
from forgecli.sdk.manifest import PluginManifest
|
|
31
|
+
from forgecli.sdk.version import Version
|
|
32
|
+
|
|
33
|
+
_MANIFEST_FILENAME = "forgecli-plugin.toml"
|
|
34
|
+
_PLUGINS_DIRNAME = "plugins"
|
|
35
|
+
_ENTRY_POINT_GROUP = "forgecli.plugins"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class LoadedPlugin:
|
|
40
|
+
"""The result of loading a single plugin from disk or PyPI."""
|
|
41
|
+
|
|
42
|
+
manifest: PluginManifest
|
|
43
|
+
entry_point_factories: dict
|
|
44
|
+
"""Map of (kind, name) -> configure(manager) callable.
|
|
45
|
+
|
|
46
|
+
Each callable is invoked when the plugin is enabled; the
|
|
47
|
+
callable uses the manager to register workflows, providers,
|
|
48
|
+
analyzers, etc.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
source: Path | None = None # None for entry-point-only plugins
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def name(self) -> str:
|
|
55
|
+
return self.manifest.name
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def version(self) -> Version:
|
|
59
|
+
return self.manifest.version
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PluginManifestNotFoundError(LookupError):
|
|
63
|
+
"""Raised when a plugin's manifest is missing or malformed."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Backward-compatible alias for the legacy name.
|
|
67
|
+
PluginManifestNotFound = PluginManifestNotFoundError
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def default_plugins_dir() -> Path:
|
|
71
|
+
"""Return the directory where on-disk plugins live."""
|
|
72
|
+
path = config_dir() / _PLUGINS_DIRNAME
|
|
73
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def discover_filesystem(root: Path | None = None) -> list[LoadedPlugin]:
|
|
78
|
+
"""Find every plugin under ``root`` (default: ``$config_dir/plugins``)."""
|
|
79
|
+
base = root or default_plugins_dir()
|
|
80
|
+
if not base.exists():
|
|
81
|
+
return []
|
|
82
|
+
plugins: list[LoadedPlugin] = []
|
|
83
|
+
for child in sorted(base.iterdir()):
|
|
84
|
+
if not child.is_dir():
|
|
85
|
+
continue
|
|
86
|
+
manifest_path = child / _MANIFEST_FILENAME
|
|
87
|
+
if not manifest_path.exists():
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
plugins.append(load_filesystem(child))
|
|
91
|
+
except PluginManifestNotFound:
|
|
92
|
+
continue
|
|
93
|
+
return plugins
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_filesystem(plugin_dir: Path) -> LoadedPlugin:
|
|
97
|
+
"""Load a single plugin from a directory on disk."""
|
|
98
|
+
manifest_path = plugin_dir / _MANIFEST_FILENAME
|
|
99
|
+
manifest = PluginManifest.load(manifest_path)
|
|
100
|
+
factories = _load_entry_point_factories(manifest, plugin_dir)
|
|
101
|
+
return LoadedPlugin(
|
|
102
|
+
manifest=manifest,
|
|
103
|
+
entry_point_factories=factories,
|
|
104
|
+
source=plugin_dir,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def discover_entry_points() -> list[LoadedPlugin]:
|
|
109
|
+
"""Find every plugin registered via the ``forgecli.plugins`` entry point group."""
|
|
110
|
+
plugins: list[LoadedPlugin] = []
|
|
111
|
+
try:
|
|
112
|
+
entries = importlib_metadata.entry_points(group=_ENTRY_POINT_GROUP)
|
|
113
|
+
except Exception:
|
|
114
|
+
return plugins
|
|
115
|
+
# Group by distribution to load one synthetic manifest per
|
|
116
|
+
# distribution. This is a pragmatic choice that lets users ship
|
|
117
|
+
# multiple plugins in a single distribution.
|
|
118
|
+
by_distribution: dict[str | None, list] = {}
|
|
119
|
+
for ep in entries:
|
|
120
|
+
dist = getattr(ep, "dist", None)
|
|
121
|
+
dist_name = dist.name if dist is not None else "unknown"
|
|
122
|
+
by_distribution.setdefault(dist_name, []).append(ep)
|
|
123
|
+
for dist_name, ep_list in by_distribution.items():
|
|
124
|
+
try:
|
|
125
|
+
plugins.append(_load_entry_point_distribution(dist_name, ep_list))
|
|
126
|
+
except PluginManifestNotFound:
|
|
127
|
+
continue
|
|
128
|
+
except Exception:
|
|
129
|
+
continue
|
|
130
|
+
return plugins
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _load_entry_point_distribution(dist_name: str | None, entry_points: list) -> LoadedPlugin:
|
|
134
|
+
"""Load a distribution's entry-points as a synthetic plugin."""
|
|
135
|
+
if dist_name is None:
|
|
136
|
+
raise PluginManifestNotFound("entry point has no distribution")
|
|
137
|
+
try:
|
|
138
|
+
dist = importlib_metadata.distribution(dist_name)
|
|
139
|
+
except importlib_metadata.PackageNotFoundError as exc:
|
|
140
|
+
raise PluginManifestNotFound(dist_name) from exc
|
|
141
|
+
# Use the distribution's metadata to build a synthetic manifest.
|
|
142
|
+
meta = dist.metadata
|
|
143
|
+
name = _coerce_name(meta.get("Name") or dist_name, dist_name)
|
|
144
|
+
version = _coerce_version(meta.get("Version"))
|
|
145
|
+
if version is None:
|
|
146
|
+
raise PluginManifestNotFound(f"{dist_name}: no version")
|
|
147
|
+
summary = meta.get("Summary", "").strip()
|
|
148
|
+
summary = summary.splitlines()[0] if summary else f"{name} plugin"
|
|
149
|
+
manifest = PluginManifest(
|
|
150
|
+
name=name,
|
|
151
|
+
version=version,
|
|
152
|
+
summary=summary,
|
|
153
|
+
description=meta.get("Description", "") or "",
|
|
154
|
+
authors=tuple(_parse_authors(meta.get("Author"))),
|
|
155
|
+
license=meta.get("License", "") or "",
|
|
156
|
+
homepage=meta.get("Home-page", "") or "",
|
|
157
|
+
)
|
|
158
|
+
factories: dict[tuple[str, str], Callable] = {}
|
|
159
|
+
for ep in entry_points:
|
|
160
|
+
try:
|
|
161
|
+
callback = ep.load()
|
|
162
|
+
except Exception:
|
|
163
|
+
continue
|
|
164
|
+
from forgecli.sdk.manifest import EntryPointKind as _Kind
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
kind = _Kind(ep.group.replace(f"{_ENTRY_POINT_GROUP}.", ""))
|
|
168
|
+
except ValueError:
|
|
169
|
+
continue
|
|
170
|
+
factories[(kind.value, ep.name)] = callback
|
|
171
|
+
return LoadedPlugin(
|
|
172
|
+
manifest=manifest,
|
|
173
|
+
entry_point_factories=factories,
|
|
174
|
+
source=None,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _load_entry_point_factories(manifest: PluginManifest, plugin_dir: Path) -> dict:
|
|
179
|
+
"""Import each ``module:attr`` reference declared in the manifest."""
|
|
180
|
+
factories: dict = {}
|
|
181
|
+
for ep in manifest.entry_points:
|
|
182
|
+
module_name, _, attr = ep.reference.partition(":")
|
|
183
|
+
if not module_name or not attr:
|
|
184
|
+
continue
|
|
185
|
+
try:
|
|
186
|
+
module = _import_module_from_dir(module_name, plugin_dir)
|
|
187
|
+
except Exception:
|
|
188
|
+
continue
|
|
189
|
+
callback = getattr(module, attr, None)
|
|
190
|
+
if not callable(callback):
|
|
191
|
+
continue
|
|
192
|
+
factories[(ep.kind.value, ep.name)] = callback
|
|
193
|
+
return factories
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _import_module_from_dir(name: str, plugin_dir: Path) -> ModuleType:
|
|
197
|
+
"""Import ``name`` from a directory under ``plugin_dir``.
|
|
198
|
+
|
|
199
|
+
We add the plugin dir to ``sys.path`` and then ``importlib.import_module``
|
|
200
|
+
so the import works for both regular packages and loose modules.
|
|
201
|
+
"""
|
|
202
|
+
if str(plugin_dir) not in sys.path:
|
|
203
|
+
sys.path.insert(0, str(plugin_dir))
|
|
204
|
+
return importlib.import_module(name)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _coerce_name(value: str | None, fallback: str) -> str:
|
|
208
|
+
if not value:
|
|
209
|
+
return fallback.lower().replace("_", "-").replace(" ", "-")
|
|
210
|
+
return value.lower().replace("_", "-")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _coerce_version(value: str | None) -> Version | None:
|
|
214
|
+
if not value:
|
|
215
|
+
return None
|
|
216
|
+
from forgecli.sdk.version import Version, VersionParseError
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
return Version.parse(value)
|
|
220
|
+
except VersionParseError:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _parse_authors(value: str | None) -> list[str]:
|
|
225
|
+
if not value:
|
|
226
|
+
return []
|
|
227
|
+
return [a.strip() for a in value.split(",") if a.strip()]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Silence unused-import warnings.
|
|
231
|
+
|
|
232
|
+
__all__ = [
|
|
233
|
+
"LoadedPlugin",
|
|
234
|
+
"PluginManifestNotFound",
|
|
235
|
+
"default_plugins_dir",
|
|
236
|
+
"discover_entry_points",
|
|
237
|
+
"discover_filesystem",
|
|
238
|
+
"load_filesystem",
|
|
239
|
+
]
|