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/cli.py CHANGED
@@ -1,18 +1,57 @@
1
1
  import typer
2
- from forgecodecli.agent import think
3
- from forgecodecli.tools import read_file, list_files, write_file, create_dir
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
- import os
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 = os.getcwd()
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
- Model : Gemini 2.5 Flash
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
- (type 'quit' or Ctrl+C to exit)\n
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
- 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.")
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
- 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.")
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
- 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
-
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
- 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")
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 deleted")
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 in: {args.get('path', '.')}")
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
- typer.echo("ForgeCodeCLI is not set up yet.")
130
- typer.echo("Run `forgecodecli init` first.")
131
- return
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
- try:
140
- while True:
141
- user_input = input("forgecode (agent) > ").strip()
142
-
143
- if user_input.lower() in ("quit", "exit"):
144
- print("Bye")
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
- 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
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))