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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """lcode — a local-first terminal coding agent powered by open-weight models via Ollama."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
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
- if self.settings.web == "off":
191
- return SCHEMAS
192
- return [*SCHEMAS, *([WEB_SEARCH_SCHEMA] if self.search_backend() else []), WEB_FETCH_SCHEMA]
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):