codenaut 0.1.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.
- agent/__init__.py +1 -0
- agent/agent.py +236 -0
- agent/events.py +145 -0
- agent/persistence.py +123 -0
- agent/session.py +99 -0
- client/__init__.py +1 -0
- client/llm_client.py +266 -0
- client/response.py +88 -0
- codenaut-0.1.0.dist-info/METADATA +199 -0
- codenaut-0.1.0.dist-info/RECORD +63 -0
- codenaut-0.1.0.dist-info/WHEEL +5 -0
- codenaut-0.1.0.dist-info/entry_points.txt +3 -0
- codenaut-0.1.0.dist-info/licenses/LICENSE +21 -0
- codenaut-0.1.0.dist-info/top_level.txt +12 -0
- config/__init__.py +1 -0
- config/config.py +173 -0
- config/loader.py +173 -0
- context/__init__.py +1 -0
- context/compaction.py +96 -0
- context/loop_detector.py +49 -0
- context/manager.py +257 -0
- hooks/__init__.py +1 -0
- hooks/hook_system.py +140 -0
- main.py +422 -0
- prompts/__init__.py +1 -0
- prompts/system.py +380 -0
- safety/__init__.py +1 -0
- safety/approval.py +157 -0
- skills/__init__.py +9 -0
- skills/discovery.py +97 -0
- skills/models.py +14 -0
- skills/registry.py +56 -0
- tools/__init__.py +1 -0
- tools/base.py +200 -0
- tools/builtin/__init__.py +67 -0
- tools/builtin/apply_patch.py +388 -0
- tools/builtin/edit_file.py +211 -0
- tools/builtin/glob.py +80 -0
- tools/builtin/grep.py +117 -0
- tools/builtin/list_dir.py +60 -0
- tools/builtin/memory.py +115 -0
- tools/builtin/read_file.py +133 -0
- tools/builtin/shell.py +160 -0
- tools/builtin/tasks.py +336 -0
- tools/builtin/web_fetch.py +55 -0
- tools/builtin/web_search.py +60 -0
- tools/builtin/write_file.py +111 -0
- tools/discovery.py +70 -0
- tools/mcp/__init__.py +1 -0
- tools/mcp/client.py +127 -0
- tools/mcp/mcp_manager.py +83 -0
- tools/mcp/mcp_tool.py +51 -0
- tools/registry.py +156 -0
- tools/subagents.py +148 -0
- ui/__init__.py +12 -0
- ui/completer.py +160 -0
- ui/prompt.py +65 -0
- ui/tui.py +669 -0
- utils/__init__.py +1 -0
- utils/errors.py +49 -0
- utils/file_tags.py +83 -0
- utils/paths.py +40 -0
- utils/text.py +79 -0
client/llm_client.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, AsyncGenerator
|
|
3
|
+
import httpx
|
|
4
|
+
from openai import APIConnectionError, APIError, AsyncOpenAI, RateLimitError
|
|
5
|
+
|
|
6
|
+
from client.response import (
|
|
7
|
+
StreamEventType,
|
|
8
|
+
StreamEvent,
|
|
9
|
+
TextDelta,
|
|
10
|
+
TokenUsage,
|
|
11
|
+
ToolCall,
|
|
12
|
+
ToolCallDelta,
|
|
13
|
+
parse_tool_call_arguments,
|
|
14
|
+
)
|
|
15
|
+
from config.config import Config
|
|
16
|
+
from langsmith import trace
|
|
17
|
+
|
|
18
|
+
class LLMClient:
|
|
19
|
+
def __init__(self, config: Config) -> None:
|
|
20
|
+
self._client: AsyncOpenAI | None = None
|
|
21
|
+
self._max_retries: int = 3
|
|
22
|
+
self.config = config
|
|
23
|
+
|
|
24
|
+
def get_client(self) -> AsyncOpenAI:
|
|
25
|
+
if self._client is None:
|
|
26
|
+
self._client = AsyncOpenAI(
|
|
27
|
+
api_key=self.config.api_key,
|
|
28
|
+
base_url=self.config.base_url,
|
|
29
|
+
# Fail fast on a stalled endpoint instead of hanging on the SDK
|
|
30
|
+
# default (~10 min). `read` bounds silence between streamed chunks
|
|
31
|
+
# (and time-to-first-token); `connect` bounds the initial connect.
|
|
32
|
+
timeout=httpx.Timeout(90.0, connect=15.0),
|
|
33
|
+
# We run our own retry loop below; leaving the SDK's default (2)
|
|
34
|
+
# would multiply every attempt and compound stalls.
|
|
35
|
+
max_retries=0,
|
|
36
|
+
)
|
|
37
|
+
return self._client
|
|
38
|
+
|
|
39
|
+
async def close(self) -> None:
|
|
40
|
+
if self._client:
|
|
41
|
+
await self._client.close()
|
|
42
|
+
self._client = None
|
|
43
|
+
|
|
44
|
+
def _build_tools(self, tools: list[dict[str, Any]]):
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
"type": "function",
|
|
48
|
+
"function": {
|
|
49
|
+
"name": tool["name"],
|
|
50
|
+
"description": tool.get("description", ""),
|
|
51
|
+
"parameters": tool.get(
|
|
52
|
+
"parameters",
|
|
53
|
+
{
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {},
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
for tool in tools
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
def _get_cached_tokens(self, usage: Any) -> int:
|
|
64
|
+
prompt_details = getattr(usage, "prompt_tokens_details", None)
|
|
65
|
+
if prompt_details is None:
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
cached_tokens = getattr(prompt_details, "cached_tokens", None)
|
|
69
|
+
return cached_tokens or 0
|
|
70
|
+
|
|
71
|
+
async def chat_completion(
|
|
72
|
+
self,
|
|
73
|
+
messages: list[dict[str, Any]],
|
|
74
|
+
tools: list[dict[str, Any]] | None = None,
|
|
75
|
+
stream: bool = True,
|
|
76
|
+
) -> AsyncGenerator[StreamEvent, None]:
|
|
77
|
+
|
|
78
|
+
with trace(
|
|
79
|
+
name="llm.chat_completion",
|
|
80
|
+
run_type="llm",
|
|
81
|
+
inputs={
|
|
82
|
+
"messages": messages,
|
|
83
|
+
"model": self.config.model_name,
|
|
84
|
+
"tools_count": len(tools) if tools else 0,
|
|
85
|
+
},
|
|
86
|
+
metadata={"model": self.config.model_name},
|
|
87
|
+
) as run:
|
|
88
|
+
client = self.get_client()
|
|
89
|
+
|
|
90
|
+
kwargs = {
|
|
91
|
+
"model": self.config.model_name,
|
|
92
|
+
"messages": messages,
|
|
93
|
+
"stream": stream,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if tools:
|
|
97
|
+
kwargs["tools"] = self._build_tools(tools)
|
|
98
|
+
kwargs["tool_choice"] = "auto"
|
|
99
|
+
|
|
100
|
+
for attempt in range(self._max_retries + 1):
|
|
101
|
+
try:
|
|
102
|
+
if stream:
|
|
103
|
+
async for event in self._stream_response(client, kwargs):
|
|
104
|
+
yield event
|
|
105
|
+
else:
|
|
106
|
+
event = await self._non_stream_response(client, kwargs)
|
|
107
|
+
yield event
|
|
108
|
+
return
|
|
109
|
+
except RateLimitError as e:
|
|
110
|
+
if attempt < self._max_retries:
|
|
111
|
+
wait_time = 2**attempt
|
|
112
|
+
await asyncio.sleep(wait_time)
|
|
113
|
+
else:
|
|
114
|
+
yield StreamEvent(
|
|
115
|
+
type=StreamEventType.ERROR,
|
|
116
|
+
error=f"Rate limit exceeded: {e}",
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
except APIConnectionError as e:
|
|
120
|
+
if attempt < self._max_retries:
|
|
121
|
+
wait_time = 2**attempt
|
|
122
|
+
await asyncio.sleep(wait_time)
|
|
123
|
+
else:
|
|
124
|
+
yield StreamEvent(
|
|
125
|
+
type=StreamEventType.ERROR,
|
|
126
|
+
error=f"Connection error: {e}",
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
except APIError as e:
|
|
130
|
+
yield StreamEvent(
|
|
131
|
+
type=StreamEventType.ERROR,
|
|
132
|
+
error=f"API error: {e}",
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
async def _stream_response(
|
|
137
|
+
self,
|
|
138
|
+
client: AsyncOpenAI,
|
|
139
|
+
kwargs: dict[str, Any],
|
|
140
|
+
) -> AsyncGenerator[StreamEvent, None]:
|
|
141
|
+
response = await client.chat.completions.create(**kwargs)
|
|
142
|
+
|
|
143
|
+
finish_reason: str | None = None
|
|
144
|
+
usage: TokenUsage | None = None
|
|
145
|
+
tool_calls: dict[int, dict[str, Any]] = {}
|
|
146
|
+
|
|
147
|
+
async for chunk in response:
|
|
148
|
+
if hasattr(chunk, "usage") and chunk.usage:
|
|
149
|
+
usage = TokenUsage(
|
|
150
|
+
prompt_tokens=chunk.usage.prompt_tokens,
|
|
151
|
+
completion_tokens=chunk.usage.completion_tokens,
|
|
152
|
+
total_tokens=chunk.usage.total_tokens,
|
|
153
|
+
cached_tokens=self._get_cached_tokens(chunk.usage),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if not chunk.choices:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
choice = chunk.choices[0]
|
|
160
|
+
delta = choice.delta
|
|
161
|
+
|
|
162
|
+
if choice.finish_reason:
|
|
163
|
+
finish_reason = choice.finish_reason
|
|
164
|
+
|
|
165
|
+
if delta.content:
|
|
166
|
+
yield StreamEvent(
|
|
167
|
+
type=StreamEventType.TEXT_DELTA,
|
|
168
|
+
text_delta=TextDelta(delta.content),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if delta.tool_calls:
|
|
172
|
+
for tool_call_delta in delta.tool_calls:
|
|
173
|
+
idx = tool_call_delta.index
|
|
174
|
+
|
|
175
|
+
if idx not in tool_calls:
|
|
176
|
+
tool_calls[idx] = {
|
|
177
|
+
"id": "",
|
|
178
|
+
"name": "",
|
|
179
|
+
"arguments": "",
|
|
180
|
+
"started": False,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
tool_call = tool_calls[idx]
|
|
184
|
+
if tool_call_delta.id:
|
|
185
|
+
tool_call["id"] = tool_call_delta.id
|
|
186
|
+
|
|
187
|
+
function = tool_call_delta.function
|
|
188
|
+
if function and function.name:
|
|
189
|
+
tool_call["name"] = function.name
|
|
190
|
+
|
|
191
|
+
if tool_call["name"] and not tool_call["started"]:
|
|
192
|
+
tool_call["started"] = True
|
|
193
|
+
yield StreamEvent(
|
|
194
|
+
type=StreamEventType.TOOL_CALL_START,
|
|
195
|
+
tool_call_delta=ToolCallDelta(
|
|
196
|
+
call_id=tool_call["id"],
|
|
197
|
+
name=tool_call["name"],
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if function and function.arguments:
|
|
202
|
+
tool_call["arguments"] += function.arguments
|
|
203
|
+
yield StreamEvent(
|
|
204
|
+
type=StreamEventType.TOOL_CALL_DELTA,
|
|
205
|
+
tool_call_delta=ToolCallDelta(
|
|
206
|
+
call_id=tool_call["id"],
|
|
207
|
+
name=tool_call["name"],
|
|
208
|
+
arguments_delta=function.arguments,
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for idx, tc in tool_calls.items():
|
|
213
|
+
yield StreamEvent(
|
|
214
|
+
type=StreamEventType.TOOL_CALL_COMPLETE,
|
|
215
|
+
tool_call=ToolCall(
|
|
216
|
+
call_id=tc["id"],
|
|
217
|
+
name=tc["name"],
|
|
218
|
+
arguments=parse_tool_call_arguments(tc["arguments"]),
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
yield StreamEvent(
|
|
223
|
+
type=StreamEventType.MESSAGE_COMPLETE,
|
|
224
|
+
finish_reason=finish_reason,
|
|
225
|
+
usage=usage,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
async def _non_stream_response(
|
|
229
|
+
self,
|
|
230
|
+
client: AsyncOpenAI,
|
|
231
|
+
kwargs: dict[str, Any],
|
|
232
|
+
) -> StreamEvent:
|
|
233
|
+
response = await client.chat.completions.create(**kwargs)
|
|
234
|
+
choice = response.choices[0]
|
|
235
|
+
message = choice.message
|
|
236
|
+
|
|
237
|
+
text_delta = None
|
|
238
|
+
if message.content:
|
|
239
|
+
text_delta = TextDelta(content=message.content)
|
|
240
|
+
|
|
241
|
+
tool_calls: list[ToolCall] = []
|
|
242
|
+
if message.tool_calls:
|
|
243
|
+
for tc in message.tool_calls:
|
|
244
|
+
tool_calls.append(
|
|
245
|
+
ToolCall(
|
|
246
|
+
call_id=tc.id,
|
|
247
|
+
name=tc.function.name,
|
|
248
|
+
arguments=parse_tool_call_arguments(tc.function.arguments),
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
usage = None
|
|
253
|
+
if response.usage:
|
|
254
|
+
usage = TokenUsage(
|
|
255
|
+
prompt_tokens=response.usage.prompt_tokens,
|
|
256
|
+
completion_tokens=response.usage.completion_tokens,
|
|
257
|
+
total_tokens=response.usage.total_tokens,
|
|
258
|
+
cached_tokens=self._get_cached_tokens(response.usage),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return StreamEvent(
|
|
262
|
+
type=StreamEventType.MESSAGE_COMPLETE,
|
|
263
|
+
text_delta=text_delta,
|
|
264
|
+
finish_reason=choice.finish_reason,
|
|
265
|
+
usage=usage,
|
|
266
|
+
)
|
client/response.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class TextDelta:
|
|
10
|
+
content: str
|
|
11
|
+
|
|
12
|
+
def __str__(self):
|
|
13
|
+
return self.content
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StreamEventType(str, Enum):
|
|
17
|
+
TEXT_DELTA = "text_delta"
|
|
18
|
+
MESSAGE_COMPLETE = "message_complete"
|
|
19
|
+
ERROR = "error"
|
|
20
|
+
|
|
21
|
+
TOOL_CALL_START = "tool_call_start"
|
|
22
|
+
TOOL_CALL_DELTA = "tool_call_delta"
|
|
23
|
+
TOOL_CALL_COMPLETE = "tool_call_complete"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TokenUsage:
|
|
28
|
+
prompt_tokens: int = 0
|
|
29
|
+
completion_tokens: int = 0
|
|
30
|
+
total_tokens: int = 0
|
|
31
|
+
cached_tokens: int = 0
|
|
32
|
+
|
|
33
|
+
def __add__(self, other: TokenUsage):
|
|
34
|
+
return TokenUsage(
|
|
35
|
+
prompt_tokens=self.prompt_tokens + other.prompt_tokens,
|
|
36
|
+
completion_tokens=self.completion_tokens + other.completion_tokens,
|
|
37
|
+
total_tokens=self.total_tokens + other.total_tokens,
|
|
38
|
+
cached_tokens=self.cached_tokens + other.cached_tokens,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ToolCallDelta:
|
|
44
|
+
call_id: str
|
|
45
|
+
name: str | None = None
|
|
46
|
+
arguments_delta: str = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ToolCall:
|
|
51
|
+
call_id: str
|
|
52
|
+
name: str | None = None
|
|
53
|
+
arguments: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class StreamEvent:
|
|
58
|
+
type: StreamEventType
|
|
59
|
+
text_delta: TextDelta | None = None
|
|
60
|
+
error: str | None = None
|
|
61
|
+
finish_reason: str | None = None
|
|
62
|
+
tool_call_delta: ToolCallDelta | None = None
|
|
63
|
+
tool_call: ToolCall | None = None
|
|
64
|
+
usage: TokenUsage | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ToolResultMessage:
|
|
69
|
+
tool_call_id: str
|
|
70
|
+
content: str
|
|
71
|
+
is_error: bool = False
|
|
72
|
+
|
|
73
|
+
def to_openai_message(self) -> dict[str, Any]:
|
|
74
|
+
return {
|
|
75
|
+
"role": "tool",
|
|
76
|
+
"tool_call_id": self.tool_call_id,
|
|
77
|
+
"content": self.content,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_tool_call_arguments(arguments_str: str) -> dict[str, Any]:
|
|
82
|
+
if not arguments_str:
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return json.loads(arguments_str)
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
return {"raw_arguments": arguments_str}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codenaut
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal-based OpenAI-compatible coding agent with tools, approvals, MCP, and session persistence.
|
|
5
|
+
Author: nick211908
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nick211908/coding-agent
|
|
8
|
+
Project-URL: Issues, https://github.com/nick211908/coding-agent/issues
|
|
9
|
+
Project-URL: Repository, https://github.com/nick211908/coding-agent
|
|
10
|
+
Keywords: agent,cli,coding,openai,mcp
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.13
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: click>=8.4.2
|
|
22
|
+
Requires-Dist: ddgs>=9.14.4
|
|
23
|
+
Requires-Dist: fastmcp>=3.4.2
|
|
24
|
+
Requires-Dist: httpx>=0.28.1
|
|
25
|
+
Requires-Dist: langsmith>=0.13.0
|
|
26
|
+
Requires-Dist: openai>=2.44.0
|
|
27
|
+
Requires-Dist: platformdirs>=4.10.0
|
|
28
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
29
|
+
Requires-Dist: pydantic>=2.13.4
|
|
30
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
31
|
+
Requires-Dist: rich>=14.2.0
|
|
32
|
+
Requires-Dist: tiktoken>=0.13.0
|
|
33
|
+
Requires-Dist: tomli>=2.4.1; python_version < "3.11"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pylint>=4.0.6; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# code-it
|
|
40
|
+
|
|
41
|
+
A terminal-based, OpenAI-compatible coding agent that reads, edits, and runs code in your project through a ReAct-style tool-calling loop with approvals, hooks, MCP, and session persistence.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
Prerequisites: Python 3.13+ and `uv`.
|
|
46
|
+
|
|
47
|
+
Install dependencies into the project-managed environment:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv sync
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Build distributable artifacts:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This creates:
|
|
60
|
+
|
|
61
|
+
- `dist/code_it-<version>-py3-none-any.whl`
|
|
62
|
+
- `dist/code_it-<version>.tar.gz`
|
|
63
|
+
|
|
64
|
+
## First Run
|
|
65
|
+
|
|
66
|
+
After syncing the project environment, configure credentials:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv run code-it --configure
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This prompts for:
|
|
73
|
+
|
|
74
|
+
- your API key
|
|
75
|
+
- an optional base URL for any OpenAI-compatible provider
|
|
76
|
+
|
|
77
|
+
The values are saved in your user config file at:
|
|
78
|
+
|
|
79
|
+
- Windows: `%APPDATA%\code-it\config.toml`
|
|
80
|
+
- macOS: `~/Library/Application Support/code-it/config.toml`
|
|
81
|
+
- Linux: `~/.config/code-it/config.toml`
|
|
82
|
+
|
|
83
|
+
You can still use a project-local `.env` file or `API_KEY` and `BASE_URL` environment variables, but they are no longer required for packaged installs.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
Run interactively:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv run code-it
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run a single prompt:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv run code-it "Summarize what this repo does"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Target a specific working directory:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv run code-it --cwd /path/to/project "Find and fix the failing test"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Update saved credentials later:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uv run code-it --configure
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Inside an interactive session:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
[user]> /setup # update saved API credentials
|
|
115
|
+
[user]> /tools # list available tools
|
|
116
|
+
[user]> /mcp # list connected MCP servers
|
|
117
|
+
[user]> /approval auto-edit # change the approval policy
|
|
118
|
+
[user]> /save # save the current session
|
|
119
|
+
[user]> /sessions # list saved sessions
|
|
120
|
+
[user]> /resume <session_id> # resume a saved session
|
|
121
|
+
[user]> /checkpoint # snapshot the current session
|
|
122
|
+
[user]> /stats # show token/turn usage stats
|
|
123
|
+
[user]> /exit # quit
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Project Config
|
|
127
|
+
|
|
128
|
+
Project-level overrides still live in `.ai-agent/config.toml`.
|
|
129
|
+
|
|
130
|
+
You can also add:
|
|
131
|
+
|
|
132
|
+
- `AGENT.MD` for project-specific developer instructions
|
|
133
|
+
- `.ai-agent/skills/<skill-name>/SKILL.md` for reusable skills
|
|
134
|
+
- `.ai-agent/tools/*.py` for custom tools
|
|
135
|
+
|
|
136
|
+
## Publish Checklist
|
|
137
|
+
|
|
138
|
+
1. Bump `version` in `pyproject.toml`.
|
|
139
|
+
2. Run tests.
|
|
140
|
+
3. Build the wheel and sdist with `uv build --no-sources`.
|
|
141
|
+
4. Test the CLI with `uv run code-it --configure`.
|
|
142
|
+
5. Upload to TestPyPI.
|
|
143
|
+
6. Verify both `uv run code-it --configure` and `uv run code-it` work.
|
|
144
|
+
7. Publish to PyPI.
|
|
145
|
+
|
|
146
|
+
## Publish With uv
|
|
147
|
+
|
|
148
|
+
Recommended local release flow:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uv sync --frozen
|
|
152
|
+
uv run python -m unittest tests.test_config_loader tests.test_skills
|
|
153
|
+
uv build --no-sources
|
|
154
|
+
uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
|
|
155
|
+
uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
TestPyPI publish:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
$env:UV_PUBLISH_TOKEN="pypi-***"
|
|
162
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
PyPI publish:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
$env:UV_PUBLISH_TOKEN="pypi-***"
|
|
169
|
+
uv publish
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
For the first release, verify the package name on PyPI before publishing. If `code-it` is already taken, rename `project.name` in `pyproject.toml` before uploading.
|
|
173
|
+
|
|
174
|
+
## Trusted Publishing
|
|
175
|
+
|
|
176
|
+
A GitHub Actions release workflow is included at `.github/workflows/publish.yml`.
|
|
177
|
+
|
|
178
|
+
To use it:
|
|
179
|
+
|
|
180
|
+
1. Create the project on PyPI.
|
|
181
|
+
2. In the PyPI project settings, add a Trusted Publisher for this GitHub repository.
|
|
182
|
+
3. In GitHub, create an environment named `pypi`.
|
|
183
|
+
4. Push a version tag like `v0.1.0`.
|
|
184
|
+
|
|
185
|
+
That workflow will:
|
|
186
|
+
|
|
187
|
+
- sync with `uv`
|
|
188
|
+
- run the unit tests
|
|
189
|
+
- build the wheel and source distribution
|
|
190
|
+
- smoke-test both artifacts
|
|
191
|
+
- publish with `uv publish`
|
|
192
|
+
|
|
193
|
+
## Why `uv` Instead of `pip install -e .`
|
|
194
|
+
|
|
195
|
+
This repository already carries a `uv.lock`, so `uv sync` is the safer default for development. It avoids polluting a shared Python installation and reduces resolver conflicts with unrelated packages already installed on your machine.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
Distributed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
main.py,sha256=LFHn2Ov49-zNQqu3AX24UrH2PGXbNPwcnUs1qtgd7PM,17759
|
|
2
|
+
agent/__init__.py,sha256=nZIx5ApwFdMjKOQPtS-4qR5I8UA6LRlu4rqFAErAgcA,34
|
|
3
|
+
agent/agent.py,sha256=3BpqyIVlZwn5fUjk4eUpKGYAnh8VHWmp6YtvJj9oJfU,8782
|
|
4
|
+
agent/events.py,sha256=WYJ5vcaZ3ItVOjyXla1TWrKb45KN8zpIK965VZUsxks,3974
|
|
5
|
+
agent/persistence.py,sha256=8JK1fCzEHVLJC10DPkb1Amlg_Hn9ASGwjdeMoN96zhU,4165
|
|
6
|
+
agent/session.py,sha256=3Pq0muVZY85pr6a6PLohyc42BhfD6yZt2PFfCNJXRZE,3411
|
|
7
|
+
client/__init__.py,sha256=M4ahv6R9Kj3qD4JBpyUPgpjHnGy1riJM_iTn79Jw6Ig,31
|
|
8
|
+
client/llm_client.py,sha256=eT2zlCWfQd7ea56IM61nhxSVH8oIOyn-IpypVmZvySY,9693
|
|
9
|
+
client/response.py,sha256=K6tSQalNHxnr3KrpawYd93Clsr9yd8LOdLkE4W3lHDQ,2107
|
|
10
|
+
codenaut-0.1.0.dist-info/licenses/LICENSE,sha256=1LZfjHOqkRejkZyYAe7R1xGMZprN0ExOIaM-df7gYY4,1067
|
|
11
|
+
config/__init__.py,sha256=FXE68WpVrO9ePz1Hnd9ASfk4wXYD2dF6vzbeKvmm0bI,40
|
|
12
|
+
config/config.py,sha256=Yi1vpfOfEzv4dQ9qcQ807cZr3poBvJ7UhShRd1FIxUA,4928
|
|
13
|
+
config/loader.py,sha256=cS2uFa5XL90w59nKkfYgNLrAH4JP0yMKQfMpfAL4fug,5123
|
|
14
|
+
context/__init__.py,sha256=g7OlaNtUW3JOROZ8rYPjVZum6oVlr5icfNO07N6xJUg,39
|
|
15
|
+
context/compaction.py,sha256=vBS2qcMqKFcpEmCKj4cvwbmF1XxQw3EeF-44RoEytTs,3437
|
|
16
|
+
context/loop_detector.py,sha256=dTyxIIZEijjrs1chScexUu7Dlb2TAmEkOwc_D2Ac_lk,1613
|
|
17
|
+
context/manager.py,sha256=L-6Vs_5vV09PkzWrpnTwvaYNgzmK5fWRqm0xO1EeWPA,7934
|
|
18
|
+
hooks/__init__.py,sha256=2qnpTS65dGZvePaSWNu-J0azm4YNSYdbw3WCEKpmikY,30
|
|
19
|
+
hooks/hook_system.py,sha256=gb1QVUFiCEpSVdQIH7zsOEKbZt1w-TovY4P_2O200Xw,4394
|
|
20
|
+
prompts/__init__.py,sha256=oHvySSUnV3dY00xMgRyLocCXhQrTcm0lXMGONiKONh4,37
|
|
21
|
+
prompts/system.py,sha256=u5dy-e1iFwZLwyKdk7Ped9oOcHXsVI67sqjVKXNc4UU,20413
|
|
22
|
+
safety/__init__.py,sha256=OY1s9BJe2zBTV0P5Rr5TsIoFPLza_tvzl7ERl6taZ64,36
|
|
23
|
+
safety/approval.py,sha256=t00EzL5AoGz1C8wow5ELvPMRYQDANd6eY52x4yM_0Gs,4613
|
|
24
|
+
skills/__init__.py,sha256=t_e173VDOEU9SLxy20dYG3-mVgPuyGZdPvs3o_90wGs,223
|
|
25
|
+
skills/discovery.py,sha256=AgxBMNRgjGV1riHFCOs0WyVD6JK6vACjUs3uXdnPFWM,3313
|
|
26
|
+
skills/models.py,sha256=IfsqiZh8cEkii6sF_tS7C6f4juNOZVG-4snYP3hPlBc,318
|
|
27
|
+
skills/registry.py,sha256=SEbphwtXp8ER20x66gPu0W2iKgYDjmz9NPN5ijSrpWA,1819
|
|
28
|
+
tools/__init__.py,sha256=-l-DUD3QxdEYoGwI37vjwsKulVxt8m9NsLr3UJKCrzs,44
|
|
29
|
+
tools/base.py,sha256=76YetOiyB_2m4HG4rVhVrtGlcoAS4WiaHBf1EwKTcWE,5506
|
|
30
|
+
tools/discovery.py,sha256=xBI3StV_JuWy6FJQ0RhfVxj2fANCnJ6cGWXxga0j5dc,2174
|
|
31
|
+
tools/registry.py,sha256=-GzxUcWZaYJ5JIlRKBAWKDowSzN9vyhwJoOxbj2Ue6U,5302
|
|
32
|
+
tools/subagents.py,sha256=sG3K6zSmC09bHLHivJwJxObkVn_RHjCp034b0CMZsdg,5051
|
|
33
|
+
tools/builtin/__init__.py,sha256=JsemuTRhOaHnOSTebFKBQ_xjg6hBEpc5YImnqxCRrpo,1576
|
|
34
|
+
tools/builtin/apply_patch.py,sha256=VxhX8hYeJAF3xYxMzIKaEgrK68dGyG4M0UHunoVhQ0w,12272
|
|
35
|
+
tools/builtin/edit_file.py,sha256=tBQDO_q3Hbr9PZ7Q2361D9eQLSrqXfwaxoNB14_WgSA,7229
|
|
36
|
+
tools/builtin/glob.py,sha256=ed_fVL2bB6A8DmbqbXkmoCqhfYOQNz-3DY9dNeH74yI,2425
|
|
37
|
+
tools/builtin/grep.py,sha256=jClE2-uICi7onANmAyxvgfUbEhppHNnc0ioHYi1ltDw,3690
|
|
38
|
+
tools/builtin/list_dir.py,sha256=xxJ6smvuVE8qeOR5CRPgiu2aM-oN-w5ifUn8NEumKkI,1818
|
|
39
|
+
tools/builtin/memory.py,sha256=PvSayh_zikx47Dny05ZVGWswpgTpt-vYrmwprcnmX-o,4247
|
|
40
|
+
tools/builtin/read_file.py,sha256=X3OBTNNDFRCX-3n6LX79WykENy67rht7VDTVUs372nE,4341
|
|
41
|
+
tools/builtin/shell.py,sha256=BGKWHkzEaf6GSRTu8K1o9yw8v2_MFp7OD5ZMIqcnNVk,4822
|
|
42
|
+
tools/builtin/tasks.py,sha256=IChW-WpDYy-JyyvvgE4LzQFwOwTp4h7Ci4hfaV6RqeY,12526
|
|
43
|
+
tools/builtin/web_fetch.py,sha256=ooPzI8fBruEWeT6n7No4A5HGC6WYPXx0eSUn_PaAXpo,1824
|
|
44
|
+
tools/builtin/web_search.py,sha256=PD5upTRAN2xZJ_dnRX8ZO28sOwSwd-yrCIey0Fda1VY,1821
|
|
45
|
+
tools/builtin/write_file.py,sha256=07QtMR9LqVXN-G5Rtfus2WPPkMlQbnKhyY4QTJS26YE,3495
|
|
46
|
+
tools/mcp/__init__.py,sha256=hruMBtSTRDIsC5MA83-BLdaQzjy_LJKKq1OlGzxrmU8,39
|
|
47
|
+
tools/mcp/client.py,sha256=TwjIZr_55qnmy1r_aWNVgM-4Ng-B-9WSoE0w9jmWVkQ,3855
|
|
48
|
+
tools/mcp/mcp_manager.py,sha256=5FbntJ-C0BXNHmRA3q_C53Us7GT_yuXWUOjmrM0D-co,2399
|
|
49
|
+
tools/mcp/mcp_tool.py,sha256=yuWNeJ2L6fWy0afi7Kszzv9v4GNQWNVLqp5eOCvMfzM,1498
|
|
50
|
+
ui/__init__.py,sha256=iHQwkfGYAYTxR3tVv6TK-k6V-KZ-LufnUx0TspOlgcw,252
|
|
51
|
+
ui/completer.py,sha256=K9ElFxbmDgYZTTTGCXIn8VYjjZz82z3dnnE73Dh_85U,5246
|
|
52
|
+
ui/prompt.py,sha256=KrUQaQUFX-1cUfBy1SFKQrLcJWEihBRpf0D0hpkA83Y,2172
|
|
53
|
+
ui/tui.py,sha256=Lwn9ZpXXC0i2-NNBq5QfFM1-8oGdC0kUmL_X5ETlUzw,21836
|
|
54
|
+
utils/__init__.py,sha256=ZzhtEkM5EYuz8Oi6eOevMF4Lhyaz30f4Bh7exMBuByo,30
|
|
55
|
+
utils/errors.py,sha256=TGafz7_2XvFqJJKhU9d8gUVHLGkbFCZ9sNKDZpwLPOA,1429
|
|
56
|
+
utils/file_tags.py,sha256=vp5HRagPNNRxt1p3ZgDLcck3nI70gtJXoc8iXTY23QM,2876
|
|
57
|
+
utils/paths.py,sha256=UWhZu9pgrl--ganIBM3UVCZ1xRsKoxKbmWfSKaMCnrw,834
|
|
58
|
+
utils/text.py,sha256=Ja4ROXd9wPeyjxssSkVddHHGnwXyFJxCVrDPFtftvTQ,2077
|
|
59
|
+
codenaut-0.1.0.dist-info/METADATA,sha256=iiaWDohrNywMHQPBrqq-XbExp3X2qGZn338ONlvDx_w,5644
|
|
60
|
+
codenaut-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
61
|
+
codenaut-0.1.0.dist-info/entry_points.txt,sha256=wrn5K1tYT1aVSP-KckqtwtobA9oISGRWfK5axMwOkKI,59
|
|
62
|
+
codenaut-0.1.0.dist-info/top_level.txt,sha256=rCQsbocwJaqy1bpr0B0YuJNWQd31KXCmv1OjIquEWus,76
|
|
63
|
+
codenaut-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nick211908
|
|
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.
|
config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration models and loaders."""
|