forgecodecli 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.
- forgecodecli-0.1.0/PKG-INFO +127 -0
- forgecodecli-0.1.0/README.md +111 -0
- forgecodecli-0.1.0/forgecodecli/__init__.py +0 -0
- forgecodecli-0.1.0/forgecodecli/agent.py +144 -0
- forgecodecli-0.1.0/forgecodecli/cli.py +210 -0
- forgecodecli-0.1.0/forgecodecli/config.py +27 -0
- forgecodecli-0.1.0/forgecodecli/secrets.py +13 -0
- forgecodecli-0.1.0/forgecodecli/tools.py +74 -0
- forgecodecli-0.1.0/forgecodecli.egg-info/PKG-INFO +127 -0
- forgecodecli-0.1.0/forgecodecli.egg-info/SOURCES.txt +14 -0
- forgecodecli-0.1.0/forgecodecli.egg-info/dependency_links.txt +1 -0
- forgecodecli-0.1.0/forgecodecli.egg-info/entry_points.txt +2 -0
- forgecodecli-0.1.0/forgecodecli.egg-info/requires.txt +8 -0
- forgecodecli-0.1.0/forgecodecli.egg-info/top_level.txt +1 -0
- forgecodecli-0.1.0/pyproject.toml +27 -0
- forgecodecli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forgecodecli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal agentic CLI for forging code
|
|
5
|
+
Author: Sudhanshu
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer[all]>=0.9.0
|
|
10
|
+
Requires-Dist: openai>=1.0.0
|
|
11
|
+
Requires-Dist: keyring>=24.0.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# ForgeCodeCLI
|
|
18
|
+
|
|
19
|
+
An agentic, file-aware command-line tool that lets you manage and modify your codebase using natural language — powered by LLMs.
|
|
20
|
+
|
|
21
|
+
It acts as a safe, deterministic AI agent that can read files, create directories, and write code only through explicit tools, not raw hallucination.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Agentic workflow (LLM decides actions, CLI executes them)
|
|
26
|
+
- File-aware (read, list, create, write files & directories)
|
|
27
|
+
- Secure API key storage (no env vars required after setup)
|
|
28
|
+
- Deterministic and rule-based execution
|
|
29
|
+
- Interactive CLI experience
|
|
30
|
+
- Built to support multiple LLM providers (Gemini first)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Requires Python 3.9+
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install forgecodecli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### Initialize (one-time setup)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
forgecodecli init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You will be prompted to:
|
|
49
|
+
- Select an LLM provider
|
|
50
|
+
- Enter your API key (stored securely)
|
|
51
|
+
|
|
52
|
+
### Start the agent
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
forgecodecli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You are now in interactive agent mode. Example commands:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
create a folder src/app and add a main.py file that prints hello
|
|
62
|
+
read the README.md file
|
|
63
|
+
list all files in the src directory
|
|
64
|
+
quit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or press `Ctrl + C` to exit.
|
|
68
|
+
|
|
69
|
+
## Reset Configuration
|
|
70
|
+
|
|
71
|
+
To remove all configuration and API keys:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
forgecodecli reset
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Security
|
|
78
|
+
|
|
79
|
+
- API keys are stored using the system keyring
|
|
80
|
+
- No API keys are written to config files or environment variables
|
|
81
|
+
- Config files contain only non-sensitive metadata
|
|
82
|
+
|
|
83
|
+
## How It Works
|
|
84
|
+
|
|
85
|
+
1. You enter a natural language command
|
|
86
|
+
2. The LLM decides the next valid action
|
|
87
|
+
3. ForgeCodeCLI executes the action safely
|
|
88
|
+
4. The agent responds with the result
|
|
89
|
+
|
|
90
|
+
The agent is strictly limited to predefined tools, ensuring predictable and safe behavior.
|
|
91
|
+
|
|
92
|
+
## Supported Actions
|
|
93
|
+
|
|
94
|
+
- `read_file`
|
|
95
|
+
- `list_files`
|
|
96
|
+
- `create_dir`
|
|
97
|
+
- `write_file`
|
|
98
|
+
|
|
99
|
+
No action outside these tools is permitted.
|
|
100
|
+
|
|
101
|
+
## Status
|
|
102
|
+
|
|
103
|
+
This project is in active development.
|
|
104
|
+
|
|
105
|
+
**Current version supports:**
|
|
106
|
+
- Gemini LLM
|
|
107
|
+
- Interactive agent mode
|
|
108
|
+
|
|
109
|
+
**Planned features:**
|
|
110
|
+
- Multiple LLM providers
|
|
111
|
+
- Model switching
|
|
112
|
+
- Streaming responses
|
|
113
|
+
- Session memory
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT License
|
|
118
|
+
|
|
119
|
+
## Author
|
|
120
|
+
|
|
121
|
+
Built by Sudhanshu
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# ForgeCodeCLI
|
|
2
|
+
|
|
3
|
+
An agentic, file-aware command-line tool that lets you manage and modify your codebase using natural language — powered by LLMs.
|
|
4
|
+
|
|
5
|
+
It acts as a safe, deterministic AI agent that can read files, create directories, and write code only through explicit tools, not raw hallucination.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Agentic workflow (LLM decides actions, CLI executes them)
|
|
10
|
+
- File-aware (read, list, create, write files & directories)
|
|
11
|
+
- Secure API key storage (no env vars required after setup)
|
|
12
|
+
- Deterministic and rule-based execution
|
|
13
|
+
- Interactive CLI experience
|
|
14
|
+
- Built to support multiple LLM providers (Gemini first)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Requires Python 3.9+
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install forgecodecli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Initialize (one-time setup)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
forgecodecli init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You will be prompted to:
|
|
33
|
+
- Select an LLM provider
|
|
34
|
+
- Enter your API key (stored securely)
|
|
35
|
+
|
|
36
|
+
### Start the agent
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
forgecodecli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
You are now in interactive agent mode. Example commands:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
create a folder src/app and add a main.py file that prints hello
|
|
46
|
+
read the README.md file
|
|
47
|
+
list all files in the src directory
|
|
48
|
+
quit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or press `Ctrl + C` to exit.
|
|
52
|
+
|
|
53
|
+
## Reset Configuration
|
|
54
|
+
|
|
55
|
+
To remove all configuration and API keys:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
forgecodecli reset
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Security
|
|
62
|
+
|
|
63
|
+
- API keys are stored using the system keyring
|
|
64
|
+
- No API keys are written to config files or environment variables
|
|
65
|
+
- Config files contain only non-sensitive metadata
|
|
66
|
+
|
|
67
|
+
## How It Works
|
|
68
|
+
|
|
69
|
+
1. You enter a natural language command
|
|
70
|
+
2. The LLM decides the next valid action
|
|
71
|
+
3. ForgeCodeCLI executes the action safely
|
|
72
|
+
4. The agent responds with the result
|
|
73
|
+
|
|
74
|
+
The agent is strictly limited to predefined tools, ensuring predictable and safe behavior.
|
|
75
|
+
|
|
76
|
+
## Supported Actions
|
|
77
|
+
|
|
78
|
+
- `read_file`
|
|
79
|
+
- `list_files`
|
|
80
|
+
- `create_dir`
|
|
81
|
+
- `write_file`
|
|
82
|
+
|
|
83
|
+
No action outside these tools is permitted.
|
|
84
|
+
|
|
85
|
+
## Status
|
|
86
|
+
|
|
87
|
+
This project is in active development.
|
|
88
|
+
|
|
89
|
+
**Current version supports:**
|
|
90
|
+
- Gemini LLM
|
|
91
|
+
- Interactive agent mode
|
|
92
|
+
|
|
93
|
+
**Planned features:**
|
|
94
|
+
- Multiple LLM providers
|
|
95
|
+
- Model switching
|
|
96
|
+
- Streaming responses
|
|
97
|
+
- Session memory
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT License
|
|
102
|
+
|
|
103
|
+
## Author
|
|
104
|
+
|
|
105
|
+
Built by Sudhanshu
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
File without changes
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# from dotenv import load_dotenv
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
# import os
|
|
4
|
+
import json
|
|
5
|
+
from json import JSONDecoder
|
|
6
|
+
from openai import RateLimitError
|
|
7
|
+
from forgecodecli.config import load_config
|
|
8
|
+
from forgecodecli.secrets import load_api_key
|
|
9
|
+
|
|
10
|
+
# Load env
|
|
11
|
+
# load_dotenv()
|
|
12
|
+
|
|
13
|
+
def get_client():
|
|
14
|
+
config = load_config()
|
|
15
|
+
api_key = load_api_key()
|
|
16
|
+
|
|
17
|
+
if not config:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
"ForgeCodeCLI is not set up. Run `forgecodecli init`."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if not api_key:
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
"API key not found. Run `forgecodecli init` again."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
provider = config.get("provider")
|
|
28
|
+
|
|
29
|
+
if provider == "gemini":
|
|
30
|
+
return OpenAI(
|
|
31
|
+
api_key=api_key,
|
|
32
|
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
raise RuntimeError(f"Unsupported provider: {provider}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# client = OpenAI(
|
|
39
|
+
# api_key=os.getenv("GEMINI_API_KEY"),
|
|
40
|
+
# base_url="https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
41
|
+
# )
|
|
42
|
+
|
|
43
|
+
SYSTEM_PROMPT = """
|
|
44
|
+
You are an agent that decides what action to take.
|
|
45
|
+
|
|
46
|
+
CRITICAL RULES:
|
|
47
|
+
1. Each user request requires AT MOST TWO action that makes a change (write_file, create_dir, read_file, or list_files)
|
|
48
|
+
2. After you have completed all required actions
|
|
49
|
+
(up to the allowed limit), respond with "answer"
|
|
50
|
+
3. Do NOT take multiple write_file or create_dir actions
|
|
51
|
+
4. Do NOT repeat actions
|
|
52
|
+
|
|
53
|
+
Actions available:
|
|
54
|
+
- "read_file": read a file
|
|
55
|
+
- "list_files": list directory contents
|
|
56
|
+
- "write_file": write/create a file
|
|
57
|
+
- "create_dir": create a directory
|
|
58
|
+
- "answer": respond to the user (use this after task is complete)
|
|
59
|
+
|
|
60
|
+
RESPONSE FORMAT - You MUST respond ONLY with valid JSON, nothing else:
|
|
61
|
+
{
|
|
62
|
+
"action": "answer",
|
|
63
|
+
"args": {
|
|
64
|
+
"text": "Your message here"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
- User: "create file.py with print('hello')" → write_file → ✅ appears → immediately return answer
|
|
70
|
+
- User: "read file.py" → read_file → content appears → immediately return answer
|
|
71
|
+
- User: "what's in src?" → list_files → files appear → immediately return answer
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def think(messages: list[dict]) -> dict:
|
|
77
|
+
config = load_config()
|
|
78
|
+
model = config.get("model", "gemini-2.5-flash")
|
|
79
|
+
try:
|
|
80
|
+
client = get_client()
|
|
81
|
+
response = client.chat.completions.create(
|
|
82
|
+
model=model,
|
|
83
|
+
messages=[{"role": "system", "content": SYSTEM_PROMPT}] + messages
|
|
84
|
+
)
|
|
85
|
+
except RateLimitError as e:
|
|
86
|
+
return {
|
|
87
|
+
"action": "answer",
|
|
88
|
+
"args": {
|
|
89
|
+
"text": "⚠️ Rate limit hit. Please wait a few seconds and try again."
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return {
|
|
94
|
+
"action": "answer",
|
|
95
|
+
"args": {
|
|
96
|
+
"text": f"❌ LLM error: {str(e)}"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
content = response.choices[0].message.content
|
|
102
|
+
|
|
103
|
+
# Handle empty response
|
|
104
|
+
if content is None or not content.strip():
|
|
105
|
+
return {
|
|
106
|
+
"action": "answer",
|
|
107
|
+
"args": {
|
|
108
|
+
"text": "Task completed successfully!"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cleaned = content.strip()
|
|
113
|
+
|
|
114
|
+
# Robust JSON extraction
|
|
115
|
+
decoder = JSONDecoder()
|
|
116
|
+
|
|
117
|
+
# Try to find JSON object in the content
|
|
118
|
+
idx = cleaned.find("{")
|
|
119
|
+
if idx == -1:
|
|
120
|
+
return {
|
|
121
|
+
"action": "answer",
|
|
122
|
+
"args": {
|
|
123
|
+
"text": cleaned
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Extract from first { onwards
|
|
128
|
+
cleaned = cleaned[idx:]
|
|
129
|
+
|
|
130
|
+
# Try to decode JSON, handling partial/malformed content
|
|
131
|
+
try:
|
|
132
|
+
obj, _ = decoder.raw_decode(cleaned)
|
|
133
|
+
return obj
|
|
134
|
+
except json.JSONDecodeError:
|
|
135
|
+
# If it fails, try to find the end of a valid JSON object
|
|
136
|
+
# by trying progressively shorter strings from the end
|
|
137
|
+
for end_pos in range(len(cleaned), idx, -1):
|
|
138
|
+
try:
|
|
139
|
+
obj, _ = decoder.raw_decode(cleaned[:end_pos])
|
|
140
|
+
return obj
|
|
141
|
+
except json.JSONDecodeError:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
raise ValueError(f"Could not parse JSON from LLM output: {cleaned[:100]}...")
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from forgecodecli.agent import think
|
|
3
|
+
from forgecodecli.tools import read_file, list_files, write_file, create_dir
|
|
4
|
+
import getpass
|
|
5
|
+
|
|
6
|
+
from forgecodecli.secrets import save_api_key, delete_api_key
|
|
7
|
+
from forgecodecli.config import save_config, config_exists, delete_config
|
|
8
|
+
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
def show_logo():
|
|
14
|
+
cwd = os.getcwd()
|
|
15
|
+
|
|
16
|
+
print(f"""
|
|
17
|
+
███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
18
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
|
|
19
|
+
█████╗ ██║ ██║██████╔╝██║ ███╗█████╗
|
|
20
|
+
██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
|
|
21
|
+
██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
|
|
22
|
+
╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
|
23
|
+
|
|
24
|
+
ForgeCode CLI • Agentic File Assistant
|
|
25
|
+
Safe • Deterministic • File-aware
|
|
26
|
+
|
|
27
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
28
|
+
Agent Mode : Code Agent
|
|
29
|
+
Model : Gemini 2.5 Flash
|
|
30
|
+
Workspace : {cwd}
|
|
31
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
32
|
+
|
|
33
|
+
Type natural language commands to manage files.
|
|
34
|
+
(type 'quit' or Ctrl+C to exit)\n
|
|
35
|
+
""")
|
|
36
|
+
|
|
37
|
+
@app.command()
|
|
38
|
+
def init() :
|
|
39
|
+
"""
|
|
40
|
+
Initialize ForgeCode CLI configuration
|
|
41
|
+
"""
|
|
42
|
+
if config_exists():
|
|
43
|
+
typer.echo("ForgeCodeCLI is already set up.")
|
|
44
|
+
typer.echo("Use `forgecodecli reset` to reconfigure.")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
typer.echo("Welcome to ForgeCodeCLI ✨")
|
|
48
|
+
typer.echo("Let's set things up.\n")
|
|
49
|
+
|
|
50
|
+
typer.echo("Select LLM provider:")
|
|
51
|
+
typer.echo(" 1) Gemini")
|
|
52
|
+
typer.echo(" 2) Exit")
|
|
53
|
+
|
|
54
|
+
choice = typer.prompt(">")
|
|
55
|
+
if choice != "1":
|
|
56
|
+
typer.echo("Exiting setup.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
api_key = getpass.getpass("Enter your Gemini API Key: ")
|
|
60
|
+
if not api_key.strip():
|
|
61
|
+
typer.echo("API Key cannot be empty. Exiting setup.")
|
|
62
|
+
return
|
|
63
|
+
save_api_key(api_key)
|
|
64
|
+
|
|
65
|
+
config = {
|
|
66
|
+
"provider": "gemini",
|
|
67
|
+
"model": "gemini-2.5-flash"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
save_config(config)
|
|
71
|
+
|
|
72
|
+
typer.echo("\n✓ API key saved securely")
|
|
73
|
+
typer.echo("✓ Provider set to Gemini")
|
|
74
|
+
typer.echo("✓ Model set to gemini-2.5-flash")
|
|
75
|
+
typer.echo("\nSetup complete.")
|
|
76
|
+
typer.echo("Run `forgecodecli` to start.")
|
|
77
|
+
|
|
78
|
+
@app.command()
|
|
79
|
+
def reset():
|
|
80
|
+
"""
|
|
81
|
+
Reset ForgeCodeCLI configuration and API key
|
|
82
|
+
"""
|
|
83
|
+
if not config_exists():
|
|
84
|
+
typer.echo("ForgeCodeCLI is not set up.")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
typer.echo(
|
|
88
|
+
"This will remove your ForgeCodeCLI configuration and API key."
|
|
89
|
+
)
|
|
90
|
+
confirm = typer.prompt("Are you sure? (y/N)", default="n")
|
|
91
|
+
|
|
92
|
+
if confirm.lower() != "y":
|
|
93
|
+
typer.echo("Reset cancelled.")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
delete_api_key()
|
|
98
|
+
typer.echo("✓ API key removed")
|
|
99
|
+
except Exception:
|
|
100
|
+
typer.echo("⚠️ No API key found")
|
|
101
|
+
|
|
102
|
+
delete_config()
|
|
103
|
+
typer.echo("✓ Configuration deleted")
|
|
104
|
+
|
|
105
|
+
typer.echo("\nForgeCodeCLI has been reset.")
|
|
106
|
+
typer.echo("Run `forgecodecli init` to set it up again.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def describe_action(action: str, args: dict):
|
|
110
|
+
if action == "read_file":
|
|
111
|
+
print(f"📂 Reading file: {args.get('path')}")
|
|
112
|
+
elif action == "list_files":
|
|
113
|
+
print(f"📄 Listing files in: {args.get('path', '.')}")
|
|
114
|
+
elif action == "create_dir":
|
|
115
|
+
print(f"📁 Creating directory: {args.get('path')}")
|
|
116
|
+
elif action == "write_file":
|
|
117
|
+
print(f"✍️ Writing file: {args.get('path')}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.callback(invoke_without_command=True)
|
|
121
|
+
def run(ctx: typer.Context):
|
|
122
|
+
"""
|
|
123
|
+
ForgeCode CLI — agent with actions
|
|
124
|
+
"""
|
|
125
|
+
if ctx.invoked_subcommand is not None:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if not config_exists():
|
|
129
|
+
typer.echo("ForgeCodeCLI is not set up yet.")
|
|
130
|
+
typer.echo("Run `forgecodecli init` first.")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# ===============================
|
|
134
|
+
# INTERACTIVE MODE
|
|
135
|
+
# ===============================
|
|
136
|
+
show_logo()
|
|
137
|
+
messages = []
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
while True:
|
|
141
|
+
user_input = input("forgecode (agent) > ").strip()
|
|
142
|
+
|
|
143
|
+
if user_input.lower() in ("quit", "exit"):
|
|
144
|
+
print("Bye")
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
messages.append({"role": "user", "content": user_input})
|
|
148
|
+
# print("🤔 Planning actions...")
|
|
149
|
+
answered = False
|
|
150
|
+
|
|
151
|
+
for _ in range(5):
|
|
152
|
+
decision = think(messages)
|
|
153
|
+
action = decision.get("action")
|
|
154
|
+
args = decision.get("args", {})
|
|
155
|
+
|
|
156
|
+
if action == "read_file":
|
|
157
|
+
describe_action(action, args)
|
|
158
|
+
result = read_file(args.get("path"))
|
|
159
|
+
print(result)
|
|
160
|
+
messages.append({"role": "assistant", "content": result})
|
|
161
|
+
|
|
162
|
+
elif action == "list_files":
|
|
163
|
+
describe_action(action, args)
|
|
164
|
+
result = list_files(args.get("path", "."))
|
|
165
|
+
print(result)
|
|
166
|
+
messages.append({"role": "assistant", "content": result})
|
|
167
|
+
|
|
168
|
+
elif action == "create_dir":
|
|
169
|
+
describe_action(action, args)
|
|
170
|
+
result = create_dir(args.get("path"))
|
|
171
|
+
print(result)
|
|
172
|
+
messages.append({"role": "assistant", "content": result})
|
|
173
|
+
|
|
174
|
+
elif action == "write_file":
|
|
175
|
+
describe_action(action, args)
|
|
176
|
+
result = write_file(
|
|
177
|
+
args.get("path"),
|
|
178
|
+
args.get("content")
|
|
179
|
+
)
|
|
180
|
+
print(result)
|
|
181
|
+
messages.append({"role": "assistant", "content": result})
|
|
182
|
+
|
|
183
|
+
elif action == "answer":
|
|
184
|
+
print(args.get("text", ""))
|
|
185
|
+
answered = True
|
|
186
|
+
# Keep only last 10 messages to avoid context overflow
|
|
187
|
+
if len(messages) > 20:
|
|
188
|
+
messages = messages[-20:]
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if not answered:
|
|
192
|
+
print("⚠️ I couldn't complete this request.")
|
|
193
|
+
print("✅ Done")
|
|
194
|
+
# Keep only last 10 messages to avoid context overflow
|
|
195
|
+
if len(messages) > 20:
|
|
196
|
+
messages = messages[-20:]
|
|
197
|
+
|
|
198
|
+
except KeyboardInterrupt:
|
|
199
|
+
print("\nBye")
|
|
200
|
+
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main():
|
|
206
|
+
app()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
CONFIG_DIR = Path.home() / ".forgecodecli"
|
|
6
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
7
|
+
|
|
8
|
+
def config_exists()-> bool:
|
|
9
|
+
return CONFIG_FILE.exists()
|
|
10
|
+
|
|
11
|
+
def load_config()-> dict:
|
|
12
|
+
if not config_exists():
|
|
13
|
+
return {}
|
|
14
|
+
|
|
15
|
+
with open(CONFIG_FILE, "r") as f:
|
|
16
|
+
return json.load(f)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_config(config: dict):
|
|
20
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
21
|
+
with open(CONFIG_FILE, "w") as f:
|
|
22
|
+
json.dump(config, f, indent=2)
|
|
23
|
+
|
|
24
|
+
def delete_config():
|
|
25
|
+
if config_exists():
|
|
26
|
+
CONFIG_FILE.unlink()
|
|
27
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import keyring
|
|
2
|
+
|
|
3
|
+
SERVICE_NAME = "forgecodecli"
|
|
4
|
+
|
|
5
|
+
def save_api_key(api_key: str):
|
|
6
|
+
keyring.set_password(SERVICE_NAME, "api_key", api_key)
|
|
7
|
+
|
|
8
|
+
def load_api_key() -> str | None:
|
|
9
|
+
return keyring.get_password(SERVICE_NAME, "api_key")
|
|
10
|
+
|
|
11
|
+
def delete_api_key():
|
|
12
|
+
keyring.delete_password(SERVICE_NAME, "api_key")
|
|
13
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
def is_safe_path(path: str) -> bool:
|
|
5
|
+
if not path:
|
|
6
|
+
return False
|
|
7
|
+
if os.path.isabs(path):
|
|
8
|
+
return False
|
|
9
|
+
if ".." in path:
|
|
10
|
+
return False
|
|
11
|
+
return True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_file(path: str) -> str:
|
|
15
|
+
"""Read a file and return its contents"""
|
|
16
|
+
file_path = Path(path)
|
|
17
|
+
|
|
18
|
+
if not file_path.exists():
|
|
19
|
+
return f"Error: File '{path}' does not exist."
|
|
20
|
+
|
|
21
|
+
return file_path.read_text()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def list_files(path:str = ".")-> str:
|
|
25
|
+
if not is_safe_path(path):
|
|
26
|
+
return "Error: Unsafe path detected."
|
|
27
|
+
|
|
28
|
+
full_path = os.path.join(os.getcwd(), path)
|
|
29
|
+
|
|
30
|
+
if not os.path.exists(full_path):
|
|
31
|
+
return "❌ Path does not exist."
|
|
32
|
+
|
|
33
|
+
if not os.path.isdir(full_path):
|
|
34
|
+
return "❌ Path is not a directory."
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
items = os.listdir(full_path)
|
|
38
|
+
if not items:
|
|
39
|
+
return "📂 Directory is empty."
|
|
40
|
+
|
|
41
|
+
return "\n".join(sorted(items))
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return f"❌ Error reading directory: {str(e)}"
|
|
44
|
+
|
|
45
|
+
def write_file(path: str, content: str) -> str:
|
|
46
|
+
if not is_safe_path(path):
|
|
47
|
+
return "❌ Invalid path."
|
|
48
|
+
|
|
49
|
+
full_path = os.path.join(os.getcwd(), path)
|
|
50
|
+
|
|
51
|
+
# Prevent overwriting existing files (for now)
|
|
52
|
+
if os.path.exists(full_path):
|
|
53
|
+
return "❌ File already exists. Overwrite not allowed."
|
|
54
|
+
|
|
55
|
+
parent = os.path.dirname(full_path)
|
|
56
|
+
if parent and not os.path.exists(parent):
|
|
57
|
+
return "❌ Parent directory does not exist."
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
61
|
+
f.write(content)
|
|
62
|
+
return f"✅ File written: {path}"
|
|
63
|
+
except Exception:
|
|
64
|
+
return "❌ Failed to write file."
|
|
65
|
+
|
|
66
|
+
def create_dir(path: str)-> str:
|
|
67
|
+
if not is_safe_path(path):
|
|
68
|
+
return "❌ Invalid path."
|
|
69
|
+
full_path = os.path.join(os.getcwd(), path)
|
|
70
|
+
try:
|
|
71
|
+
os.makedirs(full_path, exist_ok=True)
|
|
72
|
+
return f"✅ Directory created: {path}"
|
|
73
|
+
except Exception:
|
|
74
|
+
return "❌ Failed to create directory."
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forgecodecli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal agentic CLI for forging code
|
|
5
|
+
Author: Sudhanshu
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer[all]>=0.9.0
|
|
10
|
+
Requires-Dist: openai>=1.0.0
|
|
11
|
+
Requires-Dist: keyring>=24.0.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# ForgeCodeCLI
|
|
18
|
+
|
|
19
|
+
An agentic, file-aware command-line tool that lets you manage and modify your codebase using natural language — powered by LLMs.
|
|
20
|
+
|
|
21
|
+
It acts as a safe, deterministic AI agent that can read files, create directories, and write code only through explicit tools, not raw hallucination.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Agentic workflow (LLM decides actions, CLI executes them)
|
|
26
|
+
- File-aware (read, list, create, write files & directories)
|
|
27
|
+
- Secure API key storage (no env vars required after setup)
|
|
28
|
+
- Deterministic and rule-based execution
|
|
29
|
+
- Interactive CLI experience
|
|
30
|
+
- Built to support multiple LLM providers (Gemini first)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Requires Python 3.9+
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install forgecodecli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### Initialize (one-time setup)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
forgecodecli init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You will be prompted to:
|
|
49
|
+
- Select an LLM provider
|
|
50
|
+
- Enter your API key (stored securely)
|
|
51
|
+
|
|
52
|
+
### Start the agent
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
forgecodecli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You are now in interactive agent mode. Example commands:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
create a folder src/app and add a main.py file that prints hello
|
|
62
|
+
read the README.md file
|
|
63
|
+
list all files in the src directory
|
|
64
|
+
quit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or press `Ctrl + C` to exit.
|
|
68
|
+
|
|
69
|
+
## Reset Configuration
|
|
70
|
+
|
|
71
|
+
To remove all configuration and API keys:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
forgecodecli reset
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Security
|
|
78
|
+
|
|
79
|
+
- API keys are stored using the system keyring
|
|
80
|
+
- No API keys are written to config files or environment variables
|
|
81
|
+
- Config files contain only non-sensitive metadata
|
|
82
|
+
|
|
83
|
+
## How It Works
|
|
84
|
+
|
|
85
|
+
1. You enter a natural language command
|
|
86
|
+
2. The LLM decides the next valid action
|
|
87
|
+
3. ForgeCodeCLI executes the action safely
|
|
88
|
+
4. The agent responds with the result
|
|
89
|
+
|
|
90
|
+
The agent is strictly limited to predefined tools, ensuring predictable and safe behavior.
|
|
91
|
+
|
|
92
|
+
## Supported Actions
|
|
93
|
+
|
|
94
|
+
- `read_file`
|
|
95
|
+
- `list_files`
|
|
96
|
+
- `create_dir`
|
|
97
|
+
- `write_file`
|
|
98
|
+
|
|
99
|
+
No action outside these tools is permitted.
|
|
100
|
+
|
|
101
|
+
## Status
|
|
102
|
+
|
|
103
|
+
This project is in active development.
|
|
104
|
+
|
|
105
|
+
**Current version supports:**
|
|
106
|
+
- Gemini LLM
|
|
107
|
+
- Interactive agent mode
|
|
108
|
+
|
|
109
|
+
**Planned features:**
|
|
110
|
+
- Multiple LLM providers
|
|
111
|
+
- Model switching
|
|
112
|
+
- Streaming responses
|
|
113
|
+
- Session memory
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT License
|
|
118
|
+
|
|
119
|
+
## Author
|
|
120
|
+
|
|
121
|
+
Built by Sudhanshu
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
forgecodecli/__init__.py
|
|
4
|
+
forgecodecli/agent.py
|
|
5
|
+
forgecodecli/cli.py
|
|
6
|
+
forgecodecli/config.py
|
|
7
|
+
forgecodecli/secrets.py
|
|
8
|
+
forgecodecli/tools.py
|
|
9
|
+
forgecodecli.egg-info/PKG-INFO
|
|
10
|
+
forgecodecli.egg-info/SOURCES.txt
|
|
11
|
+
forgecodecli.egg-info/dependency_links.txt
|
|
12
|
+
forgecodecli.egg-info/entry_points.txt
|
|
13
|
+
forgecodecli.egg-info/requires.txt
|
|
14
|
+
forgecodecli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
forgecodecli
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "forgecodecli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A minimal agentic CLI for forging code"
|
|
9
|
+
authors = [{name = "Sudhanshu"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"typer[all]>=0.9.0",
|
|
15
|
+
"openai>=1.0.0",
|
|
16
|
+
"keyring>=24.0.0"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=7.0.0",
|
|
22
|
+
"black>=23.0.0",
|
|
23
|
+
"ruff>=0.1.0"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
forgecodecli = "forgecodecli.cli:main"
|