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.
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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())