freeai-code 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.
Files changed (35) hide show
  1. freeai_code-0.1.0/LICENSE +21 -0
  2. freeai_code-0.1.0/PKG-INFO +109 -0
  3. freeai_code-0.1.0/README.md +79 -0
  4. freeai_code-0.1.0/free_code/__init__.py +3 -0
  5. freeai_code-0.1.0/free_code/__main__.py +6 -0
  6. freeai_code-0.1.0/free_code/agent.py +262 -0
  7. freeai_code-0.1.0/free_code/auth.py +74 -0
  8. freeai_code-0.1.0/free_code/cli.py +305 -0
  9. freeai_code-0.1.0/free_code/client.py +308 -0
  10. freeai_code-0.1.0/free_code/config.py +125 -0
  11. freeai_code-0.1.0/free_code/context/__init__.py +1 -0
  12. freeai_code-0.1.0/free_code/context/discovery.py +140 -0
  13. freeai_code-0.1.0/free_code/context/repo_map.py +86 -0
  14. freeai_code-0.1.0/free_code/context/window.py +169 -0
  15. freeai_code-0.1.0/free_code/models.py +56 -0
  16. freeai_code-0.1.0/free_code/streaming.py +81 -0
  17. freeai_code-0.1.0/free_code/tools/__init__.py +158 -0
  18. freeai_code-0.1.0/free_code/tools/file_ops.py +112 -0
  19. freeai_code-0.1.0/free_code/tools/git_ops.py +102 -0
  20. freeai_code-0.1.0/free_code/tools/list_files.py +107 -0
  21. freeai_code-0.1.0/free_code/tools/search.py +94 -0
  22. freeai_code-0.1.0/free_code/tools/shell.py +95 -0
  23. freeai_code-0.1.0/free_code/tools/test_runner.py +131 -0
  24. freeai_code-0.1.0/free_code/ui/__init__.py +1 -0
  25. freeai_code-0.1.0/free_code/ui/diff_view.py +55 -0
  26. freeai_code-0.1.0/free_code/ui/prompt.py +85 -0
  27. freeai_code-0.1.0/free_code/ui/terminal.py +118 -0
  28. freeai_code-0.1.0/freeai_code.egg-info/PKG-INFO +109 -0
  29. freeai_code-0.1.0/freeai_code.egg-info/SOURCES.txt +33 -0
  30. freeai_code-0.1.0/freeai_code.egg-info/dependency_links.txt +1 -0
  31. freeai_code-0.1.0/freeai_code.egg-info/entry_points.txt +2 -0
  32. freeai_code-0.1.0/freeai_code.egg-info/requires.txt +6 -0
  33. freeai_code-0.1.0/freeai_code.egg-info/top_level.txt +1 -0
  34. freeai_code-0.1.0/pyproject.toml +43 -0
  35. freeai_code-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Free.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: freeai-code
3
+ Version: 0.1.0
4
+ Summary: Free AI coding assistant — Claude Code alternative
5
+ Author: Free.ai
6
+ License: MIT
7
+ Project-URL: Homepage, https://free.ai
8
+ Project-URL: Repository, https://github.com/nadermx/free-code
9
+ Keywords: ai,coding,assistant,cli,free
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: click>=8.0
24
+ Requires-Dist: rich>=13.0
25
+ Requires-Dist: httpx>=0.25
26
+ Requires-Dist: pyyaml>=6.0
27
+ Requires-Dist: pathspec>=0.11
28
+ Requires-Dist: tiktoken>=0.5
29
+ Dynamic: license-file
30
+
31
+ # free-code
32
+
33
+ Free AI coding assistant for your terminal. Powered by [Free.ai](https://free.ai).
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install free-code
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```bash
44
+ # Interactive coding session in your project directory
45
+ cd your-project/
46
+ free-code
47
+
48
+ # Ask a one-shot question about your codebase
49
+ free-code ask "How does the auth system work?"
50
+
51
+ # Execute a task
52
+ free-code run "Add unit tests for the User model"
53
+
54
+ # Initialize and scan codebase
55
+ free-code init
56
+ ```
57
+
58
+ ## Commands
59
+
60
+ | Command | Description |
61
+ |---------|-------------|
62
+ | `free-code` | Interactive coding session (alias: `free-code chat`) |
63
+ | `free-code ask "question"` | One-shot question about the codebase |
64
+ | `free-code run "task"` | Execute a coding task |
65
+ | `free-code init` | Initialize config, scan codebase |
66
+ | `free-code config` | Show current configuration |
67
+ | `free-code config set key value` | Set a config value |
68
+ | `free-code login` | Authenticate with Free.ai |
69
+
70
+ ## Configuration
71
+
72
+ Config is stored at `~/.free-code/config.yaml`.
73
+
74
+ ```bash
75
+ # Set your Free.ai token
76
+ free-code config set token sk-free-xxx
77
+
78
+ # Use your own API key (BYOK)
79
+ free-code config set provider openai
80
+ free-code config set api_key sk-xxx
81
+
82
+ # Choose a model
83
+ free-code config set model qwen2.5-coder-32b
84
+
85
+ # Enable safe mode (confirm before file writes)
86
+ free-code config set safe_mode true
87
+ ```
88
+
89
+ ### Supported Providers
90
+
91
+ - **free.ai** (default) — Free tier with daily limits, paid plans for more
92
+ - **openai** — Bring your own OpenAI API key
93
+ - **anthropic** — Bring your own Anthropic API key
94
+ - **google** — Bring your own Google AI API key
95
+ - **openrouter** — Access 300+ models with one key
96
+
97
+ ## Features
98
+
99
+ - Reads and edits files in your project
100
+ - Shell command execution (with confirmation)
101
+ - Git integration (status, diff, commit, branch)
102
+ - Test runner auto-detection (pytest, jest, go test, cargo test)
103
+ - Streaming output with syntax highlighting
104
+ - Context window optimization
105
+ - .gitignore-aware file discovery
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,79 @@
1
+ # free-code
2
+
3
+ Free AI coding assistant for your terminal. Powered by [Free.ai](https://free.ai).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install free-code
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Interactive coding session in your project directory
15
+ cd your-project/
16
+ free-code
17
+
18
+ # Ask a one-shot question about your codebase
19
+ free-code ask "How does the auth system work?"
20
+
21
+ # Execute a task
22
+ free-code run "Add unit tests for the User model"
23
+
24
+ # Initialize and scan codebase
25
+ free-code init
26
+ ```
27
+
28
+ ## Commands
29
+
30
+ | Command | Description |
31
+ |---------|-------------|
32
+ | `free-code` | Interactive coding session (alias: `free-code chat`) |
33
+ | `free-code ask "question"` | One-shot question about the codebase |
34
+ | `free-code run "task"` | Execute a coding task |
35
+ | `free-code init` | Initialize config, scan codebase |
36
+ | `free-code config` | Show current configuration |
37
+ | `free-code config set key value` | Set a config value |
38
+ | `free-code login` | Authenticate with Free.ai |
39
+
40
+ ## Configuration
41
+
42
+ Config is stored at `~/.free-code/config.yaml`.
43
+
44
+ ```bash
45
+ # Set your Free.ai token
46
+ free-code config set token sk-free-xxx
47
+
48
+ # Use your own API key (BYOK)
49
+ free-code config set provider openai
50
+ free-code config set api_key sk-xxx
51
+
52
+ # Choose a model
53
+ free-code config set model qwen2.5-coder-32b
54
+
55
+ # Enable safe mode (confirm before file writes)
56
+ free-code config set safe_mode true
57
+ ```
58
+
59
+ ### Supported Providers
60
+
61
+ - **free.ai** (default) — Free tier with daily limits, paid plans for more
62
+ - **openai** — Bring your own OpenAI API key
63
+ - **anthropic** — Bring your own Anthropic API key
64
+ - **google** — Bring your own Google AI API key
65
+ - **openrouter** — Access 300+ models with one key
66
+
67
+ ## Features
68
+
69
+ - Reads and edits files in your project
70
+ - Shell command execution (with confirmation)
71
+ - Git integration (status, diff, commit, branch)
72
+ - Test runner auto-detection (pytest, jest, go test, cargo test)
73
+ - Streaming output with syntax highlighting
74
+ - Context window optimization
75
+ - .gitignore-aware file discovery
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,3 @@
1
+ """Free.ai Coder CLI — Free AI coding assistant."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m free_code`."""
2
+
3
+ from free_code.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,262 @@
1
+ """Local agent loop — plan, execute, observe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from rich.console import Console
11
+ from rich.live import Live
12
+ from rich.spinner import Spinner
13
+ from rich.text import Text
14
+
15
+ from free_code.client import CoderClient
16
+ from free_code.config import load_config
17
+ from free_code.context.repo_map import generate_repo_map
18
+ from free_code.context.window import build_context
19
+ from free_code.tools import TOOL_DEFINITIONS, TOOL_REGISTRY
20
+ from free_code.tools.shell import is_dangerous
21
+ from free_code.ui.terminal import (
22
+ confirm,
23
+ print_error,
24
+ print_markdown,
25
+ print_tool_call,
26
+ print_tool_result,
27
+ )
28
+
29
+ console = Console()
30
+
31
+ SYSTEM_PROMPT = """\
32
+ You are Free.ai Coder, an expert AI coding assistant running in the user's terminal.
33
+ You have access to their local filesystem and can read, write, and edit files.
34
+
35
+ ## Tools Available
36
+ You can use these tools to help the user:
37
+ - file_read: Read file contents
38
+ - file_write: Write/create files
39
+ - apply_patch: Search/replace edit in a file
40
+ - shell_command: Execute shell commands
41
+ - grep_search: Search for patterns in code
42
+ - git_status, git_diff, git_commit, git_log: Git operations
43
+ - run_tests: Run the project's test suite
44
+ - list_files: List project files
45
+
46
+ ## Guidelines
47
+ - Read files before editing them to understand the full context
48
+ - Make minimal, focused changes
49
+ - Show diffs for significant edits
50
+ - Run tests after making changes when appropriate
51
+ - Use grep_search to find relevant code before making changes
52
+ - Prefer apply_patch over file_write for editing existing files
53
+ - Ask for confirmation before destructive operations
54
+ - Be concise in explanations, verbose in code
55
+
56
+ When you need to use a tool, respond with a JSON tool call in this format:
57
+ {"tool": "tool_name", "args": {"arg1": "value1"}}
58
+
59
+ After each tool result, analyze the output and decide the next step.
60
+ When you're done, provide a final summary of what was accomplished.
61
+ """
62
+
63
+ MAX_AGENT_STEPS = 30
64
+
65
+
66
+ class Agent:
67
+ """The coding agent — runs a plan/execute/observe loop."""
68
+
69
+ def __init__(
70
+ self,
71
+ project_root: Path,
72
+ config: Optional[Dict[str, Any]] = None,
73
+ ) -> None:
74
+ self.project_root = project_root.resolve()
75
+ self.config = config or load_config()
76
+ self.client = CoderClient(self.config)
77
+ self.messages: List[Dict[str, str]] = []
78
+ self.safe_mode = self.config.get("safe_mode", True)
79
+
80
+ async def chat(self, user_message: str) -> None:
81
+ """Process a user message through the agent loop."""
82
+ # Build context on first message
83
+ if not self.messages:
84
+ context, included = build_context(
85
+ self.project_root,
86
+ user_message,
87
+ max_tokens=self.config.get("max_context_tokens", 32000),
88
+ )
89
+ system = SYSTEM_PROMPT + f"\n\n## Project Context\n\n{context}"
90
+ else:
91
+ system = None
92
+
93
+ self.messages.append({"role": "user", "content": user_message})
94
+
95
+ for step in range(MAX_AGENT_STEPS):
96
+ # Get LLM response
97
+ response_text = await self._get_response(system)
98
+ system = None # Only send system on first turn
99
+
100
+ if not response_text:
101
+ break
102
+
103
+ # Check for tool calls in the response
104
+ tool_call = self._extract_tool_call(response_text)
105
+
106
+ if tool_call:
107
+ tool_name = tool_call["tool"]
108
+ tool_args = tool_call.get("args", {})
109
+
110
+ # Print any text before the tool call
111
+ pre_text = response_text[:response_text.find('{"tool"')].strip()
112
+ if pre_text:
113
+ print_markdown(pre_text)
114
+
115
+ # Execute the tool
116
+ result = await self._execute_tool(tool_name, tool_args)
117
+
118
+ if result is None:
119
+ # Tool was cancelled by user
120
+ self.messages.append({
121
+ "role": "assistant",
122
+ "content": response_text,
123
+ })
124
+ self.messages.append({
125
+ "role": "user",
126
+ "content": "(Tool execution cancelled by user)",
127
+ })
128
+ continue
129
+
130
+ # Add assistant message and tool result
131
+ self.messages.append({
132
+ "role": "assistant",
133
+ "content": response_text,
134
+ })
135
+ self.messages.append({
136
+ "role": "user",
137
+ "content": f"[Tool result for {tool_name}]:\n{result}",
138
+ })
139
+ else:
140
+ # No tool call — this is the final response
141
+ print_markdown(response_text)
142
+ self.messages.append({
143
+ "role": "assistant",
144
+ "content": response_text,
145
+ })
146
+ break
147
+
148
+ async def _get_response(self, system: Optional[str] = None) -> str:
149
+ """Get a streaming response from the LLM."""
150
+ chunks: List[str] = []
151
+ spinner = Spinner("dots", text="Thinking...")
152
+
153
+ with Live(spinner, console=console, refresh_per_second=10, transient=True):
154
+ first_chunk = True
155
+ async for event in self.client.chat_stream(self.messages, system=system):
156
+ event_type = event.get("type", "")
157
+
158
+ if event_type == "text":
159
+ content = event.get("content", "")
160
+ if first_chunk:
161
+ first_chunk = False
162
+ chunks.append(content)
163
+
164
+ elif event_type == "error":
165
+ print_error(event.get("content", "Unknown error"))
166
+ return ""
167
+
168
+ elif event_type == "done":
169
+ break
170
+
171
+ return "".join(chunks)
172
+
173
+ def _extract_tool_call(self, text: str) -> Optional[Dict[str, Any]]:
174
+ """Extract a tool call JSON from the response text."""
175
+ # Look for {"tool": "..."} pattern
176
+ start = text.find('{"tool"')
177
+ if start == -1:
178
+ return None
179
+
180
+ # Find the matching closing brace
181
+ depth = 0
182
+ for i in range(start, len(text)):
183
+ if text[i] == "{":
184
+ depth += 1
185
+ elif text[i] == "}":
186
+ depth -= 1
187
+ if depth == 0:
188
+ try:
189
+ data = json.loads(text[start:i + 1])
190
+ if "tool" in data:
191
+ return data
192
+ except json.JSONDecodeError:
193
+ pass
194
+ break
195
+ return None
196
+
197
+ async def _execute_tool(
198
+ self,
199
+ tool_name: str,
200
+ tool_args: Dict[str, Any],
201
+ ) -> Optional[str]:
202
+ """Execute a tool and return the result. Returns None if cancelled."""
203
+ if tool_name not in TOOL_REGISTRY:
204
+ return f"Error: Unknown tool: {tool_name}"
205
+
206
+ # Safety checks
207
+ if self.safe_mode:
208
+ if tool_name in ("file_write", "apply_patch", "git_commit"):
209
+ print_tool_call(tool_name, tool_args)
210
+ if not confirm("Apply this change?"):
211
+ return None
212
+
213
+ if tool_name == "shell_command":
214
+ cmd = tool_args.get("command", "")
215
+ print_tool_call(tool_name, tool_args)
216
+ if is_dangerous(cmd):
217
+ console.print("[warning]This command could be dangerous.[/warning]")
218
+ if not confirm(f"Run: {cmd}"):
219
+ return None
220
+ else:
221
+ # Even in non-safe mode, confirm dangerous shell commands
222
+ if tool_name == "shell_command" and is_dangerous(tool_args.get("command", "")):
223
+ print_tool_call(tool_name, tool_args)
224
+ if not confirm("This command could be dangerous. Run anyway?"):
225
+ return None
226
+
227
+ if tool_name not in ("file_write", "apply_patch", "shell_command", "git_commit") or not self.safe_mode:
228
+ print_tool_call(tool_name, tool_args)
229
+
230
+ # Inject project root
231
+ tool_args["_project_root"] = str(self.project_root)
232
+
233
+ # Execute
234
+ func = TOOL_REGISTRY[tool_name]
235
+ try:
236
+ result = func(**tool_args)
237
+ except TypeError as e:
238
+ # Handle unexpected keyword arguments
239
+ tool_args_clean = {k: v for k, v in tool_args.items() if not k.startswith("_")}
240
+ tool_args_clean["_project_root"] = str(self.project_root)
241
+ try:
242
+ result = func(**tool_args_clean)
243
+ except Exception as e2:
244
+ result = f"Error executing {tool_name}: {e2}"
245
+ except Exception as e:
246
+ result = f"Error executing {tool_name}: {e}"
247
+
248
+ print_tool_result(result, collapsed=len(str(result)) > 1000)
249
+ return str(result)
250
+
251
+ def clear_history(self) -> None:
252
+ """Clear conversation history."""
253
+ self.messages.clear()
254
+
255
+ def compact_history(self) -> None:
256
+ """Summarize conversation to save context."""
257
+ if len(self.messages) <= 2:
258
+ return
259
+ # Keep first and last 4 messages, summarize the middle
260
+ kept = self.messages[:1] + self.messages[-4:]
261
+ summary = f"(Previous conversation with {len(self.messages)} messages was compacted)"
262
+ self.messages = [{"role": "user", "content": summary}] + kept
@@ -0,0 +1,74 @@
1
+ """Authentication flow for Free.ai and BYOK providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from rich.console import Console
9
+ from rich.prompt import Prompt
10
+
11
+ from free_code.config import load_config, save_config, set_config_value
12
+
13
+ console = Console()
14
+
15
+
16
+ def check_auth(config: Optional[dict] = None) -> bool:
17
+ """Check if the user has valid authentication configured."""
18
+ if config is None:
19
+ config = load_config()
20
+ provider = config.get("provider", "free.ai")
21
+ if provider == "free.ai":
22
+ # Free.ai allows anonymous usage with daily limits
23
+ return True
24
+ # BYOK providers need an API key
25
+ return bool(config.get("api_key"))
26
+
27
+
28
+ def login_flow() -> None:
29
+ """Interactive login flow."""
30
+ config = load_config()
31
+ console.print()
32
+ console.print("[bold]Free.ai Coder — Login[/bold]")
33
+ console.print()
34
+
35
+ provider = Prompt.ask(
36
+ "Provider",
37
+ choices=["free.ai", "openai", "anthropic", "google", "openrouter"],
38
+ default="free.ai",
39
+ )
40
+
41
+ if provider == "free.ai":
42
+ console.print()
43
+ console.print("Free.ai offers free daily limits with no account required.")
44
+ console.print("For higher limits, get a token at [link=https://free.ai/pricing]https://free.ai/pricing[/link]")
45
+ console.print()
46
+ token = Prompt.ask("Free.ai token (press Enter to skip)", default="")
47
+ if token:
48
+ set_config_value("token", token)
49
+ console.print("[green]Token saved.[/green]")
50
+ else:
51
+ console.print("[dim]Using free anonymous tier.[/dim]")
52
+ set_config_value("provider", "free.ai")
53
+ else:
54
+ console.print()
55
+ key_name = "API key"
56
+ if provider == "openrouter":
57
+ console.print("Get an API key at [link=https://openrouter.ai/keys]https://openrouter.ai/keys[/link]")
58
+ elif provider == "openai":
59
+ console.print("Get an API key at [link=https://platform.openai.com/api-keys]https://platform.openai.com/api-keys[/link]")
60
+ elif provider == "anthropic":
61
+ console.print("Get an API key at [link=https://console.anthropic.com/settings/keys]https://console.anthropic.com/settings/keys[/link]")
62
+ elif provider == "google":
63
+ console.print("Get an API key at [link=https://aistudio.google.com/apikey]https://aistudio.google.com/apikey[/link]")
64
+
65
+ api_key = Prompt.ask(f"\n{key_name}")
66
+ if not api_key:
67
+ console.print("[red]API key is required for {provider}.[/red]")
68
+ sys.exit(1)
69
+
70
+ set_config_value("provider", provider)
71
+ set_config_value("api_key", api_key)
72
+ console.print(f"[green]Configured {provider} provider.[/green]")
73
+
74
+ console.print("[green]Login complete. Run [bold]free-code[/bold] to start coding.[/green]")