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
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
+ ]