forgeoptimizer 1.0.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 +137 -0
- forgecli/cli/commands_wrappers.py +63 -0
- forgecli/cli/main.py +122 -0
- forgecli/cli/ui.py +56 -0
- forgecli/config/__init__.py +28 -0
- forgecli/config/loader.py +85 -0
- forgecli/config/settings.py +204 -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 +159 -0
- forgecli/core/plugins.py +56 -0
- forgecli/core/service.py +22 -0
- forgecli/docs/__init__.py +5 -0
- forgecli/docs/generator.py +119 -0
- forgecli/engine/__init__.py +105 -0
- forgecli/engine/context.py +171 -0
- forgecli/engine/defaults.py +89 -0
- forgecli/engine/events.py +185 -0
- forgecli/engine/execution.py +526 -0
- forgecli/engine/plugins.py +146 -0
- forgecli/engine/runner.py +155 -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 +108 -0
- forgecli/engine/stages/git_engine.py +30 -0
- forgecli/engine/stages/intent_analyzer.py +38 -0
- forgecli/engine/stages/model_router.py +66 -0
- forgecli/engine/stages/planning_engine.py +45 -0
- forgecli/engine/stages/repository_analyzer.py +54 -0
- forgecli/engine/stages/validation_engine.py +89 -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 +453 -0
- forgecli/graph/edge.py +19 -0
- forgecli/graph/graph.py +85 -0
- forgecli/graph/graphify.py +412 -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 +138 -0
- forgecli/memory/store.py +87 -0
- forgecli/optimizer/__init__.py +14 -0
- forgecli/optimizer/caveman/__init__.py +173 -0
- forgecli/optimizer/caveman/cli.py +64 -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 +52 -0
- forgecli/optimizer/chunker.py +85 -0
- forgecli/optimizer/optimizer.py +81 -0
- forgecli/optimizer/ponytail/__init__.py +183 -0
- forgecli/optimizer/ponytail/cli.py +168 -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 +53 -0
- forgecli/optimizer/ranker.py +37 -0
- forgecli/optimizer/summarizer.py +44 -0
- forgecli/orchestrator/__init__.py +707 -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 +271 -0
- forgecli/planner/serialize.py +106 -0
- forgecli/planner/software.py +834 -0
- forgecli/platform/__init__.py +91 -0
- forgecli/platform/core.py +201 -0
- forgecli/platform/deps.py +354 -0
- forgecli/platform/paths.py +236 -0
- forgecli/platform/shell.py +176 -0
- forgecli/platform/update.py +252 -0
- forgecli/plugins/__init__.py +251 -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 +206 -0
- forgecli/providers/base.py +206 -0
- forgecli/providers/builtin.py +26 -0
- forgecli/providers/conversation.py +78 -0
- forgecli/providers/google.py +295 -0
- forgecli/providers/http_base.py +211 -0
- forgecli/providers/mock.py +113 -0
- forgecli/providers/openai.py +202 -0
- forgecli/providers/openai_compatible.py +728 -0
- forgecli/providers/router.py +345 -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 +258 -0
- forgecli/review/analyzers/complexity.py +167 -0
- forgecli/review/analyzers/dead_code.py +262 -0
- forgecli/review/analyzers/duplicates.py +163 -0
- forgecli/review/analyzers/performance.py +165 -0
- forgecli/review/analyzers/security.py +251 -0
- forgecli/review/finding.py +68 -0
- forgecli/review/report.py +311 -0
- forgecli/review/repository.py +131 -0
- forgecli/review/suggestions.py +98 -0
- forgecli/runtime/__init__.py +6 -0
- forgecli/runtime/cache_store.py +77 -0
- forgecli/runtime/prepare.py +199 -0
- forgecli/runtime/wrappers.py +152 -0
- forgecli/sdk/__init__.py +132 -0
- forgecli/sdk/events.py +206 -0
- forgecli/sdk/interfaces.py +282 -0
- forgecli/sdk/loader.py +243 -0
- forgecli/sdk/manager.py +693 -0
- forgecli/sdk/manifest.py +397 -0
- forgecli/sdk/sandbox.py +197 -0
- forgecli/sdk/version.py +316 -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 +91 -0
- forgecli/utils/ids.py +11 -0
- forgecli/utils/io.py +27 -0
- forgecli/utils/paths.py +70 -0
- forgecli/utils/timing.py +25 -0
- forgeoptimizer-1.0.0.dist-info/METADATA +169 -0
- forgeoptimizer-1.0.0.dist-info/RECORD +156 -0
- forgeoptimizer-1.0.0.dist-info/WHEEL +4 -0
- forgeoptimizer-1.0.0.dist-info/entry_points.txt +2 -0
- forgeoptimizer-1.0.0.dist-info/licenses/LICENSE +21 -0
forgecli/core/errors.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Typed exception hierarchy for ForgeCLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ForgeCLIError(Exception):
|
|
7
|
+
"""Base class for all ForgeCLI-specific errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigError(ForgeCLIError):
|
|
11
|
+
"""Raised when configuration cannot be loaded or validated."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProviderError(ForgeCLIError):
|
|
15
|
+
"""Raised on AI provider failures (network, auth, schema)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitError(ForgeCLIError):
|
|
19
|
+
"""Raised on Git-related failures (missing repo, bad ref, etc)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PluginError(ForgeCLIError):
|
|
23
|
+
"""Raised on plugin discovery or lifecycle failures."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PipelineError(ForgeCLIError):
|
|
27
|
+
"""Raised when a builder/review/planner pipeline cannot complete."""
|
forgecli/core/events.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""A tiny pub/sub event bus used for decoupled logging and lifecycle hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
Subscriber = Callable[["Event"], "None | Awaitable[None]"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Event:
|
|
17
|
+
"""An immutable event payload."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EventBus:
|
|
25
|
+
"""Asynchronous-first pub/sub bus.
|
|
26
|
+
|
|
27
|
+
Synchronous subscribers are invoked inline; async subscribers are
|
|
28
|
+
scheduled via :func:`asyncio.create_task`. The bus is intentionally
|
|
29
|
+
simple: subscribers cannot block the publisher and ordering is
|
|
30
|
+
best-effort.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._subscribers: dict[str, list[Subscriber]] = {}
|
|
35
|
+
self._pending_tasks: set[asyncio.Task[Any]] = set()
|
|
36
|
+
|
|
37
|
+
def subscribe(self, name: str, callback: Subscriber) -> None:
|
|
38
|
+
"""Register ``callback`` for events named ``name``."""
|
|
39
|
+
self._subscribers.setdefault(name, []).append(callback)
|
|
40
|
+
|
|
41
|
+
def unsubscribe(self, name: str, callback: Subscriber) -> None:
|
|
42
|
+
bucket = self._subscribers.get(name)
|
|
43
|
+
if not bucket:
|
|
44
|
+
return
|
|
45
|
+
with contextlib.suppress(ValueError):
|
|
46
|
+
bucket.remove(callback)
|
|
47
|
+
|
|
48
|
+
async def publish(self, name: str, **payload: Any) -> None:
|
|
49
|
+
"""Publish an event, dispatching to all registered subscribers."""
|
|
50
|
+
event = Event(name=name, payload=dict(payload))
|
|
51
|
+
for callback in list(self._subscribers.get(name, ())):
|
|
52
|
+
result = callback(event)
|
|
53
|
+
if asyncio.iscoroutine(result):
|
|
54
|
+
await result
|
|
55
|
+
|
|
56
|
+
def emit(self, name: str, **payload: Any) -> None:
|
|
57
|
+
"""Synchronous variant of :meth:`publish`; async subs are not awaited."""
|
|
58
|
+
event = Event(name=name, payload=dict(payload))
|
|
59
|
+
for callback in list(self._subscribers.get(name, ())):
|
|
60
|
+
result = callback(event)
|
|
61
|
+
if asyncio.iscoroutine(result):
|
|
62
|
+
# Fire-and-forget; require an event loop to be running.
|
|
63
|
+
with contextlib.suppress(RuntimeError):
|
|
64
|
+
loop = asyncio.get_running_loop()
|
|
65
|
+
task = loop.create_task(result)
|
|
66
|
+
self._pending_tasks.add(task)
|
|
67
|
+
task.add_done_callback(self._pending_tasks.discard)
|
forgecli/core/logging.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Logging configuration built on the standard library ``logging`` package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
_LOG_FORMAT: Final[str] = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
10
|
+
_configured = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def configure_logging(level: str = "INFO") -> None:
|
|
14
|
+
"""Configure root logging once for the lifetime of the process."""
|
|
15
|
+
global _configured
|
|
16
|
+
if _configured:
|
|
17
|
+
new_level = _coerce_level(level)
|
|
18
|
+
if logging.getLogger().level != logging.DEBUG or new_level == logging.DEBUG:
|
|
19
|
+
logging.getLogger().setLevel(new_level)
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
handler = logging.StreamHandler(stream=sys.stderr)
|
|
23
|
+
handler.setFormatter(logging.Formatter(_LOG_FORMAT))
|
|
24
|
+
|
|
25
|
+
root = logging.getLogger()
|
|
26
|
+
root.handlers.clear()
|
|
27
|
+
root.addHandler(handler)
|
|
28
|
+
root.setLevel(_coerce_level(level))
|
|
29
|
+
|
|
30
|
+
# Quiet noisy third-party loggers.
|
|
31
|
+
for noisy in ("httpx", "httpcore", "git", "urllib3"):
|
|
32
|
+
logging.getLogger(noisy).setLevel(logging.WARNING)
|
|
33
|
+
|
|
34
|
+
_configured = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_logger(name: str) -> logging.Logger:
|
|
38
|
+
"""Return a configured logger, configuring root logging lazily."""
|
|
39
|
+
if not _configured:
|
|
40
|
+
configure_logging()
|
|
41
|
+
return logging.getLogger(name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _coerce_level(level: str) -> int:
|
|
45
|
+
if level.isdigit():
|
|
46
|
+
return int(level)
|
|
47
|
+
return logging.getLevelName(level.upper())
|
forgecli/core/models.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Modular registry for all supported AI models in ForgeCLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
ModelTier = Literal["latest", "recommended", "normal", "legacy", "deprecated"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ModelDef:
|
|
13
|
+
id: str
|
|
14
|
+
display_name: str
|
|
15
|
+
tier: ModelTier
|
|
16
|
+
provider: str # Lowercase provider name, e.g., 'openai', 'groq'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Single source of truth for the model catalog
|
|
20
|
+
MODEL_CATALOG: list[ModelDef] = [
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# OpenAI
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
ModelDef(id="gpt-5.5", display_name="GPT-5.5", tier="latest", provider="openai"),
|
|
25
|
+
ModelDef(id="gpt-5", display_name="GPT-5", tier="recommended", provider="openai"),
|
|
26
|
+
ModelDef(id="gpt-5-mini", display_name="GPT-5 Mini", tier="normal", provider="openai"),
|
|
27
|
+
ModelDef(id="gpt-4.1", display_name="GPT-4.1", tier="legacy", provider="openai"),
|
|
28
|
+
ModelDef(id="gpt-4.1-mini", display_name="GPT-4.1 Mini", tier="legacy", provider="openai"),
|
|
29
|
+
ModelDef(id="gpt-4o", display_name="GPT-4o", tier="legacy", provider="openai"),
|
|
30
|
+
ModelDef(id="gpt-4o-mini", display_name="GPT-4o Mini", tier="legacy", provider="openai"),
|
|
31
|
+
ModelDef(id="gpt-4-turbo", display_name="GPT-4 Turbo", tier="legacy", provider="openai"),
|
|
32
|
+
ModelDef(id="o1", display_name="o1", tier="legacy", provider="openai"),
|
|
33
|
+
ModelDef(id="o1-preview", display_name="o1 Preview", tier="legacy", provider="openai"),
|
|
34
|
+
ModelDef(id="o1-mini", display_name="o1 Mini", tier="legacy", provider="openai"),
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Anthropic
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
ModelDef(id="claude-opus-4.8", display_name="Claude Opus 4.8", tier="latest", provider="anthropic"),
|
|
40
|
+
ModelDef(id="claude-opus-4.6", display_name="Claude Opus 4.6", tier="latest", provider="anthropic"),
|
|
41
|
+
ModelDef(id="claude-sonnet-4.6", display_name="Claude Sonnet 4.6", tier="recommended", provider="anthropic"),
|
|
42
|
+
ModelDef(id="claude-sonnet-4.5", display_name="Claude Sonnet 4.5", tier="legacy", provider="anthropic"),
|
|
43
|
+
ModelDef(id="claude-haiku-4.5", display_name="Claude Haiku 4.5", tier="legacy", provider="anthropic"),
|
|
44
|
+
ModelDef(id="claude-3-5-sonnet-latest", display_name="Claude 3.5 Sonnet", tier="legacy", provider="anthropic"),
|
|
45
|
+
ModelDef(id="claude-3-5-haiku-latest", display_name="Claude 3.5 Haiku", tier="legacy", provider="anthropic"),
|
|
46
|
+
ModelDef(id="claude-3-opus-latest", display_name="Claude 3 Opus", tier="legacy", provider="anthropic"),
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Google Gemini
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
ModelDef(id="gemini-2.5-pro", display_name="Gemini 2.5 Pro", tier="recommended", provider="google"),
|
|
52
|
+
ModelDef(id="gemini-2.5-flash", display_name="Gemini 2.5 Flash", tier="recommended", provider="google"),
|
|
53
|
+
ModelDef(id="gemini-2.5-flash-lite", display_name="Gemini 2.5 Flash Lite", tier="normal", provider="google"),
|
|
54
|
+
ModelDef(id="gemini-2.0-flash", display_name="Gemini 2.0 Flash", tier="legacy", provider="google"),
|
|
55
|
+
ModelDef(id="gemini-1.5-pro", display_name="Gemini 1.5 Pro", tier="legacy", provider="google"),
|
|
56
|
+
ModelDef(id="gemini-1.5-flash", display_name="Gemini 1.5 Flash", tier="legacy", provider="google"),
|
|
57
|
+
ModelDef(id="gemini-2.0-flash-exp", display_name="Gemini 2.0 Flash Exp", tier="legacy", provider="google"),
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# OpenRouter
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
ModelDef(id="glm-5.2", display_name="GLM 5.2", tier="normal", provider="openrouter"),
|
|
63
|
+
ModelDef(id="deepseek-v3", display_name="DeepSeek V3", tier="normal", provider="openrouter"),
|
|
64
|
+
ModelDef(id="deepseek-r1", display_name="DeepSeek R1", tier="normal", provider="openrouter"),
|
|
65
|
+
ModelDef(id="qwen3-coder", display_name="Qwen3 Coder", tier="normal", provider="openrouter"),
|
|
66
|
+
ModelDef(id="qwen3-32b", display_name="Qwen3 32B", tier="normal", provider="openrouter"),
|
|
67
|
+
ModelDef(id="kimi-k2", display_name="Kimi K2", tier="normal", provider="openrouter"),
|
|
68
|
+
ModelDef(id="llama-4-maverick", display_name="Llama 4 Maverick", tier="normal", provider="openrouter"),
|
|
69
|
+
ModelDef(id="llama-4-scout", display_name="Llama 4 Scout", tier="normal", provider="openrouter"),
|
|
70
|
+
ModelDef(id="llama-3.3-70b", display_name="Llama 3.3 70B", tier="normal", provider="openrouter"),
|
|
71
|
+
ModelDef(id="gemma-3", display_name="Gemma 3", tier="normal", provider="openrouter"),
|
|
72
|
+
ModelDef(id="devstral", display_name="Devstral", tier="normal", provider="openrouter"),
|
|
73
|
+
ModelDef(id="codestral", display_name="Codestral", tier="normal", provider="openrouter"),
|
|
74
|
+
ModelDef(id="phi-4", display_name="Phi-4", tier="normal", provider="openrouter"),
|
|
75
|
+
ModelDef(id="mistral-large", display_name="Mistral Large", tier="normal", provider="openrouter"),
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Groq
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
ModelDef(id="llama-4-scout", display_name="Llama 4 Scout", tier="normal", provider="groq"),
|
|
81
|
+
ModelDef(id="deepseek-r1", display_name="DeepSeek R1", tier="normal", provider="groq"),
|
|
82
|
+
ModelDef(id="qwen3-32b", display_name="Qwen3 32B", tier="normal", provider="groq"),
|
|
83
|
+
ModelDef(id="gemma-3", display_name="Gemma 3", tier="normal", provider="groq"),
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Mistral
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
ModelDef(id="mistral-large", display_name="Mistral Large", tier="normal", provider="mistral"),
|
|
89
|
+
ModelDef(id="magistral", display_name="Magistral", tier="normal", provider="mistral"),
|
|
90
|
+
ModelDef(id="mistral-small", display_name="Mistral Small", tier="normal", provider="mistral"),
|
|
91
|
+
ModelDef(id="codestral", display_name="Codestral", tier="normal", provider="mistral"),
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# MiniMax
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
ModelDef(id="abab6.5g-chat", display_name="Abab 6.5G Chat", tier="recommended", provider="minimax"),
|
|
97
|
+
ModelDef(id="abab6.5-chat", display_name="Abab 6.5 Chat", tier="legacy", provider="minimax"),
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# xAI (Grok)
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
ModelDef(id="grok-2", display_name="Grok 2", tier="recommended", provider="xai"),
|
|
103
|
+
ModelDef(id="grok-beta", display_name="Grok Beta", tier="latest", provider="xai"),
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Together AI
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
ModelDef(id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="together"),
|
|
109
|
+
ModelDef(id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="together"),
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Fireworks AI
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
ModelDef(id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="fireworks"),
|
|
115
|
+
ModelDef(id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="fireworks"),
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# Cohere
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
ModelDef(id="command-r-plus", display_name="Command R+", tier="recommended", provider="cohere"),
|
|
121
|
+
ModelDef(id="command-r", display_name="Command R", tier="normal", provider="cohere"),
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# NVIDIA NIM
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
ModelDef(id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="nvidia"),
|
|
127
|
+
ModelDef(id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="nvidia"),
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Ollama
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
ModelDef(id="llama3", display_name="Llama 3", tier="normal", provider="ollama"),
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# LM Studio
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
ModelDef(id="local-model", display_name="Local Model", tier="normal", provider="lmstudio"),
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# vLLM
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
ModelDef(id="local-model", display_name="Local Model", tier="normal", provider="vllm"),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_model_def(model_id: str, provider: str | None = None) -> ModelDef | None:
|
|
147
|
+
"""Retrieve the model definition by model ID, optionally filtering by provider."""
|
|
148
|
+
model_id_lower = model_id.lower().strip()
|
|
149
|
+
provider_lower = provider.lower().strip() if provider else None
|
|
150
|
+
for m in MODEL_CATALOG:
|
|
151
|
+
if m.id == model_id_lower and (provider_lower is None or m.provider == provider_lower):
|
|
152
|
+
return m
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_display_name(model_id: str, provider: str | None = None) -> str:
|
|
157
|
+
"""Get the friendly display name for a model, defaulting to the ID itself."""
|
|
158
|
+
m = get_model_def(model_id, provider)
|
|
159
|
+
return m.display_name if m else model_id
|
forgecli/core/plugins.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Plugin discovery and lifecycle.
|
|
2
|
+
|
|
3
|
+
The plugin system is intentionally tiny for the scaffold: plugins are
|
|
4
|
+
loaded by entry point group (``forgecli.plugins``) and must implement
|
|
5
|
+
:class:`Plugin`. A real implementation may also support directory
|
|
6
|
+
plugins (``plugins_dir``) but the interface is the same.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from importlib import metadata
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from forgecli.core.context import AppContext
|
|
16
|
+
from forgecli.core.errors import PluginError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Plugin(ABC):
|
|
20
|
+
"""Base class for ForgeCLI plugins."""
|
|
21
|
+
|
|
22
|
+
name: str = "plugin"
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def configure(self, context: AppContext) -> None:
|
|
26
|
+
"""Register services, commands, providers, etc. on ``context``."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def discover_plugins(group: str = "forgecli.plugins") -> list[Plugin]:
|
|
30
|
+
"""Discover installed plugins via the ``group`` entry point."""
|
|
31
|
+
plugins: list[Plugin] = []
|
|
32
|
+
try:
|
|
33
|
+
entries = metadata.entry_points(group=group)
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
raise PluginError(f"Plugin discovery failed: {exc}") from exc
|
|
36
|
+
for ep in entries:
|
|
37
|
+
try:
|
|
38
|
+
plugin_cls = ep.load()
|
|
39
|
+
plugin = plugin_cls() # type: ignore[call-arg]
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
raise PluginError(f"Failed to load plugin {ep.name!r}: {exc}") from exc
|
|
42
|
+
if not isinstance(plugin, Plugin):
|
|
43
|
+
raise PluginError(
|
|
44
|
+
f"Plugin {ep.name!r} must subclass forgecli.plugins.Plugin"
|
|
45
|
+
)
|
|
46
|
+
plugins.append(plugin)
|
|
47
|
+
return plugins
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def install_plugins(context: AppContext, plugins: list[Plugin]) -> None:
|
|
51
|
+
"""Configure each plugin against ``context``."""
|
|
52
|
+
for plugin in plugins:
|
|
53
|
+
plugin.configure(context)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["Any", "Plugin", "discover_plugins", "install_plugins"]
|
forgecli/core/service.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Composable service lifecycle base class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from forgecli.core.logging import get_logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Service:
|
|
9
|
+
"""Base class for application services with structured logging."""
|
|
10
|
+
|
|
11
|
+
name: str = "service"
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.log = get_logger(f"forgecli.{self.name}")
|
|
15
|
+
|
|
16
|
+
async def start(self) -> None: # pragma: no cover - placeholder
|
|
17
|
+
"""Bring the service online. Override in subclasses."""
|
|
18
|
+
self.log.debug("start() called (no-op)")
|
|
19
|
+
|
|
20
|
+
async def stop(self) -> None: # pragma: no cover - placeholder
|
|
21
|
+
"""Tear the service down. Override in subclasses."""
|
|
22
|
+
self.log.debug("stop() called (no-op)")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Auto-generate project documentation from the Graphify knowledge graph.
|
|
2
|
+
|
|
3
|
+
The generator walks the project, asks Graphify for a snapshot, and
|
|
4
|
+
emits a Markdown report at ``docs/OVERVIEW.md`` with:
|
|
5
|
+
|
|
6
|
+
* a module-by-module summary derived from the graph nodes;
|
|
7
|
+
* the file tree of the project;
|
|
8
|
+
* a flat list of every symbol with its location.
|
|
9
|
+
|
|
10
|
+
The output is intentionally simple: the goal is to give an LLM
|
|
11
|
+
(or a human) a starting point for richer docs.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from datetime import date
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from forgecli.core.context import AppContext
|
|
20
|
+
from forgecli.graph.backend_graphify import GraphifyRepositoryGraph
|
|
21
|
+
from forgecli.utils.fs import ensure_dir
|
|
22
|
+
|
|
23
|
+
_INTRO_TEMPLATE = """\
|
|
24
|
+
# {project} — Auto-generated overview
|
|
25
|
+
|
|
26
|
+
_Generated on {today} by `forge docs`._
|
|
27
|
+
|
|
28
|
+
This document is a starting point: it summarises the module layout
|
|
29
|
+
and lists every symbol the Graphify knowledge graph knows about.
|
|
30
|
+
For deeper documentation, run `forge plan`, `forge build`, or
|
|
31
|
+
`forge explain` on individual modules.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_docs(context: AppContext, *, output: Path | None = None) -> Path:
|
|
37
|
+
"""Generate the overview file and return its path."""
|
|
38
|
+
root = context.cwd
|
|
39
|
+
target = output or (root / "docs" / "OVERVIEW.md")
|
|
40
|
+
ensure_dir(target.parent)
|
|
41
|
+
|
|
42
|
+
graph = GraphifyRepositoryGraph(root=root)
|
|
43
|
+
snapshot = graph._cached # may be None; load() if needed.
|
|
44
|
+
|
|
45
|
+
# Build a tiny ad-hoc snapshot by re-walking the project.
|
|
46
|
+
nodes = _walk_nodes(root)
|
|
47
|
+
communities = _community_buckets(nodes)
|
|
48
|
+
|
|
49
|
+
lines: list[str] = [
|
|
50
|
+
_INTRO_TEMPLATE.format(project=root.name, today=date.today().isoformat()),
|
|
51
|
+
]
|
|
52
|
+
lines.append("## Modules\n")
|
|
53
|
+
for community, members in sorted(communities.items()):
|
|
54
|
+
lines.append(f"### {community}\n")
|
|
55
|
+
for node in sorted(members, key=lambda n: str(n["path"])):
|
|
56
|
+
location = (
|
|
57
|
+
f"`{node['path']}:{node['line']}`"
|
|
58
|
+
if node["line"]
|
|
59
|
+
else f"`{node['path']}`"
|
|
60
|
+
)
|
|
61
|
+
lines.append(f"- {node['label']} — {location}")
|
|
62
|
+
lines.append("")
|
|
63
|
+
if not nodes:
|
|
64
|
+
lines.append("_No indexed symbols found._\n")
|
|
65
|
+
target.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
66
|
+
_ = snapshot # silence unused
|
|
67
|
+
return target
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _walk_nodes(root: Path) -> list[dict[str, object]]:
|
|
71
|
+
"""Build a small node list from the project tree.
|
|
72
|
+
|
|
73
|
+
This is a *fallback* when Graphify hasn't been run. The docs
|
|
74
|
+
generator is meant to be cheap and offline; it doesn't shell out
|
|
75
|
+
to the Graphify CLI.
|
|
76
|
+
"""
|
|
77
|
+
nodes: list[dict[str, object]] = []
|
|
78
|
+
for path in sorted(root.rglob("*.py")):
|
|
79
|
+
if any(part.startswith(".") for part in path.parts):
|
|
80
|
+
continue
|
|
81
|
+
if any(part in {"__pycache__", "node_modules", ".venv", "venv"} for part in path.parts):
|
|
82
|
+
continue
|
|
83
|
+
rel = path.relative_to(root)
|
|
84
|
+
try:
|
|
85
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
86
|
+
except OSError:
|
|
87
|
+
continue
|
|
88
|
+
nodes.append({"path": str(rel), "label": path.name, "line": 1})
|
|
89
|
+
for index, line in enumerate(text.splitlines(), start=1):
|
|
90
|
+
stripped = line.lstrip()
|
|
91
|
+
if stripped.startswith(("def ", "class ", "async def ")):
|
|
92
|
+
name = _extract_symbol_name(stripped)
|
|
93
|
+
if name:
|
|
94
|
+
nodes.append({"path": str(rel), "label": name, "line": index})
|
|
95
|
+
return nodes
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _extract_symbol_name(line: str) -> str | None:
|
|
99
|
+
"""Return the symbol name from a ``def foo(...)`` / ``class Foo:`` line."""
|
|
100
|
+
line = line.strip()
|
|
101
|
+
for prefix in ("async def ", "def ", "class "):
|
|
102
|
+
if line.startswith(prefix):
|
|
103
|
+
rest = line[len(prefix):]
|
|
104
|
+
return rest.split("(", 1)[0].split(":", 1)[0].strip() or None
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _community_buckets(nodes: list[dict[str, object]]) -> dict[str, list[dict[str, object]]]:
|
|
109
|
+
"""Group nodes by their top-level directory (the "module")."""
|
|
110
|
+
buckets: dict[str, list[dict[str, object]]] = {}
|
|
111
|
+
for node in nodes:
|
|
112
|
+
path = str(node["path"])
|
|
113
|
+
parts = path.split("/")
|
|
114
|
+
module = parts[0] if len(parts) > 1 else path
|
|
115
|
+
buckets.setdefault(module, []).append(node)
|
|
116
|
+
return buckets
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = ["generate_docs"]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The ForgeCLI Execution Engine.
|
|
2
|
+
|
|
3
|
+
This package defines the *contract* for every orchestration stage
|
|
4
|
+
in ForgeCLI. The pipeline is a fixed sequence of eight stages:
|
|
5
|
+
|
|
6
|
+
1. Intent Analyzer — turn the prompt into an Intent
|
|
7
|
+
2. Repository Analyzer — query Graphify for relevant context
|
|
8
|
+
3. Context Optimizer — apply Ponytail to the prompt + context
|
|
9
|
+
4. Planning Engine — produce a SoftwarePlan
|
|
10
|
+
5. Model Router — pick (provider, model) for the call
|
|
11
|
+
6. Execution Engine — invoke the LLM, extract a diff
|
|
12
|
+
7. Validation Engine — apply the diff + run tests + auto-fix
|
|
13
|
+
8. Git Engine — stage / push the changes
|
|
14
|
+
|
|
15
|
+
Every stage is an independent object behind the :class:`Stage`
|
|
16
|
+
Protocol. The :class:`ExecutionEngine` runs them in order, emits
|
|
17
|
+
structured events, supports retries, cancellation, and plugin
|
|
18
|
+
hooks. No business logic lives in this package — the actual
|
|
19
|
+
implementations live in :mod:`forgecli.engine.stages` and may be
|
|
20
|
+
replaced by plugins.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from forgecli.engine.context import (
|
|
24
|
+
EngineContext,
|
|
25
|
+
IntentAnalysis,
|
|
26
|
+
ModelSelection,
|
|
27
|
+
RetrievalResult,
|
|
28
|
+
StageLog,
|
|
29
|
+
)
|
|
30
|
+
from forgecli.engine.defaults import (
|
|
31
|
+
default_registry,
|
|
32
|
+
)
|
|
33
|
+
from forgecli.engine.events import (
|
|
34
|
+
EngineCancelledError,
|
|
35
|
+
EngineEvent,
|
|
36
|
+
EventBus,
|
|
37
|
+
LogLevel,
|
|
38
|
+
ProgressEvent,
|
|
39
|
+
StageEvent,
|
|
40
|
+
TextLogEvent,
|
|
41
|
+
)
|
|
42
|
+
from forgecli.engine.execution import (
|
|
43
|
+
EngineResult,
|
|
44
|
+
ExecutionEngine,
|
|
45
|
+
PipelineBuilder,
|
|
46
|
+
Stage,
|
|
47
|
+
StageContext,
|
|
48
|
+
StageRegistry,
|
|
49
|
+
StageResult,
|
|
50
|
+
StageStatus,
|
|
51
|
+
)
|
|
52
|
+
from forgecli.engine.plugins import (
|
|
53
|
+
EnginePluginFactory,
|
|
54
|
+
HookManager,
|
|
55
|
+
PluginHook,
|
|
56
|
+
register_plugin,
|
|
57
|
+
stage_as_plugin,
|
|
58
|
+
)
|
|
59
|
+
from forgecli.engine.stages import (
|
|
60
|
+
ContextOptimizerStage,
|
|
61
|
+
ExecutionEngineStage,
|
|
62
|
+
GitEngineStage,
|
|
63
|
+
IntentAnalyzerStage,
|
|
64
|
+
ModelRouterStage,
|
|
65
|
+
PlanningEngineStage,
|
|
66
|
+
RepositoryAnalyzerStage,
|
|
67
|
+
ValidationEngineStage,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"ContextOptimizerStage",
|
|
72
|
+
"EngineCancelledError",
|
|
73
|
+
"EngineContext",
|
|
74
|
+
"EngineEvent",
|
|
75
|
+
"EnginePluginFactory",
|
|
76
|
+
"EngineResult",
|
|
77
|
+
"EventBus",
|
|
78
|
+
"ExecutionEngine",
|
|
79
|
+
"ExecutionEngineStage",
|
|
80
|
+
"GitEngineStage",
|
|
81
|
+
"HookManager",
|
|
82
|
+
"IntentAnalysis",
|
|
83
|
+
"IntentAnalyzerStage",
|
|
84
|
+
"LogLevel",
|
|
85
|
+
"ModelRouterStage",
|
|
86
|
+
"ModelSelection",
|
|
87
|
+
"PipelineBuilder",
|
|
88
|
+
"PlanningEngineStage",
|
|
89
|
+
"PluginHook",
|
|
90
|
+
"ProgressEvent",
|
|
91
|
+
"RepositoryAnalyzerStage",
|
|
92
|
+
"RetrievalResult",
|
|
93
|
+
"Stage",
|
|
94
|
+
"StageContext",
|
|
95
|
+
"StageEvent",
|
|
96
|
+
"StageLog",
|
|
97
|
+
"StageRegistry",
|
|
98
|
+
"StageResult",
|
|
99
|
+
"StageStatus",
|
|
100
|
+
"TextLogEvent",
|
|
101
|
+
"ValidationEngineStage",
|
|
102
|
+
"default_registry",
|
|
103
|
+
"register_plugin",
|
|
104
|
+
"stage_as_plugin",
|
|
105
|
+
]
|