forgefy-cli 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/PKG-INFO +35 -11
- forgefy_cli-0.1.0/src/forgefy_cli.egg-info/PKG-INFO → forgefy_cli-0.2.0/README.md +193 -190
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/pyproject.toml +1 -1
- forgefy_cli-0.2.0/src/forgefy_cli/auth.py +65 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/cli.py +12 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/config.py +14 -6
- forgefy_cli-0.2.0/src/forgefy_cli/login.py +101 -0
- forgefy_cli-0.1.0/README.md → forgefy_cli-0.2.0/src/forgefy_cli.egg-info/PKG-INFO +214 -169
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli.egg-info/SOURCES.txt +4 -1
- forgefy_cli-0.2.0/tests/test_login.py +135 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/LICENSE +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/setup.cfg +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/__init__.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/chat.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/context.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/editing.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/file_tools.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/providers.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli/skills.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli.egg-info/dependency_links.txt +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli.egg-info/entry_points.txt +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli.egg-info/requires.txt +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/src/forgefy_cli.egg-info/top_level.txt +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/tests/test_chat.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/tests/test_chat_process.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/tests/test_cli.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/tests/test_create.py +0 -0
- {forgefy_cli-0.1.0 → forgefy_cli-0.2.0}/tests/test_edit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgefy-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Forgefy CLI — coding assistance with local and hosted model-provider profiles.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
Project-URL: Homepage, https://forgefy.app
|
|
@@ -27,18 +27,28 @@ coverage depend on the selected model; no model is guaranteed to be best at ever
|
|
|
27
27
|
|
|
28
28
|
## Install
|
|
29
29
|
|
|
30
|
+
No Python required — installs a standalone `forgefy` binary and puts it on
|
|
31
|
+
your PATH, the same way Ollama's or Claude Code's installer does:
|
|
32
|
+
|
|
33
|
+
macOS / Linux:
|
|
34
|
+
```sh
|
|
35
|
+
curl -fsSL https://raw.githubusercontent.com/Polybamz/forgefy-cli/main/install.sh | sh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Windows (PowerShell):
|
|
30
39
|
```powershell
|
|
31
|
-
|
|
40
|
+
irm https://raw.githubusercontent.com/Polybamz/forgefy-cli/main/install.ps1 | iex
|
|
32
41
|
```
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
Already have Python? `pip`/`pipx` work too — prefer `pipx` over plain `pip`,
|
|
44
|
+
since `pip install` can silently install to a user directory that isn't on
|
|
45
|
+
your PATH:
|
|
35
46
|
|
|
36
47
|
```powershell
|
|
37
|
-
|
|
48
|
+
pipx install forgefy-cli
|
|
38
49
|
```
|
|
39
50
|
|
|
40
|
-
Either way
|
|
41
|
-
Python environment you ran `pip install` in):
|
|
51
|
+
Either way, open a new terminal and you should have the `forgefy` command:
|
|
42
52
|
|
|
43
53
|
```powershell
|
|
44
54
|
forgefy --help
|
|
@@ -179,12 +189,26 @@ pytest
|
|
|
179
189
|
|
|
180
190
|
## Releasing (maintainers)
|
|
181
191
|
|
|
182
|
-
CI runs on every push/PR (`.github/workflows/ci.yml`).
|
|
192
|
+
CI runs on every push/PR (`.github/workflows/ci.yml`). One tag produces both
|
|
193
|
+
distribution channels:
|
|
183
194
|
|
|
184
195
|
1. Bump `version` in `pyproject.toml` and commit.
|
|
185
196
|
2. `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
|
186
|
-
3. `.github/workflows/release.yml`
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
3. `.github/workflows/release.yml` then, in parallel:
|
|
198
|
+
- builds and publishes the PyPI package via Trusted Publishing — no token
|
|
199
|
+
stored in the repo. One-time setup: on the PyPI project's *Publishing*
|
|
200
|
+
settings, add a Trusted Publisher for `Polybamz/forgefy-cli`, workflow
|
|
201
|
+
`release.yml`, environment `pypi`.
|
|
202
|
+
- builds a standalone `forgefy` binary for Windows/macOS/Linux with
|
|
203
|
+
PyInstaller and attaches them to a GitHub Release for the tag — what
|
|
204
|
+
`install.sh`/`install.ps1` fetch. No setup needed; uses the repo's
|
|
205
|
+
built-in `GITHUB_TOKEN`.
|
|
206
|
+
|
|
207
|
+
To build the standalone binary locally (e.g. to test before tagging):
|
|
208
|
+
|
|
209
|
+
```powershell
|
|
210
|
+
pip install -e . pyinstaller
|
|
211
|
+
pyinstaller --onefile --name forgefy --paths src --hidden-import anyio._backends._asyncio --distpath dist_native --workpath build_native --specpath build_native build_installer/entrypoint.py
|
|
212
|
+
.\dist_native\forgefy.exe --help
|
|
213
|
+
```
|
|
190
214
|
|
|
@@ -1,190 +1,193 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
forgefy
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
forgefy
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
The
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
1
|
+
# Forgefy CLI (initial release)
|
|
2
|
+
|
|
3
|
+
A Python 3.11+ coding-assistance CLI. Language-independent prompts support writing,
|
|
4
|
+
debugging, reviewing, testing, refactoring and planning code. Quality and language
|
|
5
|
+
coverage depend on the selected model; no model is guaranteed to be best at everything.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
No Python required — installs a standalone `forgefy` binary and puts it on
|
|
10
|
+
your PATH, the same way Ollama's or Claude Code's installer does:
|
|
11
|
+
|
|
12
|
+
macOS / Linux:
|
|
13
|
+
```sh
|
|
14
|
+
curl -fsSL https://raw.githubusercontent.com/Polybamz/forgefy-cli/main/install.sh | sh
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Windows (PowerShell):
|
|
18
|
+
```powershell
|
|
19
|
+
irm https://raw.githubusercontent.com/Polybamz/forgefy-cli/main/install.ps1 | iex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Already have Python? `pip`/`pipx` work too — prefer `pipx` over plain `pip`,
|
|
23
|
+
since `pip install` can silently install to a user directory that isn't on
|
|
24
|
+
your PATH:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
pipx install forgefy-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Either way, open a new terminal and you should have the `forgefy` command:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
forgefy --help
|
|
34
|
+
forgefy config --init
|
|
35
|
+
forgefy providers
|
|
36
|
+
forgefy skills
|
|
37
|
+
forgefy doctor
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Developing locally
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
git clone https://github.com/Polybamz/forgefy-cli.git
|
|
44
|
+
cd forgefy-cli
|
|
45
|
+
pip install -e ".[dev]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Local and hosted models
|
|
49
|
+
|
|
50
|
+
With Ollama running and a model installed, list exact IDs and choose one:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
forgefy models --provider ollama
|
|
54
|
+
forgefy run "Write a Rust function with unit tests that validates an email address" --provider ollama --model YOUR_INSTALLED_MODEL
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For OpenRouter, set `OPENROUTER_API_KEY` in your environment, list models, then select
|
|
58
|
+
an exact available ID. Free-tier models and availability are provider-controlled;
|
|
59
|
+
verify pricing before sending requests. Local inference has hardware/energy costs.
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
forgefy models --provider openrouter
|
|
63
|
+
forgefy run "Explain this Python module and suggest tests" --provider openrouter --model YOUR_MODEL_ID --workspace 'C:\Users\USER\Desktop\polycarp\forgefy-cli' --file src/forgefy_cli/context.py --skill review
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Built-in profiles: Ollama, OpenAI, OpenRouter, DeepSeek and Groq. Anthropic and Gemini
|
|
67
|
+
models can be used through OpenRouter when offered there; native Anthropic/Gemini
|
|
68
|
+
protocols are not implemented. No automatic provider or paid-model fallback occurs.
|
|
69
|
+
Compatibility requires the `/models` and `/chat/completions` endpoints; a listed model
|
|
70
|
+
is not necessarily a compatible text-generation model.
|
|
71
|
+
|
|
72
|
+
## Provider plugins
|
|
73
|
+
|
|
74
|
+
`forgefy config` prints the config path (normally your home directory's
|
|
75
|
+
`.forgefy/config.toml`). `FORGEFY_CONFIG` can select an alternate file.
|
|
76
|
+
Add declarative OpenAI-compatible provider profiles:
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
default_provider = "local_server"
|
|
80
|
+
default_model = "your-model-id"
|
|
81
|
+
|
|
82
|
+
[providers.local_server]
|
|
83
|
+
base_url = "http://localhost:1234/v1"
|
|
84
|
+
api_key_env = ""
|
|
85
|
+
|
|
86
|
+
[providers.company]
|
|
87
|
+
base_url = "https://models.example.com/v1"
|
|
88
|
+
api_key_env = "COMPANY_MODEL_KEY"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Never store keys directly in configuration. Custom profiles cannot override built-in
|
|
92
|
+
names. HTTPS is required for non-loopback endpoints. Only configure servers you trust:
|
|
93
|
+
the chosen server receives your prompt, explicit file context, and its configured key.
|
|
94
|
+
These plugins are configuration, not executable Python code or an MCP integration.
|
|
95
|
+
|
|
96
|
+
## Skills and context
|
|
97
|
+
|
|
98
|
+
Choose `--skill code|debug|review|test|refactor|plan`. Repeat `--skill-file` to include
|
|
99
|
+
trusted UTF-8 Markdown instructions. Repeat `--file` to send workspace-relative source
|
|
100
|
+
files. Use a prompt of `-` for standard input. No repository files are sent implicitly.
|
|
101
|
+
Files must resolve within the workspace. Common credential paths are excluded, but this
|
|
102
|
+
is not a secret scanner: review every file and prompt before sending. Requests have a
|
|
103
|
+
120,000-character input cap; individual models may require much smaller inputs.
|
|
104
|
+
|
|
105
|
+
## Interactive chat
|
|
106
|
+
|
|
107
|
+
```powershell
|
|
108
|
+
& 'C:\Users\USER\Desktop\polycarp\.venv\Scripts\forgefy.exe' chat --provider ollama --model llama3:latest
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Chat retains conversation history in memory for follow-up questions. `/new` clears it,
|
|
112
|
+
`/help` lists commands, and `/exit` or `/quit` ends the session. EOF exits normally;
|
|
113
|
+
Ctrl+C cancels. History is not saved to disk. Each request resends retained history,
|
|
114
|
+
so hosted-provider usage can grow each turn. No automatic paid fallback occurs.
|
|
115
|
+
Oldest complete user/assistant pairs are omitted when conversational content exceeds
|
|
116
|
+
120,000 characters; system/skill instructions are additional. This is a character cap,
|
|
117
|
+
not a token budget. Failed requests preserve prior history. Chat accepts single-line
|
|
118
|
+
turns and skill plugins; explicit `--file` context is currently supported by `run` only.
|
|
119
|
+
|
|
120
|
+
## Approved file editing
|
|
121
|
+
|
|
122
|
+
`forgefy edit` can modify explicitly selected, **existing** UTF-8 files with a model
|
|
123
|
+
supporting OpenAI-compatible tool calling. `run` and `chat` remain suggestion-only.
|
|
124
|
+
In an interactive terminal, for example:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
& 'C:\Users\USER\Desktop\polycarp\.venv\Scripts\forgefy.exe' edit "Improve error handling in this module" --provider ollama --model YOUR_TOOL_CALLING_MODEL --workspace 'C:\Users\USER\Desktop\polycarp\forgefy-cli' --file src/forgefy_cli/context.py
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Review the selected files for secrets before starting: the model can read their contents
|
|
131
|
+
and send them to the selected provider without further read approval. Each replacement
|
|
132
|
+
shows a complete diff and requires typing `yes`. There is no automatic approval flag.
|
|
133
|
+
The executor requires a prior read, an exact single match, and unchanged contents before
|
|
134
|
+
and after approval. Updates use a sibling temporary file and atomic replacement.
|
|
135
|
+
|
|
136
|
+
Only files named by repeatable `--file` options are accessible. Hidden paths, common
|
|
137
|
+
credential files, symlinks, junctions, hardlinks and nonregular files are rejected.
|
|
138
|
+
Files and replacements are limited to 32,000 bytes; oversized diffs are rejected, not
|
|
139
|
+
truncated for approval. These checks are not a secret scanner or an OS security sandbox.
|
|
140
|
+
Use a trusted workspace without concurrent writers: a small filesystem race window
|
|
141
|
+
remains between validation and replacement. Atomic replacement preserves mode bits,
|
|
142
|
+
not necessarily all filesystem metadata or custom ACLs.
|
|
143
|
+
|
|
144
|
+
The default limit is 12 model requests (`--max-turns` accepts 1–30), with a
|
|
145
|
+
120,000-character serialized conversation cap. Exit code 2 means a limit stopped an
|
|
146
|
+
incomplete session; 1 indicates an error and 130 indicates cancellation. Exit code 0
|
|
147
|
+
means the model finished, not that its changes are correct or tested. Applied edits
|
|
148
|
+
remain on disk if the session stops or fails—there is no session-wide rollback.
|
|
149
|
+
Use version control or backups and review the printed list of files actually changed.
|
|
150
|
+
|
|
151
|
+
Editing currently supports replacements only: no file creation, deletion, shell commands,
|
|
152
|
+
or custom skill files. Its integration tests use mocked model responses and temporary
|
|
153
|
+
files; live model-driven editing has not been verified.
|
|
154
|
+
|
|
155
|
+
## Current boundaries
|
|
156
|
+
|
|
157
|
+
This release can apply approved replacements, but it does **not** execute
|
|
158
|
+
commands, run tests on generated code, stream tokens, persist chat history, or connect
|
|
159
|
+
to the Forgefy admin catalogue. Provider profiles and skill files are the initial plugin
|
|
160
|
+
interfaces, not a full autonomous coding-agent system. Output is untrusted: inspect it
|
|
161
|
+
before running anything. Tests use mocked HTTP, not live model quality benchmarks.
|
|
162
|
+
|
|
163
|
+
## Tests
|
|
164
|
+
|
|
165
|
+
```powershell
|
|
166
|
+
pytest
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Releasing (maintainers)
|
|
170
|
+
|
|
171
|
+
CI runs on every push/PR (`.github/workflows/ci.yml`). One tag produces both
|
|
172
|
+
distribution channels:
|
|
173
|
+
|
|
174
|
+
1. Bump `version` in `pyproject.toml` and commit.
|
|
175
|
+
2. `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
|
176
|
+
3. `.github/workflows/release.yml` then, in parallel:
|
|
177
|
+
- builds and publishes the PyPI package via Trusted Publishing — no token
|
|
178
|
+
stored in the repo. One-time setup: on the PyPI project's *Publishing*
|
|
179
|
+
settings, add a Trusted Publisher for `Polybamz/forgefy-cli`, workflow
|
|
180
|
+
`release.yml`, environment `pypi`.
|
|
181
|
+
- builds a standalone `forgefy` binary for Windows/macOS/Linux with
|
|
182
|
+
PyInstaller and attaches them to a GitHub Release for the tag — what
|
|
183
|
+
`install.sh`/`install.ps1` fetch. No setup needed; uses the repo's
|
|
184
|
+
built-in `GITHUB_TOKEN`.
|
|
185
|
+
|
|
186
|
+
To build the standalone binary locally (e.g. to test before tagging):
|
|
187
|
+
|
|
188
|
+
```powershell
|
|
189
|
+
pip install -e . pyinstaller
|
|
190
|
+
pyinstaller --onefile --name forgefy --paths src --hidden-import anyio._backends._asyncio --distpath dist_native --workpath build_native --specpath build_native build_installer/entrypoint.py
|
|
191
|
+
.\dist_native\forgefy.exe --help
|
|
192
|
+
```
|
|
193
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Local storage for the credential `forgefy login` obtains.
|
|
2
|
+
|
|
3
|
+
config.py is explicit that provider credentials are read only from the
|
|
4
|
+
environment and never stored in config.toml. This module doesn't relax that:
|
|
5
|
+
Provider.headers() still reads only os.environ. `forgefy login` just gives
|
|
6
|
+
one more, opt-in way to populate $FORGEFY_API_KEY for the process — from a
|
|
7
|
+
single-purpose file the user created by explicitly running `login`, kept out
|
|
8
|
+
of config.toml and permissioned as tightly as the OS allows, the same
|
|
9
|
+
convention `gh`/`docker`/`npm` use for their own login credential.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import stat
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def credentials_path() -> Path:
|
|
20
|
+
return Path(
|
|
21
|
+
os.environ.get("FORGEFY_CREDENTIALS", str(Path.home() / ".forgefy" / "credentials.json"))
|
|
22
|
+
).expanduser()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def save_credentials(api_key: str) -> Path:
|
|
26
|
+
path = credentials_path()
|
|
27
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
path.write_text(json.dumps({"api_key": api_key}), encoding="utf-8")
|
|
29
|
+
try:
|
|
30
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) # 0600 — no-op on Windows, effective on POSIX
|
|
31
|
+
except OSError:
|
|
32
|
+
pass # best-effort; some filesystems (e.g. certain network mounts) reject chmod
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_credentials() -> str | None:
|
|
37
|
+
path = credentials_path()
|
|
38
|
+
if not path.exists():
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
42
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
43
|
+
return None
|
|
44
|
+
key = data.get("api_key") if isinstance(data, dict) else None
|
|
45
|
+
return key if isinstance(key, str) and key else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def clear_credentials() -> bool:
|
|
49
|
+
path = credentials_path()
|
|
50
|
+
if path.exists():
|
|
51
|
+
path.unlink()
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def bootstrap_env() -> None:
|
|
57
|
+
"""Populate $FORGEFY_API_KEY from the stored credential if not already set.
|
|
58
|
+
|
|
59
|
+
Called once at CLI startup, before providers are resolved. An explicitly
|
|
60
|
+
exported FORGEFY_API_KEY always wins over a stored one.
|
|
61
|
+
"""
|
|
62
|
+
if not os.environ.get("FORGEFY_API_KEY"):
|
|
63
|
+
key = load_credentials()
|
|
64
|
+
if key:
|
|
65
|
+
os.environ["FORGEFY_API_KEY"] = key
|
|
@@ -8,6 +8,7 @@ import sys
|
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
|
|
11
|
+
from .auth import bootstrap_env
|
|
11
12
|
from .chat import chat_loop
|
|
12
13
|
from .config import TEMPLATE, config_path, load_config
|
|
13
14
|
from .context import build_prompt
|
|
@@ -43,6 +44,9 @@ def parser() -> argparse.ArgumentParser:
|
|
|
43
44
|
edit.add_argument("--max-turns", type=int, default=12, help="Maximum model requests (1–30; default 12)")
|
|
44
45
|
models = sub.add_parser("models", help="List live provider model IDs (availability and pricing vary)")
|
|
45
46
|
models.add_argument("--provider")
|
|
47
|
+
login = sub.add_parser("login", help="Sign in with your Forgefy account via the browser (device code)")
|
|
48
|
+
login.add_argument("--api-url", help="Override FORGEFY_API_URL for this login only")
|
|
49
|
+
sub.add_parser("logout", help="Remove the locally stored Forgefy account credential")
|
|
46
50
|
sub.add_parser("providers", help="List built-in and configured provider plugins")
|
|
47
51
|
sub.add_parser("skills", help="List built-in coding skills")
|
|
48
52
|
sub.add_parser("doctor", help="Check configuration and key presence without sending requests")
|
|
@@ -53,7 +57,15 @@ def parser() -> argparse.ArgumentParser:
|
|
|
53
57
|
|
|
54
58
|
def main(argv: list[str] | None = None) -> int:
|
|
55
59
|
args = parser().parse_args(argv)
|
|
60
|
+
bootstrap_env()
|
|
56
61
|
try:
|
|
62
|
+
if args.command == "login":
|
|
63
|
+
from .login import device_login
|
|
64
|
+
with httpx.Client(timeout=15) as http:
|
|
65
|
+
return device_login(http, api_url=args.api_url)
|
|
66
|
+
if args.command == "logout":
|
|
67
|
+
from .login import logout
|
|
68
|
+
return logout()
|
|
57
69
|
if args.command == "config":
|
|
58
70
|
path = config_path()
|
|
59
71
|
if args.init:
|
|
@@ -24,10 +24,16 @@ class Provider:
|
|
|
24
24
|
return {"Authorization": f"Bearer {key}"}
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
def forgefy_api_url() -> str:
|
|
28
|
+
"""Return the Forgefy API origin — same $FORGEFY_API_URL convention as the
|
|
29
|
+
official SDKs (sdks/python, sdks/typescript) and the self-hosted docs, since
|
|
30
|
+
the API has no single fixed public host.
|
|
31
|
+
|
|
32
|
+
Read fresh (not cached at import time) so `forgefy login` picks up a
|
|
33
|
+
`--api-url` override passed after the process has already started.
|
|
34
|
+
"""
|
|
35
|
+
return os.environ.get("FORGEFY_API_URL", "http://localhost:5000").rstrip("/")
|
|
36
|
+
|
|
31
37
|
|
|
32
38
|
BUILTINS = {
|
|
33
39
|
"ollama": Provider("ollama", "http://localhost:11434/v1"),
|
|
@@ -39,8 +45,10 @@ BUILTINS = {
|
|
|
39
45
|
# Developers page — the same fgy_live_… keys and monthly token budget the
|
|
40
46
|
# extract API and web builds use — and `export FORGEFY_API_KEY=fgy_live_…`.
|
|
41
47
|
# Set FORGEFY_API_URL too if you're not on the default host (self-hosted,
|
|
42
|
-
# local dev).
|
|
43
|
-
|
|
48
|
+
# local dev). Run `forgefy login` to fill FORGEFY_API_KEY automatically
|
|
49
|
+
# instead of copying a key from the Developers page by hand. See
|
|
50
|
+
# app/api/v1/cli.py in forgefy-backend.
|
|
51
|
+
"forgefy": Provider("forgefy", f"{forgefy_api_url()}/api/v1/cli", "FORGEFY_API_KEY"),
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
TEMPLATE = '''# Keep API keys in environment variables, never here.
|