forgecodecli 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forgecodecli/agent.py +126 -55
- forgecodecli/cli.py +317 -138
- forgecodecli/git_tools.py +69 -0
- forgecodecli/path_resolver.py +24 -0
- forgecodecli/tools.py +123 -17
- forgecodecli-0.2.0.dist-info/METADATA +198 -0
- forgecodecli-0.2.0.dist-info/RECORD +13 -0
- {forgecodecli-0.1.0.dist-info → forgecodecli-0.2.0.dist-info}/WHEEL +1 -1
- forgecodecli-0.1.0.dist-info/METADATA +0 -127
- forgecodecli-0.1.0.dist-info/RECORD +0 -11
- {forgecodecli-0.1.0.dist-info → forgecodecli-0.2.0.dist-info}/entry_points.txt +0 -0
- {forgecodecli-0.1.0.dist-info → forgecodecli-0.2.0.dist-info}/top_level.txt +0 -0
forgecodecli/cli.py
CHANGED
|
@@ -1,18 +1,57 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
4
5
|
import getpass
|
|
6
|
+
import subprocess
|
|
5
7
|
|
|
8
|
+
from forgecodecli import path_resolver
|
|
9
|
+
from forgecodecli.agent import think
|
|
10
|
+
from forgecodecli.tools import read_file, list_files, write_file, create_dir, delete_file, delete_dir, move_file, move_dir, undo
|
|
11
|
+
from forgecodecli.git_tools import (
|
|
12
|
+
git_init, git_add, git_commit, git_push, git_set_origin,
|
|
13
|
+
git_status, git_log, git_branch, git_pull, git_clone
|
|
14
|
+
)
|
|
6
15
|
from forgecodecli.secrets import save_api_key, delete_api_key
|
|
7
|
-
from forgecodecli.config import save_config, config_exists, delete_config
|
|
16
|
+
from forgecodecli.config import save_config, config_exists, delete_config, load_config
|
|
17
|
+
from forgecodecli.path_resolver import resolve_path
|
|
8
18
|
|
|
9
19
|
app = typer.Typer()
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
IS_EXE = getattr(sys, "frozen", False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ===============================
|
|
24
|
+
# UI
|
|
25
|
+
# ===============================
|
|
26
|
+
PROVIDERS = {
|
|
27
|
+
"1": {"name": "Google Gemini", "key": "gemini", "models": [
|
|
28
|
+
("gemini-2.5-flash", "Fast & free tier"),
|
|
29
|
+
("gemini-2.0-flash", "Latest Gemini"),
|
|
30
|
+
("gemini-1.5-pro", "Most powerful Gemini"),
|
|
31
|
+
]},
|
|
32
|
+
"2": {"name": "OpenAI", "key": "openai", "models": [
|
|
33
|
+
("gpt-4o", "Most capable GPT"),
|
|
34
|
+
("gpt-4-turbo", "Fast GPT-4"),
|
|
35
|
+
("gpt-3.5-turbo", "Fast & cheap"),
|
|
36
|
+
]},
|
|
37
|
+
"3": {"name": "Anthropic (Claude)", "key": "anthropic", "models": [
|
|
38
|
+
("claude-3-5-sonnet-20241022", "Best balance"),
|
|
39
|
+
("claude-3-opus-20240229", "Most powerful"),
|
|
40
|
+
("claude-3-haiku-20240307", "Fastest & cheapest"),
|
|
41
|
+
]},
|
|
42
|
+
"4": {"name": "Groq", "key": "groq", "models": [
|
|
43
|
+
("llama-3.3-70b-versatile", "Best Llama 3.3"),
|
|
44
|
+
("mixtral-8x7b-32768", "Mixtral 8x7B"),
|
|
45
|
+
("gemma2-9b-it", "Google Gemma2"),
|
|
46
|
+
]},
|
|
47
|
+
}
|
|
12
48
|
|
|
13
49
|
def show_logo():
|
|
14
|
-
cwd =
|
|
15
|
-
|
|
50
|
+
cwd = path_resolver.CLI_CWD
|
|
51
|
+
config = load_config()
|
|
52
|
+
provider_key = config.get("provider", "gemini")
|
|
53
|
+
model = config.get("model", "gemini-2.5-flash")
|
|
54
|
+
provider_name = next((v["name"] for v in PROVIDERS.values() if v["key"] == provider_key), provider_key)
|
|
16
55
|
print(f"""
|
|
17
56
|
███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
18
57
|
██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
|
|
@@ -26,185 +65,325 @@ Safe • Deterministic • File-aware
|
|
|
26
65
|
|
|
27
66
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
28
67
|
Agent Mode : Code Agent
|
|
29
|
-
|
|
68
|
+
Provider : {provider_name}
|
|
69
|
+
Model : {model}
|
|
30
70
|
Workspace : {cwd}
|
|
31
71
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
32
72
|
|
|
33
73
|
Type natural language commands to manage files.
|
|
34
|
-
|
|
74
|
+
Type 'help' for commands.
|
|
35
75
|
""")
|
|
36
76
|
|
|
77
|
+
def wait_before_exit():
|
|
78
|
+
if IS_EXE and os.name == "nt":
|
|
79
|
+
try:
|
|
80
|
+
input("\nPress Enter to close...")
|
|
81
|
+
except EOFError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ===============================
|
|
86
|
+
# SETUP COMMANDS
|
|
87
|
+
# ===============================
|
|
37
88
|
@app.command()
|
|
38
|
-
def init()
|
|
39
|
-
"""
|
|
40
|
-
Initialize ForgeCode CLI configuration
|
|
41
|
-
"""
|
|
89
|
+
def init():
|
|
90
|
+
"""Initialize ForgeCodeCLI"""
|
|
42
91
|
if config_exists():
|
|
43
92
|
typer.echo("ForgeCodeCLI is already set up.")
|
|
44
|
-
typer.echo("Use `forgecodecli reset` to reconfigure.")
|
|
45
93
|
return
|
|
46
|
-
|
|
47
|
-
typer.echo("Welcome to ForgeCodeCLI
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
typer.echo("Select LLM
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
|
|
95
|
+
typer.echo("Welcome to ForgeCodeCLI ✨\n")
|
|
96
|
+
|
|
97
|
+
# ── Step 1: Select Provider ──
|
|
98
|
+
typer.echo("Select LLM Provider:")
|
|
99
|
+
for num, p in PROVIDERS.items():
|
|
100
|
+
typer.echo(f" {num}) {p['name']}")
|
|
101
|
+
typer.echo(" 5) Exit")
|
|
102
|
+
|
|
103
|
+
provider_choice = typer.prompt(">").strip()
|
|
104
|
+
if provider_choice not in PROVIDERS:
|
|
105
|
+
typer.echo("Setup cancelled.")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
provider = PROVIDERS[provider_choice]
|
|
109
|
+
|
|
110
|
+
# ── Step 2: Select Model ──
|
|
111
|
+
typer.echo(f"\nSelect Model for {provider['name']}:")
|
|
112
|
+
for i, (model_id, desc) in enumerate(provider["models"], 1):
|
|
113
|
+
typer.echo(f" {i}) {model_id} — {desc}")
|
|
114
|
+
|
|
115
|
+
model_choice = typer.prompt(">").strip()
|
|
116
|
+
try:
|
|
117
|
+
selected_model = provider["models"][int(model_choice) - 1][0]
|
|
118
|
+
except (ValueError, IndexError):
|
|
119
|
+
typer.echo("Invalid choice. Setup cancelled.")
|
|
57
120
|
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
|
|
122
|
+
# ── Step 3: Enter API Key ──
|
|
123
|
+
api_key = getpass.getpass(f"\nEnter your {provider['name']} API Key: ").strip()
|
|
124
|
+
if not api_key:
|
|
125
|
+
typer.echo("API Key cannot be empty.")
|
|
62
126
|
return
|
|
127
|
+
|
|
63
128
|
save_api_key(api_key)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
129
|
+
save_config({
|
|
130
|
+
"provider": provider["key"],
|
|
131
|
+
"model": selected_model
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
# Auto-install provider SDK if needed
|
|
135
|
+
if provider["key"] == "anthropic":
|
|
136
|
+
typer.echo("\n📦 Installing Anthropic SDK...")
|
|
137
|
+
try:
|
|
138
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "anthropic", "-q"])
|
|
139
|
+
typer.echo("✓ Anthropic SDK installed")
|
|
140
|
+
except subprocess.CalledProcessError:
|
|
141
|
+
typer.echo("⚠️ Could not auto-install. Run: pip install anthropic")
|
|
142
|
+
|
|
143
|
+
typer.echo(f"\n✓ Setup complete")
|
|
144
|
+
typer.echo(f" Provider : {provider['name']}")
|
|
145
|
+
typer.echo(f" Model : {selected_model}")
|
|
146
|
+
typer.echo("\nRun `forgecodecli` to start.")
|
|
147
|
+
|
|
148
|
+
|
|
78
149
|
@app.command()
|
|
79
150
|
def reset():
|
|
80
|
-
"""
|
|
81
|
-
Reset ForgeCodeCLI configuration and API key
|
|
82
|
-
"""
|
|
151
|
+
"""Reset configuration"""
|
|
83
152
|
if not config_exists():
|
|
84
153
|
typer.echo("ForgeCodeCLI is not set up.")
|
|
85
154
|
return
|
|
86
155
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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")
|
|
156
|
+
if not IS_EXE:
|
|
157
|
+
confirm = typer.prompt("Are you sure? (y/N)", default="n")
|
|
158
|
+
if confirm.lower() != "y":
|
|
159
|
+
typer.echo("Reset cancelled.")
|
|
160
|
+
return
|
|
101
161
|
|
|
162
|
+
delete_api_key()
|
|
102
163
|
delete_config()
|
|
103
|
-
typer.echo("✓ Configuration
|
|
104
|
-
|
|
105
|
-
typer.echo("\nForgeCodeCLI has been reset.")
|
|
106
|
-
typer.echo("Run `forgecodecli init` to set it up again.")
|
|
164
|
+
typer.echo("✓ Configuration reset")
|
|
107
165
|
|
|
108
166
|
|
|
167
|
+
# ===============================
|
|
168
|
+
# ACTION UI
|
|
169
|
+
# ===============================
|
|
109
170
|
def describe_action(action: str, args: dict):
|
|
110
171
|
if action == "read_file":
|
|
111
172
|
print(f"📂 Reading file: {args.get('path')}")
|
|
112
173
|
elif action == "list_files":
|
|
113
|
-
print(f"📄 Listing files
|
|
174
|
+
print(f"📄 Listing files: {args.get('path', '.')}")
|
|
114
175
|
elif action == "create_dir":
|
|
115
176
|
print(f"📁 Creating directory: {args.get('path')}")
|
|
116
177
|
elif action == "write_file":
|
|
117
178
|
print(f"✍️ Writing file: {args.get('path')}")
|
|
118
|
-
|
|
119
|
-
|
|
179
|
+
elif action == "delete_file":
|
|
180
|
+
print(f"🗑️ Deleting file: {args.get('path')}")
|
|
181
|
+
elif action == "delete_dir":
|
|
182
|
+
print(f"🗑️ Deleting directory: {args.get('path')}")
|
|
183
|
+
elif action == "move_file":
|
|
184
|
+
print(f"🔄 Moving file: {args.get('src')} → {args.get('dst')}")
|
|
185
|
+
elif action == "move_dir":
|
|
186
|
+
print(f"🔄 Moving directory: {args.get('src')} → {args.get('dst')}")
|
|
187
|
+
elif action == "undo":
|
|
188
|
+
print(f"↩️ Undoing last operation")
|
|
189
|
+
elif action == "git_init":
|
|
190
|
+
print(f"🔧 Initializing git repository")
|
|
191
|
+
elif action == "git_add":
|
|
192
|
+
print(f"📝 Staging files: {args.get('path', '.')}")
|
|
193
|
+
elif action == "git_commit":
|
|
194
|
+
print(f"💾 Committing: {args.get('message', '')}")
|
|
195
|
+
elif action == "git_push":
|
|
196
|
+
print(f"🚀 Pushing to {args.get('branch', 'main')}")
|
|
197
|
+
elif action == "git_set_origin":
|
|
198
|
+
print(f"🔗 Setting origin: {args.get('url', '')}")
|
|
199
|
+
elif action == "git_status":
|
|
200
|
+
print(f"📊 Checking git status")
|
|
201
|
+
elif action == "git_log":
|
|
202
|
+
print(f"📜 Showing commit history")
|
|
203
|
+
elif action == "git_branch":
|
|
204
|
+
if args.get("name"):
|
|
205
|
+
print(f"🌿 Creating branch: {args.get('name')}")
|
|
206
|
+
else:
|
|
207
|
+
print(f"🌿 Listing branches")
|
|
208
|
+
elif action == "git_pull":
|
|
209
|
+
print(f"⬇️ Pulling from remote")
|
|
210
|
+
elif action == "git_clone":
|
|
211
|
+
print(f"📦 Cloning repository: {args.get('url', '')}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ===============================
|
|
215
|
+
# MAIN LOOP
|
|
216
|
+
# ===============================
|
|
120
217
|
@app.callback(invoke_without_command=True)
|
|
121
218
|
def run(ctx: typer.Context):
|
|
122
|
-
"""
|
|
123
|
-
ForgeCode CLI — agent with actions
|
|
124
|
-
"""
|
|
125
219
|
if ctx.invoked_subcommand is not None:
|
|
126
220
|
return
|
|
127
221
|
|
|
128
222
|
if not config_exists():
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
223
|
+
print("ForgeCodeCLI is not set up.")
|
|
224
|
+
print("Starting setup...\n")
|
|
225
|
+
init()
|
|
132
226
|
|
|
133
|
-
# ===============================
|
|
134
|
-
# INTERACTIVE MODE
|
|
135
|
-
# ===============================
|
|
136
227
|
show_logo()
|
|
137
228
|
messages = []
|
|
138
229
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
230
|
+
while True:
|
|
231
|
+
try:
|
|
232
|
+
raw_input = input("forgecode > ").strip()
|
|
233
|
+
user_input = raw_input.lower()
|
|
234
|
+
if not user_input:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# -------- INTERNAL COMMANDS --------
|
|
238
|
+
if user_input == "exit":
|
|
239
|
+
print("Goodbye.")
|
|
240
|
+
wait_before_exit()
|
|
241
|
+
sys.exit(0)
|
|
242
|
+
|
|
243
|
+
if user_input == "help":
|
|
244
|
+
print("""
|
|
245
|
+
Available commands:
|
|
246
|
+
help - Show this help
|
|
247
|
+
undo - Undo last file operation
|
|
248
|
+
reset - Reset configuration
|
|
249
|
+
exit - Exit ForgeCodeCLI
|
|
250
|
+
""")
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
if user_input == "undo":
|
|
254
|
+
result = undo()
|
|
255
|
+
print(result)
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
if user_input == "reset":
|
|
259
|
+
reset()
|
|
260
|
+
print("\nRestarting setup...\n")
|
|
261
|
+
init()
|
|
262
|
+
messages = []
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
if user_input.startswith(("cd ", "navigate ", "go to ")):
|
|
266
|
+
target = raw_input.split(" ",1)[1]
|
|
267
|
+
new_path = resolve_path(target)
|
|
268
|
+
|
|
269
|
+
if os.path.isdir(new_path):
|
|
270
|
+
path_resolver.CLI_CWD = new_path
|
|
271
|
+
print(f"{path_resolver.CLI_CWD} > ")
|
|
272
|
+
else:
|
|
273
|
+
print("❌ Directory does not exist.")
|
|
274
|
+
continue
|
|
275
|
+
# -------- SEND TO AGENT --------
|
|
276
|
+
messages.append({"role": "user", "content": raw_input})
|
|
277
|
+
answered = False
|
|
278
|
+
actions_taken = 0
|
|
279
|
+
MAX_ACTIONS = 2
|
|
280
|
+
|
|
281
|
+
for _ in range(MAX_ACTIONS + 2):
|
|
282
|
+
# If action limit hit, force agent to answer
|
|
283
|
+
if actions_taken >= MAX_ACTIONS:
|
|
284
|
+
messages.append({"role": "user", "content": "[System]: You have completed the required actions. Now respond with the 'answer' action only."})
|
|
285
|
+
|
|
286
|
+
decision = think(messages)
|
|
287
|
+
action = decision.get("action")
|
|
288
|
+
args = decision.get("args", {})
|
|
289
|
+
|
|
290
|
+
# Append agent's decision as assistant message
|
|
291
|
+
messages.append({"role": "assistant", "content": json.dumps(decision)})
|
|
292
|
+
|
|
293
|
+
if action == "read_file":
|
|
294
|
+
describe_action(action, args)
|
|
295
|
+
real_path = resolve_path(args.get("path"))
|
|
296
|
+
result = read_file(real_path)
|
|
297
|
+
elif action == "list_files":
|
|
298
|
+
describe_action(action, args)
|
|
299
|
+
real_path = resolve_path(args.get("path", "."))
|
|
300
|
+
result = list_files(real_path)
|
|
301
|
+
elif action == "create_dir":
|
|
302
|
+
describe_action(action, args)
|
|
303
|
+
real_path = resolve_path(args.get("path"))
|
|
304
|
+
result = create_dir(real_path)
|
|
305
|
+
elif action == "write_file":
|
|
306
|
+
describe_action(action, args)
|
|
307
|
+
real_path = resolve_path(args.get("path"))
|
|
308
|
+
result = write_file(real_path, args.get("content"))
|
|
309
|
+
elif action == "delete_file":
|
|
310
|
+
describe_action(action, args)
|
|
311
|
+
real_path = resolve_path(args.get("path"))
|
|
312
|
+
result = delete_file(real_path)
|
|
313
|
+
elif action == "delete_dir":
|
|
314
|
+
describe_action(action, args)
|
|
315
|
+
real_path = resolve_path(args.get("path"))
|
|
316
|
+
result = delete_dir(real_path)
|
|
317
|
+
elif action == "move_file":
|
|
318
|
+
describe_action(action, args)
|
|
319
|
+
src_path = resolve_path(args.get("src"))
|
|
320
|
+
dst_path = resolve_path(args.get("dst"))
|
|
321
|
+
result = move_file(src_path, dst_path)
|
|
322
|
+
elif action == "move_dir":
|
|
323
|
+
describe_action(action, args)
|
|
324
|
+
src_path = resolve_path(args.get("src"))
|
|
325
|
+
dst_path = resolve_path(args.get("dst"))
|
|
326
|
+
result = move_dir(src_path, dst_path)
|
|
327
|
+
elif action == "undo":
|
|
328
|
+
describe_action(action, args)
|
|
329
|
+
result = undo()
|
|
330
|
+
elif action == "git_init":
|
|
331
|
+
describe_action(action, args)
|
|
332
|
+
result = git_init()
|
|
333
|
+
elif action == "git_add":
|
|
334
|
+
describe_action(action, args)
|
|
335
|
+
result = git_add(args.get("path", "."))
|
|
336
|
+
elif action == "git_commit":
|
|
337
|
+
describe_action(action, args)
|
|
338
|
+
result = git_commit(args.get("message", ""))
|
|
339
|
+
elif action == "git_push":
|
|
340
|
+
describe_action(action, args)
|
|
341
|
+
result = git_push(args.get("branch", "main"))
|
|
342
|
+
elif action == "git_set_origin":
|
|
343
|
+
describe_action(action, args)
|
|
344
|
+
result = git_set_origin(args.get("url", ""))
|
|
345
|
+
elif action == "git_status":
|
|
346
|
+
describe_action(action, args)
|
|
347
|
+
result = git_status()
|
|
348
|
+
elif action == "git_log":
|
|
349
|
+
describe_action(action, args)
|
|
350
|
+
result = git_log(args.get("lines", 5))
|
|
351
|
+
elif action == "git_branch":
|
|
352
|
+
describe_action(action, args)
|
|
353
|
+
result = git_branch(args.get("name"))
|
|
354
|
+
elif action == "git_pull":
|
|
355
|
+
describe_action(action, args)
|
|
356
|
+
result = git_pull()
|
|
357
|
+
elif action == "git_clone":
|
|
358
|
+
describe_action(action, args)
|
|
359
|
+
result = git_clone(args.get("url", ""), args.get("path", "."))
|
|
360
|
+
elif action == "answer":
|
|
361
|
+
print(args.get("text", ""))
|
|
362
|
+
answered = True
|
|
145
363
|
break
|
|
364
|
+
else:
|
|
365
|
+
result = "⚠️ Unknown action"
|
|
366
|
+
|
|
367
|
+
actions_taken += 1
|
|
368
|
+
print(result)
|
|
369
|
+
# Append tool result as user message so agent knows what happened
|
|
370
|
+
messages.append({"role": "user", "content": f"[Tool result]: {result}"})
|
|
146
371
|
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
372
|
+
if not answered:
|
|
373
|
+
print("⚠️ Could not complete request.")
|
|
202
374
|
|
|
375
|
+
if len(messages) > 20:
|
|
376
|
+
messages = messages[-20:]
|
|
203
377
|
|
|
378
|
+
except KeyboardInterrupt:
|
|
379
|
+
print("\nInterrupted. Type 'exit' to quit.")
|
|
204
380
|
|
|
381
|
+
|
|
382
|
+
# ===============================
|
|
383
|
+
# ENTRY
|
|
384
|
+
# ===============================
|
|
205
385
|
def main():
|
|
206
386
|
app()
|
|
207
387
|
|
|
208
|
-
|
|
209
388
|
if __name__ == "__main__":
|
|
210
389
|
main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Git operations for ForgeCodeCLI"""
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
def run_git_command(cmd: str) -> str:
|
|
6
|
+
"""Execute a git command and return output"""
|
|
7
|
+
try:
|
|
8
|
+
result = subprocess.run(
|
|
9
|
+
cmd,
|
|
10
|
+
shell=True,
|
|
11
|
+
capture_output=True,
|
|
12
|
+
text=True,
|
|
13
|
+
cwd=os.getcwd()
|
|
14
|
+
)
|
|
15
|
+
if result.returncode != 0:
|
|
16
|
+
return f"❌ {result.stderr.strip()}"
|
|
17
|
+
return f"✅ {result.stdout.strip()}"
|
|
18
|
+
except Exception as e:
|
|
19
|
+
return f"❌ Error: {str(e)}"
|
|
20
|
+
|
|
21
|
+
def git_init() -> str:
|
|
22
|
+
"""Initialize a new git repository"""
|
|
23
|
+
return run_git_command("git init")
|
|
24
|
+
|
|
25
|
+
def git_add(path: str = ".") -> str:
|
|
26
|
+
"""Stage files for commit"""
|
|
27
|
+
if path == ".":
|
|
28
|
+
return run_git_command("git add .")
|
|
29
|
+
return run_git_command(f'git add "{path}"')
|
|
30
|
+
|
|
31
|
+
def git_commit(message: str) -> str:
|
|
32
|
+
"""Commit staged changes"""
|
|
33
|
+
if not message:
|
|
34
|
+
return "❌ Commit message cannot be empty"
|
|
35
|
+
return run_git_command(f'git commit -m "{message}"')
|
|
36
|
+
|
|
37
|
+
def git_push(branch: str = "main") -> str:
|
|
38
|
+
"""Push commits to remote repository"""
|
|
39
|
+
return run_git_command(f"git push origin {branch}")
|
|
40
|
+
|
|
41
|
+
def git_set_origin(url: str) -> str:
|
|
42
|
+
"""Set remote repository URL"""
|
|
43
|
+
if not url:
|
|
44
|
+
return "❌ Repository URL cannot be empty"
|
|
45
|
+
return run_git_command(f'git remote add origin "{url}"')
|
|
46
|
+
|
|
47
|
+
def git_status() -> str:
|
|
48
|
+
"""Show git status"""
|
|
49
|
+
return run_git_command("git status")
|
|
50
|
+
|
|
51
|
+
def git_log(lines: int = 5) -> str:
|
|
52
|
+
"""Show commit history"""
|
|
53
|
+
return run_git_command(f"git log --oneline -n {lines}")
|
|
54
|
+
|
|
55
|
+
def git_branch(name: str = None) -> str:
|
|
56
|
+
"""List branches or create new branch"""
|
|
57
|
+
if name:
|
|
58
|
+
return run_git_command(f"git checkout -b {name}")
|
|
59
|
+
return run_git_command("git branch -a")
|
|
60
|
+
|
|
61
|
+
def git_pull() -> str:
|
|
62
|
+
"""Pull changes from remote"""
|
|
63
|
+
return run_git_command("git pull origin main")
|
|
64
|
+
|
|
65
|
+
def git_clone(url: str, path: str = ".") -> str:
|
|
66
|
+
"""Clone a repository"""
|
|
67
|
+
if not url:
|
|
68
|
+
return "❌ Repository URL cannot be empty"
|
|
69
|
+
return run_git_command(f'git clone "{url}" "{path}"')
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
CLI_CWD = os.getcwd()
|
|
4
|
+
|
|
5
|
+
def resolve_path(path: str) -> str:
|
|
6
|
+
global CLI_CWD
|
|
7
|
+
|
|
8
|
+
if not path or path in (".", "cwd"):
|
|
9
|
+
return CLI_CWD
|
|
10
|
+
|
|
11
|
+
path = path.strip()
|
|
12
|
+
|
|
13
|
+
if os.path.isabs(path):
|
|
14
|
+
return path
|
|
15
|
+
|
|
16
|
+
home = os.path.expanduser("~")
|
|
17
|
+
|
|
18
|
+
if path.lower() == "desktop":
|
|
19
|
+
return os.path.join(home, "Desktop")
|
|
20
|
+
|
|
21
|
+
if path.lower() == "documents":
|
|
22
|
+
return os.path.join(home, "Documents")
|
|
23
|
+
|
|
24
|
+
return os.path.abspath(os.path.join(CLI_CWD, path))
|