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.
- freeai_code-0.1.0/LICENSE +21 -0
- freeai_code-0.1.0/PKG-INFO +109 -0
- freeai_code-0.1.0/README.md +79 -0
- freeai_code-0.1.0/free_code/__init__.py +3 -0
- freeai_code-0.1.0/free_code/__main__.py +6 -0
- freeai_code-0.1.0/free_code/agent.py +262 -0
- freeai_code-0.1.0/free_code/auth.py +74 -0
- freeai_code-0.1.0/free_code/cli.py +305 -0
- freeai_code-0.1.0/free_code/client.py +308 -0
- freeai_code-0.1.0/free_code/config.py +125 -0
- freeai_code-0.1.0/free_code/context/__init__.py +1 -0
- freeai_code-0.1.0/free_code/context/discovery.py +140 -0
- freeai_code-0.1.0/free_code/context/repo_map.py +86 -0
- freeai_code-0.1.0/free_code/context/window.py +169 -0
- freeai_code-0.1.0/free_code/models.py +56 -0
- freeai_code-0.1.0/free_code/streaming.py +81 -0
- freeai_code-0.1.0/free_code/tools/__init__.py +158 -0
- freeai_code-0.1.0/free_code/tools/file_ops.py +112 -0
- freeai_code-0.1.0/free_code/tools/git_ops.py +102 -0
- freeai_code-0.1.0/free_code/tools/list_files.py +107 -0
- freeai_code-0.1.0/free_code/tools/search.py +94 -0
- freeai_code-0.1.0/free_code/tools/shell.py +95 -0
- freeai_code-0.1.0/free_code/tools/test_runner.py +131 -0
- freeai_code-0.1.0/free_code/ui/__init__.py +1 -0
- freeai_code-0.1.0/free_code/ui/diff_view.py +55 -0
- freeai_code-0.1.0/free_code/ui/prompt.py +85 -0
- freeai_code-0.1.0/free_code/ui/terminal.py +118 -0
- freeai_code-0.1.0/freeai_code.egg-info/PKG-INFO +109 -0
- freeai_code-0.1.0/freeai_code.egg-info/SOURCES.txt +33 -0
- freeai_code-0.1.0/freeai_code.egg-info/dependency_links.txt +1 -0
- freeai_code-0.1.0/freeai_code.egg-info/entry_points.txt +2 -0
- freeai_code-0.1.0/freeai_code.egg-info/requires.txt +6 -0
- freeai_code-0.1.0/freeai_code.egg-info/top_level.txt +1 -0
- freeai_code-0.1.0/pyproject.toml +43 -0
- 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,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]")
|