ollama-coding-agent 0.5.0__tar.gz → 0.5.2__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.
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/PKG-INFO +2 -1
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/cli.py +63 -46
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/config.py +4 -1
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/mcp_server.py +55 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/tools.py +133 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/ui.py +13 -3
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/PKG-INFO +2 -1
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/requires.txt +1 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/pyproject.toml +2 -1
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/LICENSE +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/PYPI_README.md +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/README.md +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/__init__.py +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/__main__.py +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/agent.py +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/intent.py +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/mcp_client.py +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/ollama_client.py +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/coding_agent/session_store.py +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/SOURCES.txt +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/dependency_links.txt +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/entry_points.txt +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/top_level.txt +0 -0
- {ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ollama-coding-agent
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/HarryChen1995/coding_agent
|
|
@@ -17,6 +17,7 @@ Requires-Dist: httpx>=0.28
|
|
|
17
17
|
Requires-Dist: rich>=15.0
|
|
18
18
|
Requires-Dist: typer>=0.27
|
|
19
19
|
Requires-Dist: mcp>=1.27
|
|
20
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
20
21
|
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# Ollama Coding Agent
|
|
@@ -32,6 +32,7 @@ Examples:
|
|
|
32
32
|
|
|
33
33
|
import asyncio
|
|
34
34
|
import os
|
|
35
|
+
from contextlib import nullcontext
|
|
35
36
|
from typing import List, Optional
|
|
36
37
|
|
|
37
38
|
import typer
|
|
@@ -220,61 +221,78 @@ def main(
|
|
|
220
221
|
async def _interactive(cfg: AgentConfig, resume: Optional[str], session_name: Optional[str]):
|
|
221
222
|
"""REPL: keep one MCP client open across turns (avoids re-spawning the
|
|
222
223
|
tool-server subprocess every turn) and keep resuming the same session
|
|
223
|
-
(fresh on turn 1, then whatever session that turn created/resumed).
|
|
224
|
+
(fresh on turn 1, then whatever session that turn created/resumed).
|
|
225
|
+
|
|
226
|
+
Input is read through a prompt_toolkit PromptSession wrapped in
|
|
227
|
+
patch_stdout(), so the input line stays pinned to the bottom of the
|
|
228
|
+
terminal — parsing/thinking spinners, panels, and results all scroll in
|
|
229
|
+
the region above it instead of interleaving with the prompt. Falls back
|
|
230
|
+
to a plain input() loop if rich/prompt_toolkit aren't installed."""
|
|
224
231
|
agent = CodingAgent(cfg)
|
|
225
232
|
session_id = resume
|
|
226
233
|
|
|
227
234
|
try:
|
|
228
235
|
from . import ui
|
|
236
|
+
from prompt_toolkit import PromptSession
|
|
237
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
229
238
|
ui.interactive_banner(cfg.model, resumed=resume)
|
|
239
|
+
prompt_session = PromptSession()
|
|
240
|
+
# raw=True: pass Rich's ANSI-coded output straight through instead of
|
|
241
|
+
# patch_stdout()'s default write() path, which sanitizes/escapes text
|
|
242
|
+
# (it assumes plain text) and mangles embedded escape codes into
|
|
243
|
+
# literal garbage like "?[32m" on the screen.
|
|
244
|
+
stdout_cm = patch_stdout(raw=True)
|
|
230
245
|
except ImportError:
|
|
231
246
|
typer.echo(f"Interactive mode (model: {cfg.model}). Type a task, /sessions to list, /exit to quit.\n")
|
|
247
|
+
prompt_session = None
|
|
248
|
+
stdout_cm = nullcontext()
|
|
232
249
|
|
|
233
250
|
if resume:
|
|
234
251
|
_show_resumed_history(cfg.db_path, resume)
|
|
235
252
|
|
|
236
253
|
async with MCPToolClient(cfg.project_root, mcp_config_path=cfg.mcp_config_path or None,
|
|
237
254
|
extra_servers=cfg.mcp_servers or None) as client:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
session_id
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
255
|
+
with stdout_cm:
|
|
256
|
+
while True:
|
|
257
|
+
try:
|
|
258
|
+
task = await _read_task(prompt_session)
|
|
259
|
+
except (EOFError, KeyboardInterrupt):
|
|
260
|
+
typer.echo()
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
task = task.strip()
|
|
264
|
+
if not task:
|
|
265
|
+
continue
|
|
266
|
+
if task in ("/exit", "/quit"):
|
|
267
|
+
break
|
|
268
|
+
if task == "/sessions":
|
|
269
|
+
_print_sessions(agent.store.list_sessions())
|
|
270
|
+
continue
|
|
271
|
+
if task.startswith("/delete "):
|
|
272
|
+
target = task[len("/delete "):].strip()
|
|
273
|
+
if agent.store.delete_session(target):
|
|
274
|
+
typer.echo(f"Deleted session {target!r}.")
|
|
275
|
+
if session_id is not None and agent.store.resolve_session_id(session_id) is None:
|
|
276
|
+
session_id = None # the session we were resuming just got deleted
|
|
277
|
+
else:
|
|
278
|
+
typer.echo(f"No session found with id or name {target!r}.", err=True)
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
result = await agent.run(task, resume_session_id=session_id, client=client,
|
|
283
|
+
session_name=session_name, show_banner=False)
|
|
284
|
+
except ValueError as e:
|
|
285
|
+
typer.echo(f"Error: {e}", err=True)
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
session_id = agent.session_id
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
from . import ui
|
|
292
|
+
ui.final_result(result)
|
|
293
|
+
except ImportError:
|
|
294
|
+
typer.echo("\n=== RESULT ===")
|
|
295
|
+
typer.echo(result)
|
|
278
296
|
|
|
279
297
|
|
|
280
298
|
def _show_resumed_history(db_path: str, resume: str):
|
|
@@ -298,12 +316,11 @@ def _show_resumed_history(db_path: str, resume: str):
|
|
|
298
316
|
typer.echo("--- end history ---\n")
|
|
299
317
|
|
|
300
318
|
|
|
301
|
-
def _read_task() -> str:
|
|
302
|
-
|
|
319
|
+
async def _read_task(prompt_session) -> str:
|
|
320
|
+
if prompt_session is not None:
|
|
303
321
|
from . import ui
|
|
304
|
-
return ui.
|
|
305
|
-
|
|
306
|
-
return input("> ")
|
|
322
|
+
return await ui.prompt_task_async(prompt_session)
|
|
323
|
+
return input("> ")
|
|
307
324
|
|
|
308
325
|
|
|
309
326
|
def _print_sessions(sessions: list):
|
|
@@ -17,7 +17,10 @@ class AgentConfig:
|
|
|
17
17
|
# Anything not listed here (write_file, edit_file, run_shell by default)
|
|
18
18
|
# will print what it's about to do and wait for confirmation, unless
|
|
19
19
|
# auto_approve=True.
|
|
20
|
-
safe_tools: tuple = (
|
|
20
|
+
safe_tools: tuple = (
|
|
21
|
+
"read_file", "list_dir", "search_files", "glob_files",
|
|
22
|
+
"git_diff", "git_status", "git_log", "git_show", "git_branch", "git_fetch",
|
|
23
|
+
)
|
|
21
24
|
|
|
22
25
|
auto_approve: bool = False # True = never prompt (use in CI with care)
|
|
23
26
|
max_steps: int = 25 # hard cap on agent loop iterations
|
|
@@ -59,6 +59,61 @@ def git_diff(path: str = ".") -> str:
|
|
|
59
59
|
return impl.git_diff(path)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
@mcp.tool()
|
|
63
|
+
def git_status(path: str = ".") -> str:
|
|
64
|
+
"""Show the working tree status (short format) and current branch."""
|
|
65
|
+
return impl.git_status(path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def git_log(path: str = ".", max_count: int = 20) -> str:
|
|
70
|
+
"""Show recent commit history (hash, date, author, subject)."""
|
|
71
|
+
return impl.git_log(path, max_count)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
def git_show(ref: str = "HEAD", path: str = ".") -> str:
|
|
76
|
+
"""Show a commit's metadata and diff (defaults to HEAD)."""
|
|
77
|
+
return impl.git_show(ref, path)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@mcp.tool()
|
|
81
|
+
def git_branch(path: str = ".") -> str:
|
|
82
|
+
"""List local branches, marking the current one and its upstream tracking info."""
|
|
83
|
+
return impl.git_branch(path)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@mcp.tool()
|
|
87
|
+
def git_fetch(remote: str = "origin") -> str:
|
|
88
|
+
"""Update remote-tracking refs from a remote without touching the working tree."""
|
|
89
|
+
return impl.git_fetch(remote)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@mcp.tool()
|
|
93
|
+
def git_add(paths: str = ".") -> str:
|
|
94
|
+
"""Stage files for commit. `paths` is a space-separated list of file \
|
|
95
|
+
paths relative to the project root, or "." to stage all changes."""
|
|
96
|
+
return impl.git_add(paths)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@mcp.tool()
|
|
100
|
+
def git_commit(message: str) -> str:
|
|
101
|
+
"""Commit staged changes with the given commit message."""
|
|
102
|
+
return impl.git_commit(message)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def git_pull(remote: str = "origin", branch: str = "") -> str:
|
|
107
|
+
"""Fetch and merge from a remote into the current branch."""
|
|
108
|
+
return impl.git_pull(remote, branch)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@mcp.tool()
|
|
112
|
+
def git_push(remote: str = "origin", branch: str = "") -> str:
|
|
113
|
+
"""Push commits to a remote (defaults to pushing the current branch to origin)."""
|
|
114
|
+
return impl.git_push(remote, branch)
|
|
115
|
+
|
|
116
|
+
|
|
62
117
|
@mcp.tool()
|
|
63
118
|
def write_file(path: str, content: str, overwrite: bool = False) -> str:
|
|
64
119
|
"""Create a NEW file with content. Fails if the file already exists
|
|
@@ -157,8 +157,141 @@ class Tools:
|
|
|
157
157
|
except Exception as e:
|
|
158
158
|
return f"ERROR: {e}"
|
|
159
159
|
|
|
160
|
+
def git_status(self, path: str = ".") -> str:
|
|
161
|
+
p = _resolve_in_scope(self.cfg.project_root, path)
|
|
162
|
+
try:
|
|
163
|
+
result = subprocess.run(
|
|
164
|
+
["git", "status", "--short", "--branch"], cwd=p, capture_output=True, text=True, timeout=15,
|
|
165
|
+
)
|
|
166
|
+
return _truncate(result.stdout or "(clean)", self.cfg.max_output_chars)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
return f"ERROR: {e}"
|
|
169
|
+
|
|
170
|
+
def git_log(self, path: str = ".", max_count: int = 20) -> str:
|
|
171
|
+
p = _resolve_in_scope(self.cfg.project_root, path)
|
|
172
|
+
try:
|
|
173
|
+
result = subprocess.run(
|
|
174
|
+
["git", "log", f"-{max_count}", "--pretty=format:%h %ad %an: %s", "--date=short"],
|
|
175
|
+
cwd=p, capture_output=True, text=True, timeout=15,
|
|
176
|
+
)
|
|
177
|
+
if result.returncode != 0:
|
|
178
|
+
return f"ERROR: {result.stderr.strip()}"
|
|
179
|
+
return _truncate(result.stdout or "(no commits)", self.cfg.max_output_chars)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return f"ERROR: {e}"
|
|
182
|
+
|
|
183
|
+
def git_show(self, ref: str = "HEAD", path: str = ".") -> str:
|
|
184
|
+
p = _resolve_in_scope(self.cfg.project_root, path)
|
|
185
|
+
try:
|
|
186
|
+
result = subprocess.run(
|
|
187
|
+
["git", "show", ref], cwd=p, capture_output=True, text=True, timeout=15,
|
|
188
|
+
)
|
|
189
|
+
if result.returncode != 0:
|
|
190
|
+
return f"ERROR: {result.stderr.strip()}"
|
|
191
|
+
return _truncate(result.stdout, self.cfg.max_output_chars)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
return f"ERROR: {e}"
|
|
194
|
+
|
|
195
|
+
def git_branch(self, path: str = ".") -> str:
|
|
196
|
+
p = _resolve_in_scope(self.cfg.project_root, path)
|
|
197
|
+
try:
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
["git", "branch", "-vv"], cwd=p, capture_output=True, text=True, timeout=15,
|
|
200
|
+
)
|
|
201
|
+
if result.returncode != 0:
|
|
202
|
+
return f"ERROR: {result.stderr.strip()}"
|
|
203
|
+
return _truncate(result.stdout or "(no branches)", self.cfg.max_output_chars)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return f"ERROR: {e}"
|
|
206
|
+
|
|
207
|
+
def git_fetch(self, remote: str = "origin") -> str:
|
|
208
|
+
"""Update remote-tracking refs without touching the working tree —
|
|
209
|
+
safe to run without approval, unlike pull/push."""
|
|
210
|
+
try:
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
["git", "fetch", remote], cwd=self.cfg.project_root,
|
|
213
|
+
capture_output=True, text=True, timeout=self.cfg.shell_timeout_s,
|
|
214
|
+
)
|
|
215
|
+
out = f"exit_code: {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
216
|
+
return _truncate(out, self.cfg.max_output_chars)
|
|
217
|
+
except subprocess.TimeoutExpired:
|
|
218
|
+
return f"ERROR: git fetch timed out after {self.cfg.shell_timeout_s}s"
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return f"ERROR: {e}"
|
|
221
|
+
|
|
160
222
|
# ---- write tools (require approval unless auto_approve) ----
|
|
161
223
|
|
|
224
|
+
def git_add(self, paths: str = ".") -> str:
|
|
225
|
+
"""Stage files for commit. `paths` is a space-separated list of file
|
|
226
|
+
paths relative to the project root, or "." to stage all changes."""
|
|
227
|
+
if paths.strip() == ".":
|
|
228
|
+
args = ["git", "add", "-A"]
|
|
229
|
+
else:
|
|
230
|
+
resolved = []
|
|
231
|
+
for part in paths.split():
|
|
232
|
+
p = _resolve_in_scope(self.cfg.project_root, part)
|
|
233
|
+
resolved.append(os.path.relpath(p, self.cfg.project_root))
|
|
234
|
+
args = ["git", "add"] + resolved
|
|
235
|
+
try:
|
|
236
|
+
result = subprocess.run(
|
|
237
|
+
args, cwd=self.cfg.project_root, capture_output=True, text=True, timeout=15,
|
|
238
|
+
)
|
|
239
|
+
if result.returncode != 0:
|
|
240
|
+
return f"ERROR: {result.stderr.strip()}"
|
|
241
|
+
return f"Staged: {paths}"
|
|
242
|
+
except Exception as e:
|
|
243
|
+
return f"ERROR: {e}"
|
|
244
|
+
|
|
245
|
+
def git_commit(self, message: str) -> str:
|
|
246
|
+
if not message.strip():
|
|
247
|
+
return "ERROR: commit message cannot be empty"
|
|
248
|
+
try:
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
["git", "commit", "-m", message], cwd=self.cfg.project_root,
|
|
251
|
+
capture_output=True, text=True, timeout=15,
|
|
252
|
+
)
|
|
253
|
+
out = f"exit_code: {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
254
|
+
return _truncate(out, self.cfg.max_output_chars)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
return f"ERROR: {e}"
|
|
257
|
+
|
|
258
|
+
def git_pull(self, remote: str = "origin", branch: str = "") -> str:
|
|
259
|
+
"""Fetch and merge from a remote into the current branch. Touches
|
|
260
|
+
the working tree, so this always requires approval (never auto_approve
|
|
261
|
+
-exempt like the read-only git tools)."""
|
|
262
|
+
args = ["git", "pull", remote]
|
|
263
|
+
if branch:
|
|
264
|
+
args.append(branch)
|
|
265
|
+
try:
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
args, cwd=self.cfg.project_root, capture_output=True, text=True,
|
|
268
|
+
timeout=self.cfg.shell_timeout_s,
|
|
269
|
+
)
|
|
270
|
+
out = f"exit_code: {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
271
|
+
return _truncate(out, self.cfg.max_output_chars)
|
|
272
|
+
except subprocess.TimeoutExpired:
|
|
273
|
+
return f"ERROR: git pull timed out after {self.cfg.shell_timeout_s}s"
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return f"ERROR: {e}"
|
|
276
|
+
|
|
277
|
+
def git_push(self, remote: str = "origin", branch: str = "") -> str:
|
|
278
|
+
"""Push commits to a remote. No force-push option is exposed —
|
|
279
|
+
this tool only ever does a plain push."""
|
|
280
|
+
args = ["git", "push", remote]
|
|
281
|
+
if branch:
|
|
282
|
+
args.append(branch)
|
|
283
|
+
try:
|
|
284
|
+
result = subprocess.run(
|
|
285
|
+
args, cwd=self.cfg.project_root, capture_output=True, text=True,
|
|
286
|
+
timeout=self.cfg.shell_timeout_s,
|
|
287
|
+
)
|
|
288
|
+
out = f"exit_code: {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
289
|
+
return _truncate(out, self.cfg.max_output_chars)
|
|
290
|
+
except subprocess.TimeoutExpired:
|
|
291
|
+
return f"ERROR: git push timed out after {self.cfg.shell_timeout_s}s"
|
|
292
|
+
except Exception as e:
|
|
293
|
+
return f"ERROR: {e}"
|
|
294
|
+
|
|
162
295
|
def write_file(self, path: str, content: str, overwrite: bool = False) -> str:
|
|
163
296
|
p = _resolve_in_scope(self.cfg.project_root, path)
|
|
164
297
|
if os.path.exists(p) and not overwrite:
|
|
@@ -7,10 +7,11 @@ isn't installed, agent.py falls back to plain print() (see its import guard).
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
|
|
10
|
+
from prompt_toolkit.formatted_text import HTML
|
|
10
11
|
from rich.console import Console
|
|
11
12
|
from rich.markdown import Markdown
|
|
12
13
|
from rich.panel import Panel
|
|
13
|
-
from rich.prompt import Confirm
|
|
14
|
+
from rich.prompt import Confirm
|
|
14
15
|
from rich.syntax import Syntax
|
|
15
16
|
from rich.rule import Rule
|
|
16
17
|
from rich.table import Table
|
|
@@ -46,9 +47,18 @@ def interactive_banner(model: str, resumed: str = None):
|
|
|
46
47
|
console.print("[dim]Type a task, /sessions to list saved sessions, /exit to quit.[/dim]\n")
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def
|
|
50
|
+
async def prompt_task_async(session) -> str:
|
|
51
|
+
"""Read one line of input via a prompt_toolkit PromptSession, so the
|
|
52
|
+
input line stays pinned to the bottom of the terminal — all other
|
|
53
|
+
output (parsing/thinking spinners, panels, results) scrolls in the
|
|
54
|
+
region above it instead of interleaving with the prompt. `session` is
|
|
55
|
+
a prompt_toolkit.PromptSession the caller creates once and reuses
|
|
56
|
+
across turns (so up-arrow history works). Must be called inside the
|
|
57
|
+
caller's `patch_stdout()` context so Rich's output (which resolves
|
|
58
|
+
sys.stdout lazily on every print) is redrawn above the pinned prompt
|
|
59
|
+
instead of corrupting it."""
|
|
50
60
|
console.print(Rule(style="dim"))
|
|
51
|
-
return
|
|
61
|
+
return await session.prompt_async(HTML("<ansigreen><b>❯</b></ansigreen> "))
|
|
52
62
|
|
|
53
63
|
|
|
54
64
|
def thinking(label: str = "Thinking…"):
|
{ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ollama-coding-agent
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/HarryChen1995/coding_agent
|
|
@@ -17,6 +17,7 @@ Requires-Dist: httpx>=0.28
|
|
|
17
17
|
Requires-Dist: rich>=15.0
|
|
18
18
|
Requires-Dist: typer>=0.27
|
|
19
19
|
Requires-Dist: mcp>=1.27
|
|
20
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
20
21
|
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# Ollama Coding Agent
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ollama-coding-agent"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
description = "AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP"
|
|
9
9
|
readme = "PYPI_README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -20,6 +20,7 @@ dependencies = [
|
|
|
20
20
|
"rich>=15.0",
|
|
21
21
|
"typer>=0.27",
|
|
22
22
|
"mcp>=1.27",
|
|
23
|
+
"prompt_toolkit>=3.0",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ollama_coding_agent-0.5.0 → ollama_coding_agent-0.5.2}/ollama_coding_agent.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|