multi-forge 0.2.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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
forge/cli/auth.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""Authentication CLI commands.
|
|
2
|
+
|
|
3
|
+
Provides ``forge authentication login`` for storing credentials in
|
|
4
|
+
``~/.forge/credentials.yaml``, ``forge authentication status`` to check
|
|
5
|
+
credential status, ``forge authentication logout`` to remove stored
|
|
6
|
+
credentials, and ``forge authentication profiles`` to list saved profiles.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
forge authentication login # Credential selection menu
|
|
10
|
+
forge authentication login -c anthropic-api # Single credential
|
|
11
|
+
forge authentication login -c anthropic-api --profile work
|
|
12
|
+
forge authentication status # Dual-view status
|
|
13
|
+
forge authentication logout --profile default # Remove stored credentials
|
|
14
|
+
forge authentication profiles # List saved profiles
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from forge.core.auth.capabilities import (
|
|
25
|
+
CREDENTIALS,
|
|
26
|
+
RETIRED_NAMES,
|
|
27
|
+
Credential,
|
|
28
|
+
EnvVar,
|
|
29
|
+
)
|
|
30
|
+
from forge.core.auth.credentials_file import (
|
|
31
|
+
CredentialVersionError,
|
|
32
|
+
delete_profile,
|
|
33
|
+
list_profiles,
|
|
34
|
+
load_profile,
|
|
35
|
+
resolve_profile,
|
|
36
|
+
save_profile,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _mask_value(value: str) -> str:
|
|
43
|
+
"""Mask all but first/last 4 chars of a secret value."""
|
|
44
|
+
if len(value) <= 8:
|
|
45
|
+
return "****"
|
|
46
|
+
return value[:4] + "…" + value[-4:]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_var_source(
|
|
50
|
+
ev: EnvVar,
|
|
51
|
+
file_secrets: dict[str, str],
|
|
52
|
+
ignore_env: bool,
|
|
53
|
+
) -> tuple[str | None, str]:
|
|
54
|
+
"""Resolve a single env var's value and source label.
|
|
55
|
+
|
|
56
|
+
Returns (value_or_None, source_label).
|
|
57
|
+
"""
|
|
58
|
+
env_val = os.environ.get(ev.name)
|
|
59
|
+
file_val = file_secrets.get(ev.name)
|
|
60
|
+
|
|
61
|
+
if ignore_env:
|
|
62
|
+
if file_val:
|
|
63
|
+
return file_val, "file"
|
|
64
|
+
if env_val:
|
|
65
|
+
return None, "not configured (env ignored)"
|
|
66
|
+
return None, "not configured"
|
|
67
|
+
|
|
68
|
+
if env_val:
|
|
69
|
+
return env_val, "env"
|
|
70
|
+
if file_val:
|
|
71
|
+
return file_val, "file"
|
|
72
|
+
return None, "not configured"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _credential_state(
|
|
76
|
+
cred: Credential,
|
|
77
|
+
file_secrets: dict[str, str],
|
|
78
|
+
ignore_env: bool,
|
|
79
|
+
profile_name: str,
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Compute aggregate configuration state for a credential.
|
|
82
|
+
|
|
83
|
+
Returns one of: "configured (env)", "configured (file)", "configured (env+file)",
|
|
84
|
+
"partially configured", "not configured", "not configured (env ignored)".
|
|
85
|
+
"""
|
|
86
|
+
sources: set[str] = set()
|
|
87
|
+
any_missing = False
|
|
88
|
+
env_ignored_present = False
|
|
89
|
+
|
|
90
|
+
for ev in cred.env_vars:
|
|
91
|
+
if not ev.required:
|
|
92
|
+
continue
|
|
93
|
+
_, source = _resolve_var_source(ev, file_secrets, ignore_env)
|
|
94
|
+
if source == "not configured":
|
|
95
|
+
any_missing = True
|
|
96
|
+
elif source == "not configured (env ignored)":
|
|
97
|
+
any_missing = True
|
|
98
|
+
env_ignored_present = True
|
|
99
|
+
else:
|
|
100
|
+
sources.add(source)
|
|
101
|
+
|
|
102
|
+
if not sources:
|
|
103
|
+
if env_ignored_present:
|
|
104
|
+
return "not configured (env ignored)"
|
|
105
|
+
return "not configured"
|
|
106
|
+
if any_missing:
|
|
107
|
+
return "partially configured"
|
|
108
|
+
|
|
109
|
+
if sources == {"env"}:
|
|
110
|
+
return "configured (env)"
|
|
111
|
+
if sources == {"file"}:
|
|
112
|
+
return f"configured (file:{profile_name})"
|
|
113
|
+
if sources == {"env", "file"}:
|
|
114
|
+
return "configured (env+file)"
|
|
115
|
+
return "configured"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _capability_summary(cred: Credential) -> str:
|
|
119
|
+
"""One-line capability description for the credential menu."""
|
|
120
|
+
features = ", ".join(cred.unlocks_features)
|
|
121
|
+
if features and cred.note:
|
|
122
|
+
return f"{features} ({cred.note})"
|
|
123
|
+
if features:
|
|
124
|
+
return features
|
|
125
|
+
if cred.note:
|
|
126
|
+
return cred.note
|
|
127
|
+
return cred.name
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── Click commands ────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@click.group()
|
|
134
|
+
def auth() -> None:
|
|
135
|
+
"""Manage authentication and credentials.
|
|
136
|
+
|
|
137
|
+
\b
|
|
138
|
+
Examples:
|
|
139
|
+
forge authentication login # Store credentials
|
|
140
|
+
forge authentication status # Check credential sources
|
|
141
|
+
forge authentication profiles # List saved profiles
|
|
142
|
+
"""
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@auth.command("login")
|
|
147
|
+
@click.option(
|
|
148
|
+
"--credential",
|
|
149
|
+
"-c",
|
|
150
|
+
"credential",
|
|
151
|
+
type=str,
|
|
152
|
+
default=None,
|
|
153
|
+
help="Credential to configure (e.g. openrouter, anthropic-api, gemini-api)",
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--profile",
|
|
157
|
+
default=None,
|
|
158
|
+
help="Profile name to store credentials in (default: 'default' or FORGE_PROFILE)",
|
|
159
|
+
)
|
|
160
|
+
def login(credential: str | None, profile: str | None) -> None:
|
|
161
|
+
"""Store credentials for Forge proxy routing and subprocesses.
|
|
162
|
+
|
|
163
|
+
These are for Forge, NOT your Claude Code login (OAuth/Max plan).
|
|
164
|
+
Press Enter to keep the existing value or skip env-provided keys.
|
|
165
|
+
|
|
166
|
+
\b
|
|
167
|
+
Examples:
|
|
168
|
+
forge auth login # Credential selection menu
|
|
169
|
+
forge auth login -c anthropic-api # Single credential
|
|
170
|
+
forge auth login -c openrouter --profile work
|
|
171
|
+
"""
|
|
172
|
+
profile_name = resolve_profile(profile)
|
|
173
|
+
|
|
174
|
+
# Validate credential name
|
|
175
|
+
if credential is not None:
|
|
176
|
+
if credential in RETIRED_NAMES:
|
|
177
|
+
click.secho(RETIRED_NAMES[credential], fg="yellow", err=True)
|
|
178
|
+
raise SystemExit(1)
|
|
179
|
+
if credential not in CREDENTIALS:
|
|
180
|
+
click.secho(f"Unknown credential '{credential}'.", fg="red", err=True)
|
|
181
|
+
click.echo(f"Available: {', '.join(CREDENTIALS)}", err=True)
|
|
182
|
+
raise SystemExit(1)
|
|
183
|
+
|
|
184
|
+
ignore_env = _get_auth_ignore_env()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
existing = load_profile(profile_name)
|
|
188
|
+
except CredentialVersionError as e:
|
|
189
|
+
click.secho(f"✗ {e}", fg="red")
|
|
190
|
+
raise SystemExit(1)
|
|
191
|
+
except ValueError:
|
|
192
|
+
existing = {}
|
|
193
|
+
click.secho("⚠︎ Existing credentials file is corrupt -- starting fresh.", fg="yellow")
|
|
194
|
+
|
|
195
|
+
# Select which credentials to configure
|
|
196
|
+
if credential is not None:
|
|
197
|
+
to_configure = [CREDENTIALS[credential]]
|
|
198
|
+
else:
|
|
199
|
+
to_configure = _credential_menu(existing, ignore_env, profile_name)
|
|
200
|
+
if not to_configure:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Prompt for each credential's env vars
|
|
204
|
+
collected: dict[str, str] = {}
|
|
205
|
+
|
|
206
|
+
click.echo(f"\nConfiguring credentials for profile '{profile_name}'")
|
|
207
|
+
click.echo("-" * 40)
|
|
208
|
+
|
|
209
|
+
for cred in to_configure:
|
|
210
|
+
_prompt_credential(cred, existing, collected, ignore_env)
|
|
211
|
+
|
|
212
|
+
if collected:
|
|
213
|
+
path = save_profile(profile_name, collected, merge=True)
|
|
214
|
+
click.echo()
|
|
215
|
+
click.secho(
|
|
216
|
+
f"✓ Credentials saved to {path} (profile: {profile_name})",
|
|
217
|
+
fg="green",
|
|
218
|
+
)
|
|
219
|
+
click.echo("Tip: Use 'forge auth status' to verify.")
|
|
220
|
+
else:
|
|
221
|
+
click.echo("\nNo credentials to save.")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _credential_menu(
|
|
225
|
+
file_secrets: dict[str, str],
|
|
226
|
+
ignore_env: bool,
|
|
227
|
+
profile_name: str,
|
|
228
|
+
) -> list[Credential]:
|
|
229
|
+
"""Show numbered credential selection menu. Returns selected credentials."""
|
|
230
|
+
click.echo("\nForge credentials")
|
|
231
|
+
click.echo("These are for Forge proxy routing and subprocesses, NOT your Claude Code login.")
|
|
232
|
+
click.echo("Claude Code authenticates separately (OAuth, Max plan, etc.).\n")
|
|
233
|
+
|
|
234
|
+
cred_list = list(CREDENTIALS.values())
|
|
235
|
+
for i, cred in enumerate(cred_list, 1):
|
|
236
|
+
state = _credential_state(cred, file_secrets, ignore_env, profile_name)
|
|
237
|
+
marker = "*" if state.startswith("configured") else "-"
|
|
238
|
+
summary = _capability_summary(cred)
|
|
239
|
+
click.echo(f" [{i}] {cred.name:<18} {marker} {state:<28} {summary}")
|
|
240
|
+
|
|
241
|
+
click.echo()
|
|
242
|
+
raw = click.prompt(
|
|
243
|
+
f"Select credentials [1-{len(cred_list)}, comma-separated, or 'all']",
|
|
244
|
+
default="all",
|
|
245
|
+
show_default=True,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if raw.strip().lower() == "all":
|
|
249
|
+
return cred_list
|
|
250
|
+
|
|
251
|
+
selected: list[Credential] = []
|
|
252
|
+
for part in raw.split(","):
|
|
253
|
+
part = part.strip()
|
|
254
|
+
try:
|
|
255
|
+
idx = int(part) - 1
|
|
256
|
+
if 0 <= idx < len(cred_list):
|
|
257
|
+
selected.append(cred_list[idx])
|
|
258
|
+
except ValueError:
|
|
259
|
+
click.secho(f"Ignoring invalid selection: {part}", fg="yellow")
|
|
260
|
+
|
|
261
|
+
return selected
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _prompt_credential(
|
|
265
|
+
cred: Credential,
|
|
266
|
+
existing: dict[str, str],
|
|
267
|
+
collected: dict[str, str],
|
|
268
|
+
ignore_env: bool,
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Prompt for a single credential's env vars."""
|
|
271
|
+
header = f"\n{cred.name}"
|
|
272
|
+
if cred.note:
|
|
273
|
+
header += f": {cred.note}"
|
|
274
|
+
click.echo(header)
|
|
275
|
+
|
|
276
|
+
if cred.not_needed_for:
|
|
277
|
+
click.echo()
|
|
278
|
+
for item in cred.not_needed_for:
|
|
279
|
+
click.echo(f" NOT needed for: {item}")
|
|
280
|
+
click.echo()
|
|
281
|
+
|
|
282
|
+
for ev in cred.env_vars:
|
|
283
|
+
_prompt_env_var(ev, existing, collected, ignore_env)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _prompt_env_var(
|
|
287
|
+
ev: EnvVar,
|
|
288
|
+
existing: dict[str, str],
|
|
289
|
+
collected: dict[str, str],
|
|
290
|
+
ignore_env: bool,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Prompt for a single env var with env-aware skip behavior."""
|
|
293
|
+
current = existing.get(ev.name, "")
|
|
294
|
+
raw_env_value = os.environ.get(ev.name)
|
|
295
|
+
env_value = None if ignore_env else raw_env_value
|
|
296
|
+
|
|
297
|
+
if ignore_env and raw_env_value:
|
|
298
|
+
display = _mask_value(raw_env_value) if ev.secret else raw_env_value
|
|
299
|
+
click.echo(f" {ev.name}: set in environment ({display}) but auth_ignore_env is active.")
|
|
300
|
+
click.echo(" Enter a value for the credential file, or press Enter to skip.")
|
|
301
|
+
prompt_text = f" {ev.name} [skip]"
|
|
302
|
+
elif env_value:
|
|
303
|
+
display = _mask_value(env_value) if ev.secret else env_value
|
|
304
|
+
click.echo(f" {ev.name}: already set via environment variable ({display})")
|
|
305
|
+
click.echo(" Storing in credential file is optional (env var takes precedence).")
|
|
306
|
+
prompt_text = f" {ev.name} [skip]"
|
|
307
|
+
elif current:
|
|
308
|
+
default_display = _mask_value(current) if ev.secret else current
|
|
309
|
+
prompt_text = f" {ev.name} [{default_display}]"
|
|
310
|
+
elif ev.default_value:
|
|
311
|
+
click.echo(f" {ev.name}: default is {ev.default_value}")
|
|
312
|
+
prompt_text = f" {ev.name} [skip]"
|
|
313
|
+
else:
|
|
314
|
+
prompt_text = f" {ev.name}"
|
|
315
|
+
|
|
316
|
+
value = click.prompt(
|
|
317
|
+
prompt_text,
|
|
318
|
+
default="",
|
|
319
|
+
show_default=False,
|
|
320
|
+
hide_input=ev.secret,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if value:
|
|
324
|
+
collected[ev.name] = value
|
|
325
|
+
elif current:
|
|
326
|
+
collected[ev.name] = current
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@auth.command("status")
|
|
330
|
+
@click.option(
|
|
331
|
+
"--profile",
|
|
332
|
+
default=None,
|
|
333
|
+
help="Profile to check (default: 'default' or FORGE_PROFILE)",
|
|
334
|
+
)
|
|
335
|
+
def status(profile: str | None) -> None:
|
|
336
|
+
"""Show credential status with capability summary and source details.
|
|
337
|
+
|
|
338
|
+
\b
|
|
339
|
+
Examples:
|
|
340
|
+
forge authentication status
|
|
341
|
+
forge authentication status --profile work
|
|
342
|
+
"""
|
|
343
|
+
profile_name = resolve_profile(profile)
|
|
344
|
+
|
|
345
|
+
ignore_env = _get_auth_ignore_env()
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
file_secrets = load_profile(profile_name)
|
|
349
|
+
except CredentialVersionError as e:
|
|
350
|
+
click.secho(f"✗ {e}", fg="red")
|
|
351
|
+
raise SystemExit(1)
|
|
352
|
+
except ValueError:
|
|
353
|
+
file_secrets = {}
|
|
354
|
+
click.secho("⚠︎ Credentials file is corrupt -- file-based values unavailable.", fg="yellow")
|
|
355
|
+
click.echo("Tip: Run 'forge auth login' to recreate the file.")
|
|
356
|
+
|
|
357
|
+
click.echo(f"\nCredential status (profile: {profile_name})")
|
|
358
|
+
click.echo("=" * 50)
|
|
359
|
+
|
|
360
|
+
# Section 1: Capability summary
|
|
361
|
+
configured: list[str] = []
|
|
362
|
+
not_configured: list[str] = []
|
|
363
|
+
|
|
364
|
+
for cred in CREDENTIALS.values():
|
|
365
|
+
state = _credential_state(cred, file_secrets, ignore_env, profile_name)
|
|
366
|
+
summary = _capability_summary(cred)
|
|
367
|
+
if state.startswith("configured"):
|
|
368
|
+
# Find primary source for display
|
|
369
|
+
primary_source = state.split("(", 1)[1].rstrip(")") if "(" in state else ""
|
|
370
|
+
configured.append(f" * {cred.name:<18} {summary} ({primary_source})")
|
|
371
|
+
else:
|
|
372
|
+
not_configured.append(f" - {cred.name:<18} {summary} ({state})")
|
|
373
|
+
|
|
374
|
+
if configured:
|
|
375
|
+
click.echo("\nConfigured capabilities:")
|
|
376
|
+
for line in configured:
|
|
377
|
+
click.secho(line, fg="green")
|
|
378
|
+
|
|
379
|
+
if not_configured:
|
|
380
|
+
click.echo("\nNot configured (set up if needed):")
|
|
381
|
+
for line in not_configured:
|
|
382
|
+
click.echo(line)
|
|
383
|
+
|
|
384
|
+
# Section 2: Credential details
|
|
385
|
+
click.echo("\nCredential details:")
|
|
386
|
+
|
|
387
|
+
for cred in CREDENTIALS.values():
|
|
388
|
+
click.echo(f"\n {cred.name}")
|
|
389
|
+
|
|
390
|
+
for ev in cred.env_vars:
|
|
391
|
+
value, source = _resolve_var_source(ev, file_secrets, ignore_env)
|
|
392
|
+
if value:
|
|
393
|
+
display = _mask_value(value) if ev.secret else value
|
|
394
|
+
source_label = f"file:{profile_name}" if source == "file" else source
|
|
395
|
+
click.secho(f" * {ev.name} = {display} ({source_label})", fg="green")
|
|
396
|
+
elif ev.default_value and source == "not configured":
|
|
397
|
+
click.echo(f" - {ev.name} = {ev.default_value} (default)")
|
|
398
|
+
else:
|
|
399
|
+
click.echo(f" - {ev.name} {source}")
|
|
400
|
+
|
|
401
|
+
click.echo()
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@auth.command("logout")
|
|
405
|
+
@click.option(
|
|
406
|
+
"--profile",
|
|
407
|
+
default=None,
|
|
408
|
+
help="Profile to remove credentials from (default: 'default' or FORGE_PROFILE)",
|
|
409
|
+
)
|
|
410
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
411
|
+
@click.option("--force", "-f", is_flag=True, hidden=True, help="Deprecated alias for --yes")
|
|
412
|
+
def logout(profile: str | None, yes: bool, force: bool) -> None:
|
|
413
|
+
"""Remove stored credentials for a profile.
|
|
414
|
+
|
|
415
|
+
Deletes the profile from ~/.forge/credentials.yaml.
|
|
416
|
+
Environment variables are not affected.
|
|
417
|
+
|
|
418
|
+
\b
|
|
419
|
+
Examples:
|
|
420
|
+
forge authentication logout
|
|
421
|
+
forge authentication logout --profile work
|
|
422
|
+
forge authentication logout -y # Skip confirmation
|
|
423
|
+
"""
|
|
424
|
+
yes = yes or force
|
|
425
|
+
profile_name = resolve_profile(profile)
|
|
426
|
+
|
|
427
|
+
if not yes:
|
|
428
|
+
if not click.confirm(f"Remove stored credentials for profile '{profile_name}'?"):
|
|
429
|
+
click.echo("Aborted.")
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
if delete_profile(profile_name):
|
|
433
|
+
click.secho(f"✓ Removed profile '{profile_name}'", fg="green")
|
|
434
|
+
else:
|
|
435
|
+
click.echo(f"Profile '{profile_name}' not found (nothing to remove).")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@auth.command("profiles")
|
|
439
|
+
def profiles_cmd() -> None:
|
|
440
|
+
"""List saved credential profiles.
|
|
441
|
+
|
|
442
|
+
\b
|
|
443
|
+
Examples:
|
|
444
|
+
forge authentication profiles
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
profile_names = list_profiles()
|
|
448
|
+
except CredentialVersionError as e:
|
|
449
|
+
click.secho(f"✗ {e}", fg="red")
|
|
450
|
+
raise SystemExit(1)
|
|
451
|
+
except ValueError as e:
|
|
452
|
+
click.secho(f"Error reading credentials file: {e}", fg="red")
|
|
453
|
+
click.echo("\nTip: Run 'forge auth login' to recreate the file.")
|
|
454
|
+
raise SystemExit(1)
|
|
455
|
+
|
|
456
|
+
if not profile_names:
|
|
457
|
+
click.echo("No profiles found.")
|
|
458
|
+
click.echo("\nTip: Run 'forge auth login' to create one.")
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
active = resolve_profile()
|
|
462
|
+
|
|
463
|
+
click.echo(f"\nSaved profiles ({len(profile_names)}):")
|
|
464
|
+
click.echo("-" * 30)
|
|
465
|
+
|
|
466
|
+
for name in profile_names:
|
|
467
|
+
secrets = load_profile(name)
|
|
468
|
+
key_count = len(secrets)
|
|
469
|
+
marker = " ← active" if name == active else ""
|
|
470
|
+
click.echo(f" {name} ({key_count} keys){marker}")
|
|
471
|
+
|
|
472
|
+
click.echo()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _get_auth_ignore_env() -> bool:
|
|
476
|
+
"""Read auth_ignore_env from runtime config."""
|
|
477
|
+
try:
|
|
478
|
+
from forge.runtime_config import get_runtime_config
|
|
479
|
+
|
|
480
|
+
return get_runtime_config().auth_ignore_env
|
|
481
|
+
except Exception as e:
|
|
482
|
+
_log.debug("Could not read auth_ignore_env; using environment credentials: %s", e)
|
|
483
|
+
return False
|