forgefy-cli 0.1.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/LICENSE +32 -0
- forgefy_cli-0.1.0/PKG-INFO +190 -0
- forgefy_cli-0.1.0/README.md +169 -0
- forgefy_cli-0.1.0/pyproject.toml +33 -0
- forgefy_cli-0.1.0/setup.cfg +4 -0
- forgefy_cli-0.1.0/src/forgefy_cli/__init__.py +0 -0
- forgefy_cli-0.1.0/src/forgefy_cli/chat.py +49 -0
- forgefy_cli-0.1.0/src/forgefy_cli/cli.py +115 -0
- forgefy_cli-0.1.0/src/forgefy_cli/config.py +96 -0
- forgefy_cli-0.1.0/src/forgefy_cli/context.py +41 -0
- forgefy_cli-0.1.0/src/forgefy_cli/editing.py +76 -0
- forgefy_cli-0.1.0/src/forgefy_cli/file_tools.py +232 -0
- forgefy_cli-0.1.0/src/forgefy_cli/providers.py +119 -0
- forgefy_cli-0.1.0/src/forgefy_cli/skills.py +34 -0
- forgefy_cli-0.1.0/src/forgefy_cli.egg-info/PKG-INFO +190 -0
- forgefy_cli-0.1.0/src/forgefy_cli.egg-info/SOURCES.txt +23 -0
- forgefy_cli-0.1.0/src/forgefy_cli.egg-info/dependency_links.txt +1 -0
- forgefy_cli-0.1.0/src/forgefy_cli.egg-info/entry_points.txt +2 -0
- forgefy_cli-0.1.0/src/forgefy_cli.egg-info/requires.txt +4 -0
- forgefy_cli-0.1.0/src/forgefy_cli.egg-info/top_level.txt +1 -0
- forgefy_cli-0.1.0/tests/test_chat.py +104 -0
- forgefy_cli-0.1.0/tests/test_chat_process.py +117 -0
- forgefy_cli-0.1.0/tests/test_cli.py +104 -0
- forgefy_cli-0.1.0/tests/test_create.py +59 -0
- forgefy_cli-0.1.0/tests/test_edit.py +102 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Forgefy CLI License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Forgefy. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software ("Forgefy CLI") and its source code are made available so you
|
|
6
|
+
may install, run, and inspect it. This is not an open-source license: rights
|
|
7
|
+
beyond those stated below are reserved.
|
|
8
|
+
|
|
9
|
+
1. Grant of use. You may install and run Forgefy CLI, for any purpose,
|
|
10
|
+
including commercial use, subject to the terms below.
|
|
11
|
+
|
|
12
|
+
2. Restrictions. You may not, without prior written permission from Forgefy:
|
|
13
|
+
a. redistribute Forgefy CLI, in original or modified form, whether
|
|
14
|
+
standalone or bundled with other software;
|
|
15
|
+
b. use the Forgefy name, logo, or branding to promote software you
|
|
16
|
+
distribute derived from this source;
|
|
17
|
+
c. remove or alter this license notice from copies of the software.
|
|
18
|
+
|
|
19
|
+
3. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
20
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
21
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
22
|
+
|
|
23
|
+
4. Limitation of liability. IN NO EVENT SHALL FORGEFY BE LIABLE FOR ANY
|
|
24
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE SOFTWARE OR ITS USE.
|
|
25
|
+
|
|
26
|
+
5. Third-party services. Forgefy CLI can send data you provide (prompts,
|
|
27
|
+
explicitly selected files) to model providers you configure, including
|
|
28
|
+
Forgefy's own hosted API. Your use of those providers is subject to their
|
|
29
|
+
own terms.
|
|
30
|
+
|
|
31
|
+
Contact: [add a contact email/URL for licensing requests] for permissions
|
|
32
|
+
beyond this license.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forgefy-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Forgefy CLI — coding assistance with local and hosted model-provider profiles.
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
Project-URL: Homepage, https://forgefy.app
|
|
7
|
+
Project-URL: Repository, https://github.com/Polybamz/forgefy-cli
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Forgefy CLI (initial release)
|
|
23
|
+
|
|
24
|
+
A Python 3.11+ coding-assistance CLI. Language-independent prompts support writing,
|
|
25
|
+
debugging, reviewing, testing, refactoring and planning code. Quality and language
|
|
26
|
+
coverage depend on the selected model; no model is guaranteed to be best at everything.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
pip install forgefy-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Not yet published to PyPI? Install straight from GitHub instead:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
pip install git+https://github.com/Polybamz/forgefy-cli.git
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Either way this creates the `forgefy` executable on your PATH (inside whichever
|
|
41
|
+
Python environment you ran `pip install` in):
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
forgefy --help
|
|
45
|
+
forgefy config --init
|
|
46
|
+
forgefy providers
|
|
47
|
+
forgefy skills
|
|
48
|
+
forgefy doctor
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Developing locally
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
git clone https://github.com/Polybamz/forgefy-cli.git
|
|
55
|
+
cd forgefy-cli
|
|
56
|
+
pip install -e ".[dev]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Local and hosted models
|
|
60
|
+
|
|
61
|
+
With Ollama running and a model installed, list exact IDs and choose one:
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
forgefy models --provider ollama
|
|
65
|
+
forgefy run "Write a Rust function with unit tests that validates an email address" --provider ollama --model YOUR_INSTALLED_MODEL
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For OpenRouter, set `OPENROUTER_API_KEY` in your environment, list models, then select
|
|
69
|
+
an exact available ID. Free-tier models and availability are provider-controlled;
|
|
70
|
+
verify pricing before sending requests. Local inference has hardware/energy costs.
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
forgefy models --provider openrouter
|
|
74
|
+
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
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Built-in profiles: Ollama, OpenAI, OpenRouter, DeepSeek and Groq. Anthropic and Gemini
|
|
78
|
+
models can be used through OpenRouter when offered there; native Anthropic/Gemini
|
|
79
|
+
protocols are not implemented. No automatic provider or paid-model fallback occurs.
|
|
80
|
+
Compatibility requires the `/models` and `/chat/completions` endpoints; a listed model
|
|
81
|
+
is not necessarily a compatible text-generation model.
|
|
82
|
+
|
|
83
|
+
## Provider plugins
|
|
84
|
+
|
|
85
|
+
`forgefy config` prints the config path (normally your home directory's
|
|
86
|
+
`.forgefy/config.toml`). `FORGEFY_CONFIG` can select an alternate file.
|
|
87
|
+
Add declarative OpenAI-compatible provider profiles:
|
|
88
|
+
|
|
89
|
+
```toml
|
|
90
|
+
default_provider = "local_server"
|
|
91
|
+
default_model = "your-model-id"
|
|
92
|
+
|
|
93
|
+
[providers.local_server]
|
|
94
|
+
base_url = "http://localhost:1234/v1"
|
|
95
|
+
api_key_env = ""
|
|
96
|
+
|
|
97
|
+
[providers.company]
|
|
98
|
+
base_url = "https://models.example.com/v1"
|
|
99
|
+
api_key_env = "COMPANY_MODEL_KEY"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Never store keys directly in configuration. Custom profiles cannot override built-in
|
|
103
|
+
names. HTTPS is required for non-loopback endpoints. Only configure servers you trust:
|
|
104
|
+
the chosen server receives your prompt, explicit file context, and its configured key.
|
|
105
|
+
These plugins are configuration, not executable Python code or an MCP integration.
|
|
106
|
+
|
|
107
|
+
## Skills and context
|
|
108
|
+
|
|
109
|
+
Choose `--skill code|debug|review|test|refactor|plan`. Repeat `--skill-file` to include
|
|
110
|
+
trusted UTF-8 Markdown instructions. Repeat `--file` to send workspace-relative source
|
|
111
|
+
files. Use a prompt of `-` for standard input. No repository files are sent implicitly.
|
|
112
|
+
Files must resolve within the workspace. Common credential paths are excluded, but this
|
|
113
|
+
is not a secret scanner: review every file and prompt before sending. Requests have a
|
|
114
|
+
120,000-character input cap; individual models may require much smaller inputs.
|
|
115
|
+
|
|
116
|
+
## Interactive chat
|
|
117
|
+
|
|
118
|
+
```powershell
|
|
119
|
+
& 'C:\Users\USER\Desktop\polycarp\.venv\Scripts\forgefy.exe' chat --provider ollama --model llama3:latest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Chat retains conversation history in memory for follow-up questions. `/new` clears it,
|
|
123
|
+
`/help` lists commands, and `/exit` or `/quit` ends the session. EOF exits normally;
|
|
124
|
+
Ctrl+C cancels. History is not saved to disk. Each request resends retained history,
|
|
125
|
+
so hosted-provider usage can grow each turn. No automatic paid fallback occurs.
|
|
126
|
+
Oldest complete user/assistant pairs are omitted when conversational content exceeds
|
|
127
|
+
120,000 characters; system/skill instructions are additional. This is a character cap,
|
|
128
|
+
not a token budget. Failed requests preserve prior history. Chat accepts single-line
|
|
129
|
+
turns and skill plugins; explicit `--file` context is currently supported by `run` only.
|
|
130
|
+
|
|
131
|
+
## Approved file editing
|
|
132
|
+
|
|
133
|
+
`forgefy edit` can modify explicitly selected, **existing** UTF-8 files with a model
|
|
134
|
+
supporting OpenAI-compatible tool calling. `run` and `chat` remain suggestion-only.
|
|
135
|
+
In an interactive terminal, for example:
|
|
136
|
+
|
|
137
|
+
```powershell
|
|
138
|
+
& '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
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Review the selected files for secrets before starting: the model can read their contents
|
|
142
|
+
and send them to the selected provider without further read approval. Each replacement
|
|
143
|
+
shows a complete diff and requires typing `yes`. There is no automatic approval flag.
|
|
144
|
+
The executor requires a prior read, an exact single match, and unchanged contents before
|
|
145
|
+
and after approval. Updates use a sibling temporary file and atomic replacement.
|
|
146
|
+
|
|
147
|
+
Only files named by repeatable `--file` options are accessible. Hidden paths, common
|
|
148
|
+
credential files, symlinks, junctions, hardlinks and nonregular files are rejected.
|
|
149
|
+
Files and replacements are limited to 32,000 bytes; oversized diffs are rejected, not
|
|
150
|
+
truncated for approval. These checks are not a secret scanner or an OS security sandbox.
|
|
151
|
+
Use a trusted workspace without concurrent writers: a small filesystem race window
|
|
152
|
+
remains between validation and replacement. Atomic replacement preserves mode bits,
|
|
153
|
+
not necessarily all filesystem metadata or custom ACLs.
|
|
154
|
+
|
|
155
|
+
The default limit is 12 model requests (`--max-turns` accepts 1–30), with a
|
|
156
|
+
120,000-character serialized conversation cap. Exit code 2 means a limit stopped an
|
|
157
|
+
incomplete session; 1 indicates an error and 130 indicates cancellation. Exit code 0
|
|
158
|
+
means the model finished, not that its changes are correct or tested. Applied edits
|
|
159
|
+
remain on disk if the session stops or fails—there is no session-wide rollback.
|
|
160
|
+
Use version control or backups and review the printed list of files actually changed.
|
|
161
|
+
|
|
162
|
+
Editing currently supports replacements only: no file creation, deletion, shell commands,
|
|
163
|
+
or custom skill files. Its integration tests use mocked model responses and temporary
|
|
164
|
+
files; live model-driven editing has not been verified.
|
|
165
|
+
|
|
166
|
+
## Current boundaries
|
|
167
|
+
|
|
168
|
+
This release can apply approved replacements, but it does **not** execute
|
|
169
|
+
commands, run tests on generated code, stream tokens, persist chat history, or connect
|
|
170
|
+
to the Forgefy admin catalogue. Provider profiles and skill files are the initial plugin
|
|
171
|
+
interfaces, not a full autonomous coding-agent system. Output is untrusted: inspect it
|
|
172
|
+
before running anything. Tests use mocked HTTP, not live model quality benchmarks.
|
|
173
|
+
|
|
174
|
+
## Tests
|
|
175
|
+
|
|
176
|
+
```powershell
|
|
177
|
+
pytest
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Releasing (maintainers)
|
|
181
|
+
|
|
182
|
+
CI runs on every push/PR (`.github/workflows/ci.yml`). To publish a new version to PyPI:
|
|
183
|
+
|
|
184
|
+
1. Bump `version` in `pyproject.toml` and commit.
|
|
185
|
+
2. `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
|
186
|
+
3. `.github/workflows/release.yml` builds, tests, and publishes via PyPI Trusted
|
|
187
|
+
Publishing — no token stored in the repo. One-time setup: on the PyPI project's
|
|
188
|
+
*Publishing* settings, add a Trusted Publisher for `Polybamz/forgefy-cli`,
|
|
189
|
+
workflow `release.yml`, environment `pypi`.
|
|
190
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
```powershell
|
|
10
|
+
pip install forgefy-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Not yet published to PyPI? Install straight from GitHub instead:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
pip install git+https://github.com/Polybamz/forgefy-cli.git
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Either way this creates the `forgefy` executable on your PATH (inside whichever
|
|
20
|
+
Python environment you ran `pip install` in):
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
forgefy --help
|
|
24
|
+
forgefy config --init
|
|
25
|
+
forgefy providers
|
|
26
|
+
forgefy skills
|
|
27
|
+
forgefy doctor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Developing locally
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
git clone https://github.com/Polybamz/forgefy-cli.git
|
|
34
|
+
cd forgefy-cli
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Local and hosted models
|
|
39
|
+
|
|
40
|
+
With Ollama running and a model installed, list exact IDs and choose one:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
forgefy models --provider ollama
|
|
44
|
+
forgefy run "Write a Rust function with unit tests that validates an email address" --provider ollama --model YOUR_INSTALLED_MODEL
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For OpenRouter, set `OPENROUTER_API_KEY` in your environment, list models, then select
|
|
48
|
+
an exact available ID. Free-tier models and availability are provider-controlled;
|
|
49
|
+
verify pricing before sending requests. Local inference has hardware/energy costs.
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
forgefy models --provider openrouter
|
|
53
|
+
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
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Built-in profiles: Ollama, OpenAI, OpenRouter, DeepSeek and Groq. Anthropic and Gemini
|
|
57
|
+
models can be used through OpenRouter when offered there; native Anthropic/Gemini
|
|
58
|
+
protocols are not implemented. No automatic provider or paid-model fallback occurs.
|
|
59
|
+
Compatibility requires the `/models` and `/chat/completions` endpoints; a listed model
|
|
60
|
+
is not necessarily a compatible text-generation model.
|
|
61
|
+
|
|
62
|
+
## Provider plugins
|
|
63
|
+
|
|
64
|
+
`forgefy config` prints the config path (normally your home directory's
|
|
65
|
+
`.forgefy/config.toml`). `FORGEFY_CONFIG` can select an alternate file.
|
|
66
|
+
Add declarative OpenAI-compatible provider profiles:
|
|
67
|
+
|
|
68
|
+
```toml
|
|
69
|
+
default_provider = "local_server"
|
|
70
|
+
default_model = "your-model-id"
|
|
71
|
+
|
|
72
|
+
[providers.local_server]
|
|
73
|
+
base_url = "http://localhost:1234/v1"
|
|
74
|
+
api_key_env = ""
|
|
75
|
+
|
|
76
|
+
[providers.company]
|
|
77
|
+
base_url = "https://models.example.com/v1"
|
|
78
|
+
api_key_env = "COMPANY_MODEL_KEY"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Never store keys directly in configuration. Custom profiles cannot override built-in
|
|
82
|
+
names. HTTPS is required for non-loopback endpoints. Only configure servers you trust:
|
|
83
|
+
the chosen server receives your prompt, explicit file context, and its configured key.
|
|
84
|
+
These plugins are configuration, not executable Python code or an MCP integration.
|
|
85
|
+
|
|
86
|
+
## Skills and context
|
|
87
|
+
|
|
88
|
+
Choose `--skill code|debug|review|test|refactor|plan`. Repeat `--skill-file` to include
|
|
89
|
+
trusted UTF-8 Markdown instructions. Repeat `--file` to send workspace-relative source
|
|
90
|
+
files. Use a prompt of `-` for standard input. No repository files are sent implicitly.
|
|
91
|
+
Files must resolve within the workspace. Common credential paths are excluded, but this
|
|
92
|
+
is not a secret scanner: review every file and prompt before sending. Requests have a
|
|
93
|
+
120,000-character input cap; individual models may require much smaller inputs.
|
|
94
|
+
|
|
95
|
+
## Interactive chat
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
& 'C:\Users\USER\Desktop\polycarp\.venv\Scripts\forgefy.exe' chat --provider ollama --model llama3:latest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Chat retains conversation history in memory for follow-up questions. `/new` clears it,
|
|
102
|
+
`/help` lists commands, and `/exit` or `/quit` ends the session. EOF exits normally;
|
|
103
|
+
Ctrl+C cancels. History is not saved to disk. Each request resends retained history,
|
|
104
|
+
so hosted-provider usage can grow each turn. No automatic paid fallback occurs.
|
|
105
|
+
Oldest complete user/assistant pairs are omitted when conversational content exceeds
|
|
106
|
+
120,000 characters; system/skill instructions are additional. This is a character cap,
|
|
107
|
+
not a token budget. Failed requests preserve prior history. Chat accepts single-line
|
|
108
|
+
turns and skill plugins; explicit `--file` context is currently supported by `run` only.
|
|
109
|
+
|
|
110
|
+
## Approved file editing
|
|
111
|
+
|
|
112
|
+
`forgefy edit` can modify explicitly selected, **existing** UTF-8 files with a model
|
|
113
|
+
supporting OpenAI-compatible tool calling. `run` and `chat` remain suggestion-only.
|
|
114
|
+
In an interactive terminal, for example:
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
& '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
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Review the selected files for secrets before starting: the model can read their contents
|
|
121
|
+
and send them to the selected provider without further read approval. Each replacement
|
|
122
|
+
shows a complete diff and requires typing `yes`. There is no automatic approval flag.
|
|
123
|
+
The executor requires a prior read, an exact single match, and unchanged contents before
|
|
124
|
+
and after approval. Updates use a sibling temporary file and atomic replacement.
|
|
125
|
+
|
|
126
|
+
Only files named by repeatable `--file` options are accessible. Hidden paths, common
|
|
127
|
+
credential files, symlinks, junctions, hardlinks and nonregular files are rejected.
|
|
128
|
+
Files and replacements are limited to 32,000 bytes; oversized diffs are rejected, not
|
|
129
|
+
truncated for approval. These checks are not a secret scanner or an OS security sandbox.
|
|
130
|
+
Use a trusted workspace without concurrent writers: a small filesystem race window
|
|
131
|
+
remains between validation and replacement. Atomic replacement preserves mode bits,
|
|
132
|
+
not necessarily all filesystem metadata or custom ACLs.
|
|
133
|
+
|
|
134
|
+
The default limit is 12 model requests (`--max-turns` accepts 1–30), with a
|
|
135
|
+
120,000-character serialized conversation cap. Exit code 2 means a limit stopped an
|
|
136
|
+
incomplete session; 1 indicates an error and 130 indicates cancellation. Exit code 0
|
|
137
|
+
means the model finished, not that its changes are correct or tested. Applied edits
|
|
138
|
+
remain on disk if the session stops or fails—there is no session-wide rollback.
|
|
139
|
+
Use version control or backups and review the printed list of files actually changed.
|
|
140
|
+
|
|
141
|
+
Editing currently supports replacements only: no file creation, deletion, shell commands,
|
|
142
|
+
or custom skill files. Its integration tests use mocked model responses and temporary
|
|
143
|
+
files; live model-driven editing has not been verified.
|
|
144
|
+
|
|
145
|
+
## Current boundaries
|
|
146
|
+
|
|
147
|
+
This release can apply approved replacements, but it does **not** execute
|
|
148
|
+
commands, run tests on generated code, stream tokens, persist chat history, or connect
|
|
149
|
+
to the Forgefy admin catalogue. Provider profiles and skill files are the initial plugin
|
|
150
|
+
interfaces, not a full autonomous coding-agent system. Output is untrusted: inspect it
|
|
151
|
+
before running anything. Tests use mocked HTTP, not live model quality benchmarks.
|
|
152
|
+
|
|
153
|
+
## Tests
|
|
154
|
+
|
|
155
|
+
```powershell
|
|
156
|
+
pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Releasing (maintainers)
|
|
160
|
+
|
|
161
|
+
CI runs on every push/PR (`.github/workflows/ci.yml`). To publish a new version to PyPI:
|
|
162
|
+
|
|
163
|
+
1. Bump `version` in `pyproject.toml` and commit.
|
|
164
|
+
2. `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
|
165
|
+
3. `.github/workflows/release.yml` builds, tests, and publishes via PyPI Trusted
|
|
166
|
+
Publishing — no token stored in the repo. One-time setup: on the PyPI project's
|
|
167
|
+
*Publishing* settings, add a Trusted Publisher for `Polybamz/forgefy-cli`,
|
|
168
|
+
workflow `release.yml`, environment `pypi`.
|
|
169
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "forgefy-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Forgefy CLI — coding assistance with local and hosted model-provider profiles."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "LicenseRef-Proprietary"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"httpx>=0.27",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest>=8"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://forgefy.app"
|
|
26
|
+
Repository = "https://github.com/Polybamz/forgefy-cli"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
forgefy = "forgefy_cli.cli:main"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["setuptools>=77"]
|
|
33
|
+
build-backend = "setuptools.build_meta"
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Interactive multi-turn chat. Replies are printed for review; nothing is executed."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from .context import LIMIT
|
|
7
|
+
from .providers import ProviderError
|
|
8
|
+
|
|
9
|
+
LEAVING = "/exit or /quit (leave), /new (clear history), /help (commands)"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def chat_loop(client, model: str, system: str, input_fn: Callable[..., str] = input, output: Callable[[str], None] = print) -> int:
|
|
13
|
+
"""Read user turns until /exit, /quit or end of input. History is sent each turn."""
|
|
14
|
+
history: list[tuple[str, str]] = []
|
|
15
|
+
while True:
|
|
16
|
+
try:
|
|
17
|
+
line = input_fn("you> ").strip()
|
|
18
|
+
except EOFError:
|
|
19
|
+
return 0
|
|
20
|
+
if not line:
|
|
21
|
+
continue
|
|
22
|
+
if line in {"/exit", "/quit"}:
|
|
23
|
+
return 0
|
|
24
|
+
if line == "/new":
|
|
25
|
+
history.clear()
|
|
26
|
+
output("Context cleared.")
|
|
27
|
+
continue
|
|
28
|
+
if line == "/help":
|
|
29
|
+
output(f"Commands: {LEAVING}")
|
|
30
|
+
continue
|
|
31
|
+
if len(line) > LIMIT:
|
|
32
|
+
output(f"Forgefy: this turn exceeds the {LIMIT}-character context cap; use /new or a shorter prompt.")
|
|
33
|
+
continue
|
|
34
|
+
candidate = history + [("user", line)]
|
|
35
|
+
total = sum(len(content) for _, content in candidate)
|
|
36
|
+
removed = 0
|
|
37
|
+
while total > LIMIT and len(candidate) > 1:
|
|
38
|
+
total -= len(candidate[0][1]) + len(candidate[1][1])
|
|
39
|
+
del candidate[:2] # Always discard a complete user/assistant pair.
|
|
40
|
+
removed += 1
|
|
41
|
+
try:
|
|
42
|
+
reply = client.chat(model, system, candidate)
|
|
43
|
+
except (ProviderError, ValueError) as exc:
|
|
44
|
+
output(f"Forgefy: {exc}")
|
|
45
|
+
continue
|
|
46
|
+
history = candidate + [("assistant", reply)]
|
|
47
|
+
if removed:
|
|
48
|
+
output(f"Context limit: omitted {removed} oldest turn pair(s).")
|
|
49
|
+
output(reply)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Forgefy command-line interface."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .chat import chat_loop
|
|
12
|
+
from .config import TEMPLATE, config_path, load_config
|
|
13
|
+
from .context import build_prompt
|
|
14
|
+
from .editing import edit_files
|
|
15
|
+
from .providers import ModelClient, ProviderError
|
|
16
|
+
from .skills import SKILLS, system_prompt
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parser() -> argparse.ArgumentParser:
|
|
20
|
+
result = argparse.ArgumentParser(prog="forgefy", description="Forgefy: local or hosted coding assistance. Run/chat suggest; edit applies approved file changes. No command execution.")
|
|
21
|
+
result.add_argument("--version", action="version", version="Forgefy CLI 0.1.0")
|
|
22
|
+
sub = result.add_subparsers(dest="command", required=True)
|
|
23
|
+
run = sub.add_parser("run", help="Send one coding request and print the response")
|
|
24
|
+
run.add_argument("prompt", help="Coding request; use '-' to read from stdin")
|
|
25
|
+
run.add_argument("--provider", help="Provider profile name (default: config or ollama)")
|
|
26
|
+
run.add_argument("--model", help="Exact provider model ID; no automatic paid fallback")
|
|
27
|
+
run.add_argument("--workspace", type=Path, default=Path.cwd())
|
|
28
|
+
run.add_argument("--file", action="append", default=[], help="Explicit relative file to send; repeatable. Review for secrets first.")
|
|
29
|
+
run.add_argument("--skill", choices=sorted(SKILLS), default="code")
|
|
30
|
+
run.add_argument("--skill-file", type=Path, action="append", default=[], help="Trusted Markdown instructions to send; repeatable")
|
|
31
|
+
chat = sub.add_parser("chat", help="Multi-turn conversation; replies are suggestions to review")
|
|
32
|
+
chat.add_argument("--provider", help="Provider profile name (default: config or ollama)")
|
|
33
|
+
chat.add_argument("--model", help="Exact provider model ID; no automatic paid fallback")
|
|
34
|
+
chat.add_argument("--skill", choices=sorted(SKILLS), default="code")
|
|
35
|
+
chat.add_argument("--skill-file", type=Path, action="append", default=[], help="Trusted Markdown instructions to send; repeatable")
|
|
36
|
+
edit = sub.add_parser("edit", help="Edit explicitly selected existing files with approval for every diff")
|
|
37
|
+
edit.add_argument("prompt", help="Requested change")
|
|
38
|
+
edit.add_argument("--provider", help="Provider profile; requires a tool-calling model")
|
|
39
|
+
edit.add_argument("--model", help="Exact provider model ID")
|
|
40
|
+
edit.add_argument("--workspace", type=Path, default=Path.cwd())
|
|
41
|
+
edit.add_argument("--file", action="append", default=[], help="Allowed existing relative file; repeatable")
|
|
42
|
+
edit.add_argument("--create", action="append", default=[], help="Approved relative path to create; repeatable, parent dir must exist")
|
|
43
|
+
edit.add_argument("--max-turns", type=int, default=12, help="Maximum model requests (1–30; default 12)")
|
|
44
|
+
models = sub.add_parser("models", help="List live provider model IDs (availability and pricing vary)")
|
|
45
|
+
models.add_argument("--provider")
|
|
46
|
+
sub.add_parser("providers", help="List built-in and configured provider plugins")
|
|
47
|
+
sub.add_parser("skills", help="List built-in coding skills")
|
|
48
|
+
sub.add_parser("doctor", help="Check configuration and key presence without sending requests")
|
|
49
|
+
config = sub.add_parser("config", help="Display config path, or create a starter file")
|
|
50
|
+
config.add_argument("--init", action="store_true", help="Create a starter config; never overwrite")
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: list[str] | None = None) -> int:
|
|
55
|
+
args = parser().parse_args(argv)
|
|
56
|
+
try:
|
|
57
|
+
if args.command == "config":
|
|
58
|
+
path = config_path()
|
|
59
|
+
if args.init:
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
with path.open("x", encoding="utf-8") as stream:
|
|
62
|
+
stream.write(TEMPLATE)
|
|
63
|
+
print(path)
|
|
64
|
+
return 0
|
|
65
|
+
if args.command == "skills":
|
|
66
|
+
for name, description in SKILLS.items():
|
|
67
|
+
print(f"{name}: {description}")
|
|
68
|
+
return 0
|
|
69
|
+
settings, providers = load_config()
|
|
70
|
+
if args.command in {"providers", "doctor"}:
|
|
71
|
+
for name, provider in providers.items():
|
|
72
|
+
status = "no key required" if not provider.api_key_env else f"{provider.api_key_env}: {'set' if os.environ.get(provider.api_key_env) else 'missing'}"
|
|
73
|
+
print(f"{name}\t{provider.base_url}\t{status}")
|
|
74
|
+
if args.command == "doctor":
|
|
75
|
+
print(f"Config: {config_path()}")
|
|
76
|
+
print("Network/model availability not checked. Use models --provider NAME.")
|
|
77
|
+
return 0
|
|
78
|
+
name = args.provider or settings.get("default_provider", "ollama")
|
|
79
|
+
if name not in providers:
|
|
80
|
+
raise ValueError(f"Unknown provider '{name}'. Run forgefy providers.")
|
|
81
|
+
provider = providers[name]
|
|
82
|
+
coding = args.command in {"run", "chat", "edit"}
|
|
83
|
+
if coding:
|
|
84
|
+
# A default model belongs to its configured provider, not an override.
|
|
85
|
+
model = args.model or (settings.get("default_model") if name == settings.get("default_provider", "ollama") else None)
|
|
86
|
+
if not model or not model.strip():
|
|
87
|
+
raise ValueError("Choose --model ID (see forgefy models), or set default_model in config.")
|
|
88
|
+
if args.command != "edit":
|
|
89
|
+
system = system_prompt(args.skill, args.skill_file)
|
|
90
|
+
with httpx.Client(timeout=httpx.Timeout(120, connect=10)) as http:
|
|
91
|
+
client = ModelClient(provider, http)
|
|
92
|
+
if args.command == "edit":
|
|
93
|
+
return edit_files(client, model, args.prompt, args.workspace, args.file, args.max_turns, args.create)
|
|
94
|
+
if args.command == "models":
|
|
95
|
+
for model_id in client.models():
|
|
96
|
+
print(model_id)
|
|
97
|
+
elif args.command == "run":
|
|
98
|
+
prompt = sys.stdin.read(120001) if args.prompt == "-" else args.prompt
|
|
99
|
+
prompt = build_prompt(prompt, args.workspace, args.file)
|
|
100
|
+
print(f"Sending request to {name} / {model}. Provider pricing applies; no fallback.", file=sys.stderr)
|
|
101
|
+
print(client.complete(model, system, prompt))
|
|
102
|
+
else:
|
|
103
|
+
print(f"Chatting with {name} / {model}. /exit to leave; replies are suggestions to review, never executed.", file=sys.stderr)
|
|
104
|
+
chat_loop(client, model, system)
|
|
105
|
+
return 0
|
|
106
|
+
except (ValueError, OSError, ProviderError) as exc:
|
|
107
|
+
print(f"Forgefy: {exc}", file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
print("\nCancelled.", file=sys.stderr)
|
|
111
|
+
return 130
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
raise SystemExit(main())
|