lcode-cli 0.3.0__py3-none-any.whl → 0.4.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.
- lcode/__init__.py +1 -1
- lcode/agent.py +48 -3
- lcode/bench.py +1084 -0
- lcode/cli.py +137 -3
- lcode/config.py +4 -0
- lcode/mcp/__init__.py +5 -0
- lcode/mcp/auth.py +392 -0
- lcode/mcp/catalog.py +109 -0
- lcode/mcp/catalog.toml +198 -0
- lcode/mcp/client.py +165 -0
- lcode/mcp/commands.py +403 -0
- lcode/mcp/config.py +221 -0
- lcode/mcp/http.py +161 -0
- lcode/mcp/manager.py +360 -0
- lcode/mcp/protocol.py +124 -0
- lcode/mcp/stdio.py +153 -0
- lcode/ollama.py +4 -0
- lcode/permissions.py +3 -1
- lcode/repl.py +52 -1
- lcode/tools.py +27 -0
- {lcode_cli-0.3.0.dist-info → lcode_cli-0.4.0.dist-info}/METADATA +8 -4
- lcode_cli-0.4.0.dist-info/RECORD +34 -0
- lcode_cli-0.3.0.dist-info/RECORD +0 -22
- {lcode_cli-0.3.0.dist-info → lcode_cli-0.4.0.dist-info}/WHEEL +0 -0
- {lcode_cli-0.3.0.dist-info → lcode_cli-0.4.0.dist-info}/entry_points.txt +0 -0
- {lcode_cli-0.3.0.dist-info → lcode_cli-0.4.0.dist-info}/licenses/LICENSE +0 -0
lcode/__init__.py
CHANGED
lcode/agent.py
CHANGED
|
@@ -15,12 +15,14 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
from rich.markdown import Markdown
|
|
18
|
+
from rich.markup import escape
|
|
18
19
|
from rich.panel import Panel
|
|
19
20
|
from rich.text import Text
|
|
20
21
|
|
|
21
22
|
from lcode import catalog, limits, sessions, web
|
|
22
23
|
from lcode.checkpoints import Checkpoints
|
|
23
24
|
from lcode.config import format_tokens
|
|
25
|
+
from lcode.mcp import McpManager
|
|
24
26
|
from lcode.ollama import Ollama, OllamaError
|
|
25
27
|
from lcode.permissions import Permissions
|
|
26
28
|
from lcode.render import MarkdownStreamer
|
|
@@ -150,10 +152,13 @@ class Agent:
|
|
|
150
152
|
self.tools = Toolbox(self)
|
|
151
153
|
self.session_id = self.new_session_id()
|
|
152
154
|
self.checkpoints = Checkpoints(self.console, settings.checkpoints)
|
|
155
|
+
self.mcp: McpManager | None = None # set by the CLI when MCP servers are configured
|
|
156
|
+
self._mcp_prompt = "" # the MCP part at the end of the system prompt
|
|
153
157
|
self.session_name = ""
|
|
154
158
|
self.session_title = ""
|
|
155
159
|
self.ctx_used = 0
|
|
156
160
|
self.last_speed = 0.0
|
|
161
|
+
self.usage = {"requests": 0, "prompt_tokens": 0, "prompt_ns": 0, "output_tokens": 0, "output_ns": 0}
|
|
157
162
|
self.messages: list[dict] = []
|
|
158
163
|
self.reset()
|
|
159
164
|
|
|
@@ -187,9 +192,32 @@ class Agent:
|
|
|
187
192
|
return web.resolve_backend(self.settings.search_backend, self.settings.searxng_url)
|
|
188
193
|
|
|
189
194
|
def tool_schemas(self) -> list[dict]:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
schemas = list(SCHEMAS)
|
|
196
|
+
if self.settings.web != "off":
|
|
197
|
+
schemas += [*([WEB_SEARCH_SCHEMA] if self.search_backend() else []), WEB_FETCH_SCHEMA]
|
|
198
|
+
if self.mcp:
|
|
199
|
+
schemas += self.mcp.schemas(self.settings.context)
|
|
200
|
+
return schemas
|
|
201
|
+
|
|
202
|
+
def prepare_mcp(self) -> None:
|
|
203
|
+
"""Before a request: wait for MCP servers still starting and describe them in the system prompt."""
|
|
204
|
+
if not self.mcp:
|
|
205
|
+
return
|
|
206
|
+
if self.mcp.pending:
|
|
207
|
+
with self.console.status(f"Starting MCP servers: {', '.join(self.mcp.pending)}", spinner="dots"):
|
|
208
|
+
self.mcp.wait()
|
|
209
|
+
for state in self.mcp.servers.values():
|
|
210
|
+
if state.status in ("failed", "login") and not state.reported:
|
|
211
|
+
state.reported = True
|
|
212
|
+
self.console.print(f"[yellow]MCP server {state.name}: {escape(state.error)}[/] [dim](/mcp)[/]")
|
|
213
|
+
self.mcp.refresh_changed()
|
|
214
|
+
section = self.mcp.prompt_section(self.settings.context)
|
|
215
|
+
if section != self._mcp_prompt:
|
|
216
|
+
content = self.messages[0]["content"]
|
|
217
|
+
if self._mcp_prompt and content.endswith(self._mcp_prompt):
|
|
218
|
+
content = content[: -len(self._mcp_prompt)]
|
|
219
|
+
self.messages[0]["content"] = content + section
|
|
220
|
+
self._mcp_prompt = section
|
|
193
221
|
|
|
194
222
|
def web_prompt(self) -> str:
|
|
195
223
|
if self.settings.web == "off":
|
|
@@ -202,6 +230,7 @@ class Agent:
|
|
|
202
230
|
|
|
203
231
|
def reset(self) -> None:
|
|
204
232
|
self.messages = [{"role": "system", "content": self.system_prompt()}]
|
|
233
|
+
self._mcp_prompt = ""
|
|
205
234
|
self.tools.read_mtimes.clear()
|
|
206
235
|
self.ctx_used = len(self.messages[0]["content"]) // 3
|
|
207
236
|
|
|
@@ -253,6 +282,7 @@ class Agent:
|
|
|
253
282
|
else:
|
|
254
283
|
note = f"The session's directory {saved_cwd} no longer exists; staying in {self.cwd}"
|
|
255
284
|
self.messages = [{"role": "system", "content": self.system_prompt()}, *data.get("messages", [])[1:]]
|
|
285
|
+
self._mcp_prompt = ""
|
|
256
286
|
self.session_id = info.id
|
|
257
287
|
self.checkpoints.load(info.id, data.get("checkpoints") or [])
|
|
258
288
|
self.session_name = data.get("name", "")
|
|
@@ -390,6 +420,11 @@ class Agent:
|
|
|
390
420
|
message["tool_calls"] = tool_calls
|
|
391
421
|
self.messages.append(message)
|
|
392
422
|
if final:
|
|
423
|
+
self.usage["requests"] += 1
|
|
424
|
+
self.usage["prompt_tokens"] += final.get("prompt_eval_count", 0)
|
|
425
|
+
self.usage["prompt_ns"] += final.get("prompt_eval_duration", 0)
|
|
426
|
+
self.usage["output_tokens"] += final.get("eval_count", 0)
|
|
427
|
+
self.usage["output_ns"] += final.get("eval_duration", 0)
|
|
393
428
|
self.ctx_used = final.get("prompt_eval_count", 0) + final.get("eval_count", 0)
|
|
394
429
|
seconds = final.get("eval_duration", 0) / 1e9
|
|
395
430
|
self.last_speed = final.get("eval_count", 0) / seconds if seconds else 0.0
|
|
@@ -409,6 +444,15 @@ class Agent:
|
|
|
409
444
|
return f"glob({args.get('pattern', '')})"
|
|
410
445
|
if name == "list_dir":
|
|
411
446
|
return f"list_dir({args.get('path', '.')})"
|
|
447
|
+
if self.mcp and self.mcp.owns(name):
|
|
448
|
+
if name == "mcp_find_tools":
|
|
449
|
+
return f"mcp_find_tools({args.get('query', '')!r})"
|
|
450
|
+
try:
|
|
451
|
+
state, tool, arguments = self.mcp.resolve(name, args)
|
|
452
|
+
except Exception:
|
|
453
|
+
return name
|
|
454
|
+
preview = json.dumps(arguments, ensure_ascii=False)
|
|
455
|
+
return f"{state.name} › {tool['name']}({preview if len(preview) <= 80 else preview[:79] + '…'})"
|
|
412
456
|
return name
|
|
413
457
|
|
|
414
458
|
def run_turn(self, user_text: str) -> None:
|
|
@@ -423,6 +467,7 @@ class Agent:
|
|
|
423
467
|
self.checkpoints.before_change(self.cwd)
|
|
424
468
|
|
|
425
469
|
def _run_turn(self, user_text: str) -> None:
|
|
470
|
+
self.prepare_mcp()
|
|
426
471
|
self.messages.append({"role": "user", "content": self.expand_mentions(user_text)})
|
|
427
472
|
malformed = 0
|
|
428
473
|
for _ in range(MAX_STEPS_PER_TURN):
|