lcode-cli 0.30.0__py3-none-any.whl → 0.31.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
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.30.0"
6
+ __version__ = "0.31.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
lcode/agent.py CHANGED
@@ -112,7 +112,7 @@ SYSTEM_PROMPT = """You are lcode, an autonomous software-engineering agent runni
112
112
  - To understand a repository: look at the layout, README/docs, entry points and config, then grep and read the relevant files. Cite code as `path:line`.
113
113
  - Read a file before editing it. Use edit_file for targeted changes and write_file for new files or full rewrites. old_string must be copied exactly from the file (without the line-number prefixes).
114
114
  - Match the existing code style, naming and structure. Don't add unrequested features.
115
- - For multi-step tasks, plan with todo_write and keep it updated.
115
+ - For multi-step tasks, plan with todo_write and keep it updated; put decisions and findings you must not lose in its notes.
116
116
  - Work autonomously until the task is done. Only stop to ask the user when you are genuinely blocked or the decision is theirs.
117
117
  - Don't run destructive or irreversible commands (rm -rf, git reset --hard, git push --force, dropping data) unless the user explicitly asked.
118
118
  - If the user denies a tool call, don't retry the same thing; adjust or ask.
@@ -613,6 +613,9 @@ class Agent:
613
613
  "title": self.session_title,
614
614
  "messages": self.messages,
615
615
  "checkpoints": self.checkpoints.to_json(),
616
+ "todos": self.tools.todos,
617
+ "notes": self.tools.notes,
618
+ "changed": list(self.tools.changed),
616
619
  }
617
620
  # Written next to it and then renamed: a crash halfway leaves the previous version, not a broken file.
618
621
  partial = f.with_name(f".{f.name}.partial")
@@ -626,6 +629,8 @@ class Agent:
626
629
  self.session_name = ""
627
630
  self.session_title = ""
628
631
  self.checkpoints.reset(self.session_id)
632
+ self.tools.todos, self.tools.notes, self.tools.changed = [], [], {}
633
+ self.check_results = []
629
634
 
630
635
  def rename(self, name: str) -> None:
631
636
  self.session_name = " ".join(name.split())
@@ -652,6 +657,9 @@ class Agent:
652
657
  self.session_name = data.get("name", "")
653
658
  self.session_title = data.get("title") or sessions.title_from(self.messages)
654
659
  self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
660
+ self.tools.todos = list(data.get("todos") or [])
661
+ self.tools.notes = [str(n) for n in data.get("notes") or []]
662
+ self.tools.changed = dict.fromkeys(str(f) for f in data.get("changed") or [])
655
663
  self.reflected = len(self.messages) # checked for notes to remember when that session ended
656
664
  self.ctx_used = sum(len(json.dumps(m)) for m in self.messages) // 3
657
665
  self.counted_chars = None
@@ -1289,6 +1297,8 @@ class Agent:
1289
1297
  self.messages, keep=1, removed=removed
1290
1298
  ) # not enough: before the last request
1291
1299
  context_tools.note_removed(self.messages, removed)
1300
+ if saved:
1301
+ context_tools.pin(self.messages, context_tools.task_sheet(self))
1292
1302
  if saved:
1293
1303
  self.sent_chars = 0 # the conversation changed: the model reads it again
1294
1304
  self.pruned += 1
@@ -1333,7 +1343,9 @@ class Agent:
1333
1343
  {"role": "system", "content": self.system_prompt()},
1334
1344
  {
1335
1345
  "role": "user",
1336
- "content": f"{sessions.SUMMARY_PREFIX}\n\n{summary}" + context_tools.requests_block(requests),
1346
+ "content": f"{sessions.SUMMARY_PREFIX}\n\n{summary}"
1347
+ + context_tools.requests_block(requests)
1348
+ + (f"\n\n{sheet}" if (sheet := context_tools.task_sheet(self)) else ""),
1337
1349
  },
1338
1350
  {"role": "assistant", "content": "Got it — I have the context from the summary and will continue."},
1339
1351
  ]
lcode/context.py CHANGED
@@ -49,7 +49,9 @@ REQUESTS_HEADER = "[The user's requests in this conversation, word for word, old
49
49
  MAX_REQUESTS = 8 # the most recent ones are kept through a summary
50
50
  MAX_REQUEST_CHARS = 1500
51
51
  # What lcode appends to the user's words: attached files, images and folders, and its own notes.
52
- ATTACHED = re.compile(r"\n\n(?:<file path=|<image path=|<directory path=|\[lcode[:\]] |\[The user wants the )")
52
+ ATTACHED = re.compile(
53
+ r"\n\n(?:<file path=|<image path=|<directory path=|\[lcode[:\]] |\[The user wants the )"
54
+ ) # also the sheet
53
55
 
54
56
 
55
57
  def request_text(content: str) -> str:
@@ -187,6 +189,51 @@ def prune(messages: list[dict], keep: int = KEEP_RECENT_REQUESTS, removed: list[
187
189
  return saved
188
190
 
189
191
 
192
+ SHEET_HEADER = "[lcode] Task sheet: the plan, notes and changes so far, kept when old messages are removed"
193
+ MAX_SHEET = 3000 # characters
194
+
195
+
196
+ def task_sheet(agent: Agent) -> str:
197
+ """The state of the work, from lcode's own records: the todo list and the model's notes (todo_write), the
198
+ files changed in the session and the last check. Pinned where old messages go: in summaries and next to the
199
+ request when tool output is removed."""
200
+ tools = agent.tools
201
+ parts = []
202
+ if tools.todos:
203
+ marks = {"completed": "[x]", "in_progress": "[>]", "pending": "[ ]"}
204
+ plan = "\n".join(f"{marks.get(t.get('status', ''), '[ ]')} {t.get('content', '')}" for t in tools.todos)
205
+ parts.append(f"Plan:\n{plan}")
206
+ if tools.notes:
207
+ parts.append("Notes:\n" + "\n".join(f"- {note}" for note in tools.notes))
208
+ if tools.changed:
209
+ files = list(tools.changed)
210
+ more = f" and {len(files) - 30} more" if len(files) > 30 else ""
211
+ parts.append("Files changed in this session: " + ", ".join(files[-30:]) + more)
212
+ if agent.check_results:
213
+ last = agent.check_results[-1]
214
+ code = last.get("exit_code")
215
+ state = "passed" if code == 0 else "didn't run" if code is None else f"failed (exit code {code})"
216
+ parts.append(f"Last check: `{last.get('command')}` {state}")
217
+ if not parts:
218
+ return ""
219
+ sheet = "\n\n".join(parts)
220
+ if len(sheet) > MAX_SHEET:
221
+ sheet = sheet[: MAX_SHEET - 20] + "\n[…cut]"
222
+ return f"{SHEET_HEADER}\n{sheet}"
223
+
224
+
225
+ def pin(messages: list[dict], text: str) -> None:
226
+ """Put a note next to the request the model is working on, replacing an earlier sheet there."""
227
+ if not text:
228
+ return
229
+ for message in reversed(messages):
230
+ content = str(message.get("content", ""))
231
+ if message.get("role") == "user" and not content.startswith(("[lcode]", SUMMARY_PREFIX)):
232
+ content = content.split("\n\n" + SHEET_HEADER, 1)[0]
233
+ message["content"] = f"{content}\n\n{text}"
234
+ return
235
+
236
+
190
237
  def note_removed(messages: list[dict], removed: list[str]) -> None:
191
238
  """Tell the model, next to the request it's working on, which files it no longer has: a stub deep in the
192
239
  conversation is easy to overlook, and models then answer from memory instead of reading again."""
lcode/repl.py CHANGED
@@ -63,6 +63,7 @@ COMMANDS = {
63
63
  "/mode": "Permission mode: ask | plan | auto-edit | yolo (Shift+Tab cycles)",
64
64
  "/cd": "Change the working directory",
65
65
  "/todos": "Show the current todo list",
66
+ "/status": "The task sheet: your requests, the plan and notes, files changed, the last check",
66
67
  "/jobs": "Background commands the model started: /jobs, /jobs stop <id>",
67
68
  "/commit": "Commit the changes with a message in the repository's style, after you approve it",
68
69
  "/review": "Review the uncommitted changes, or the branch against a base: /review [base]",
@@ -109,6 +110,31 @@ def mention(path: Path) -> str:
109
110
  return f'@"{shown}"' if " " in shown else f"@{shown}"
110
111
 
111
112
 
113
+ def status_command(agent: Agent) -> None:
114
+ """What the session is doing, as lcode keeps it for the model (the task sheet)."""
115
+ c = agent.console
116
+ requests = context_tools.user_requests(agent.messages)
117
+ if not requests and not agent.tools.todos and not agent.tools.changed:
118
+ c.print("Nothing yet: no requests in this session.")
119
+ return
120
+ if requests:
121
+ latest = requests[-3:]
122
+ lines = [f"[dim]{len(requests) - len(latest) + i + 1}.[/] {escape(r[:300])}" for i, r in enumerate(latest)]
123
+ c.print(Panel("\n".join(lines), title="Requests (latest)", title_align="left", border_style="cyan"))
124
+ if agent.tools.todos or agent.tools.notes:
125
+ agent.tools.show_todos()
126
+ if agent.tools.changed:
127
+ c.print(f"[bold]Files changed:[/] {escape(', '.join(agent.tools.changed))}")
128
+ if agent.check_results:
129
+ last = agent.check_results[-1]
130
+ code = last.get("exit_code")
131
+ state = (
132
+ "[green]passed[/]" if code == 0 else "didn't run" if code is None else f"[red]failed[/] (exit code {code})"
133
+ )
134
+ c.print(f"[bold]Last check:[/] {escape(str(last.get('command')))} {state}")
135
+ c.print("[dim]The model keeps this too, even when old messages are removed or summarized.[/]")
136
+
137
+
112
138
  def build_session(agent: Agent) -> PromptSession:
113
139
  kb = KeyBindings()
114
140
 
@@ -978,6 +1004,8 @@ def handle_command(agent: Agent, line: str, hardware: Hardware) -> bool:
978
1004
  c.print(f"Working directory: {p}")
979
1005
  else:
980
1006
  c.print(f"[red]Not a directory: {p}[/]")
1007
+ elif cmd == "/status":
1008
+ status_command(agent)
981
1009
  elif cmd == "/todos":
982
1010
  agent.tools.show_todos()
983
1011
  elif cmd == "/jobs":
lcode/tools.py CHANGED
@@ -24,6 +24,7 @@ from pathlib import Path
24
24
  from typing import TYPE_CHECKING
25
25
 
26
26
  from rich.console import Group
27
+ from rich.markup import escape
27
28
  from rich.panel import Panel
28
29
  from rich.syntax import Syntax
29
30
  from rich.text import Text
@@ -45,6 +46,7 @@ NO_NETWORK = re.compile(
45
46
  )
46
47
  MAX_TOOL_OUTPUT = 30_000 # characters returned to the model per tool call
47
48
  PENDING_LINES = 5000 # output lines waiting to be read, at most
49
+ MAX_NOTES, MAX_NOTE = 12, 300 # the model's task notes: how many, how long each
48
50
  LEFTOVER_GRACE = 0.5 # seconds to wait for output after a command ends, before stopping what it left running
49
51
  IGNORE_DIRS = {
50
52
  ".git", "node_modules", "__pycache__", ".venv", "venv", "env", ".mypy_cache", ".pytest_cache",
@@ -161,7 +163,8 @@ SCHEMAS = [
161
163
  _fn(
162
164
  "todo_write",
163
165
  "Create or update your task list for multi-step work. Pass the full list every time. Keep exactly one "
164
- "item in_progress while working.",
166
+ "item in_progress while working. In `notes`, keep what you must not forget: decisions and why, findings, "
167
+ "open questions. The list and the notes stay with you even when old messages are removed.",
165
168
  {
166
169
  "todos": {
167
170
  "type": "array",
@@ -173,7 +176,12 @@ SCHEMAS = [
173
176
  },
174
177
  "required": ["content", "status"],
175
178
  },
176
- }
179
+ },
180
+ "notes": {
181
+ "type": "array",
182
+ "items": {"type": "string"},
183
+ "description": "Short notes, the full list each time (omit to keep the current ones)",
184
+ },
177
185
  },
178
186
  ["todos"],
179
187
  ),
@@ -438,6 +446,8 @@ class Toolbox:
438
446
  self.agent = agent
439
447
  self.read_mtimes: dict[str, float] = {}
440
448
  self.formats: dict[str, TextFormat] = {} # encoding and line endings of the files read
449
+ self.notes: list[str] = [] # the model's notes on the task (todo_write): decisions, findings, questions
450
+ self.changed: dict[str, None] = {} # files lcode wrote in this session, in order
441
451
  self.todos: list[dict] = []
442
452
  # An editor's buffers (lcode acp): read unsaved changes and write through the editor.
443
453
  self.reader: Callable[[Path], str | None] | None = None
@@ -865,6 +875,7 @@ class Toolbox:
865
875
  if not p.exists() or p.read_bytes() != data:
866
876
  p.write_bytes(data) # the file on disk must match, for checkpoints and the tools
867
877
  self.read_mtimes[str(p)] = p.stat().st_mtime
878
+ self.changed[self.rel(p)] = None
868
879
 
869
880
  def t_write_file(self, path: str, content: str) -> str:
870
881
  p = self.path(path)
@@ -1248,12 +1259,29 @@ class Toolbox:
1248
1259
  return f"[{describe(job)}]" + (f"\nIts last output:\n{last}" if last else "")
1249
1260
 
1250
1261
  # -- planning
1251
- def t_todo_write(self, todos: list | str) -> str:
1262
+ def t_todo_write(self, todos: list | str, notes: list | str | None = None) -> str:
1252
1263
  if isinstance(todos, str):
1253
1264
  todos = json.loads(todos)
1254
1265
  self.todos = list(todos)
1266
+ # Small models often put a note inside an item instead of in `notes`: keep it either way.
1267
+ attached = [
1268
+ str(todo.get(key)).strip()
1269
+ for todo in self.todos
1270
+ if isinstance(todo, dict)
1271
+ for key in ("notes", "note", "details")
1272
+ if str(todo.get(key) or "").strip()
1273
+ ]
1274
+ if attached:
1275
+ notes = [*([] if notes is None else ([notes] if isinstance(notes, str) else list(notes))), *attached]
1276
+ if notes is not None:
1277
+ if isinstance(notes, str):
1278
+ try:
1279
+ notes = json.loads(notes)
1280
+ except ValueError:
1281
+ notes = [notes]
1282
+ self.notes = [str(n).strip()[:MAX_NOTE] for n in notes if str(n).strip()][:MAX_NOTES]
1255
1283
  self.show_todos()
1256
- return "Todo list updated."
1284
+ return "Todo list updated." + (f" {len(self.notes)} note(s) kept." if notes is not None else "")
1257
1285
 
1258
1286
  def show_todos(self) -> None:
1259
1287
  icons = {"completed": "[green]✔[/]", "in_progress": "[yellow]◐[/]", "pending": "[dim]○[/]"}
@@ -1265,4 +1293,6 @@ class Toolbox:
1265
1293
  elif status == "in_progress":
1266
1294
  text = f"[bold]{text}[/]"
1267
1295
  rows.append(f"{icons.get(status, '○')} {text}")
1296
+ if self.notes:
1297
+ rows += ["", *(f"[dim]•[/] {escape(note)}" for note in self.notes)]
1268
1298
  self.console.print(Panel("\n".join(rows) or "(empty)", title="Todos", title_align="left", border_style="blue"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
5
5
  Project-URL: Homepage, https://nasser1941.github.io/lcode/
6
6
  Project-URL: Documentation, https://nasser1941.github.io/lcode/
@@ -1,8 +1,8 @@
1
- lcode/__init__.py,sha256=_ncPdkojpmAnL6Af19KuRAd4o_TixpnzWrgz3usKy0A,473
1
+ lcode/__init__.py,sha256=gXBLoiYa3VhO8oR-eyFbfplfXuE6GNMT6g92TwUc_2g,473
2
2
  lcode/__main__.py,sha256=s5N5pryX2FAu93GGZ3FxUrIXHWGzxz66lKUoaV_h8Ws,35
3
3
  lcode/acp.py,sha256=d97VV3sbqXdWKhPQQNbYRaDTm8WrUk0mGbDLiOyX7jE,29058
4
4
  lcode/action.py,sha256=wuT5F9EJCdEligPeytzmEi8FpahWBhXer528tpuTj_k,16711
5
- lcode/agent.py,sha256=CVOyaV_KJQor1zlW2zou3iQXzf9UPV-3emTmuqJ5JVE,69562
5
+ lcode/agent.py,sha256=OkGViWlZd8g-MFZpaCnbRIpmrFXnKajbIeEMKV1e7-o,70275
6
6
  lcode/api.py,sha256=PhG8PZReM7DXSxG6wf7mEjAKx_TAsgj3xHApacrXGCM,15684
7
7
  lcode/backends.py,sha256=dtJHrC6ys73bRDg64z0tQz1PavmeAkiODZu1fOO9ExQ,20635
8
8
  lcode/bench.py,sha256=KDgPnkceZOS3gDDEl5j6-k8Ser2es4-IuV_S8p_46Zo,41517
@@ -15,7 +15,7 @@ lcode/cli.py,sha256=svnW6pfLtqiMXbA3wMlYESKGgGe6Xb9wlWkqg6Cz0O0,41081
15
15
  lcode/clipboard.py,sha256=nTSdJyCXPdtjH947RjvB3QvRqnL5HZsS9KB8EkC42cc,5138
16
16
  lcode/codesearch.py,sha256=Io8leN1nb_Y-Dvlm9xaGtwUWYXX-ooswuznJV6dGU1s,10766
17
17
  lcode/config.py,sha256=KSjSBvxA9nmd4h4QYk3xp8BQ7C04pjTULsSn0jcQwBc,11301
18
- lcode/context.py,sha256=WSMiitscDh2Juy4Hx0x9EPNWrn804IbOWRZly1-DU38,17022
18
+ lcode/context.py,sha256=0YYLrC7gI164HqDjW_jrgHSnfrAilrRUoTLhvw54VI8,19082
19
19
  lcode/extensions.py,sha256=W0HFDl0BvROzr7i_qtabHdIbacBgP5o5qIJLzoM2oNE,16806
20
20
  lcode/frontmatter.py,sha256=Bx0n4vLPhp7ZCWx-g7xqzqNy5VoKBxyeeikh5UFB_gA,2441
21
21
  lcode/gitflow.py,sha256=6j_nnCBZXkX1qhTI7K4CyZ5D0NAZLpH96weFEo64AkQ,27913
@@ -33,14 +33,14 @@ lcode/permissions.py,sha256=3OPVVth6uTE5kcAWMZ8X_vUnFRbf6STEHAJimZX0Yu4,12118
33
33
  lcode/planning.py,sha256=Q2upaoZPnXDMQT2F-ETxX1lk_wDntTzhSKNVYIQSoQA,6540
34
34
  lcode/render.py,sha256=gMvf4W3qkLUJ4Vot3az_hP_zRDAItJC9pP-68dHFkfA,1375
35
35
  lcode/repeats.py,sha256=-vcbj363nFcIyso7yFJgsbHz9C6VOu2EOg3Svs4eFyE,4091
36
- lcode/repl.py,sha256=ayrKySMbbRbax_WMjqYBN4fkcDpzuHniefJC3DZ4-2Y,50687
36
+ lcode/repl.py,sha256=0f3nojURlQE8Pm_JkXvh9y7nEg1CHG3xanZIs0osPxw,52107
37
37
  lcode/repomap.py,sha256=1GRJgEUjBl9XHSXsF7tSGXBCEgsY-3CzwMohaTudZ_M,13969
38
38
  lcode/residency.py,sha256=bDB2J0SZULDOz6wYuMCnkjacYk7pWi5f75iY83ycAXQ,6912
39
39
  lcode/sandbox.py,sha256=u_pkrVRXhCGhh-wFDPwuZ3jGG7UeXxXRAeBNsm37ZvE,8608
40
40
  lcode/secretscan.py,sha256=6Euvfk2gCtK7lZbuQD84zgWuSECi6OnSw8V5rFu1tfU,13575
41
41
  lcode/sessions.py,sha256=wOR-J0EHgJxLSPboKPHnvBcp5VTUYTj3tZWgpsa6-K4,4013
42
42
  lcode/subagents.py,sha256=GGn2E7QUwEsShJEHXaMhG53leyGFUZwJku_n33gJguU,29157
43
- lcode/tools.py,sha256=1zH4hkEyHQz4byV348J4zfe0VzwvjmlH1MOqiobznbA,59446
43
+ lcode/tools.py,sha256=PofQ77Y7TvsUcFcvrUDP4GirpgkQKhhC_NL_1K_8_Jo,61182
44
44
  lcode/update.py,sha256=i92bnW252VneYh0vD6djxmXsDOa5MXP20bjlLZUue_c,25236
45
45
  lcode/vision.py,sha256=w33QPmdGwP33wM4mvmAH4amFDZ1xzeXhEsSLJ-STSHY,4243
46
46
  lcode/web.py,sha256=vyxKbSRJTl1Lj3hj7Yd_NwTgbv0Xi6dup2YfUXjqp-Y,14579
@@ -60,8 +60,8 @@ lcode/servers/__init__.py,sha256=qrIhxEodEB8cjKEQ4Cicc0GsQhE2OH-9qlVHzr3-tq8,77
60
60
  lcode/servers/encord.py,sha256=WSauawL1UKYZCUAG1oalcKL_An3DVof-JM6xIgFyQSE,12769
61
61
  lcode/servers/valohai.py,sha256=e5xZGIoa4cja4f5i85q1GpL9c7cglUjV8jN2co1CFMY,14430
62
62
  lcode/servers/xai.py,sha256=h2emAvHSnkuB9WmJInlynsDhvxJwNVD2AHFX7vqMXNc,13627
63
- lcode_cli-0.30.0.dist-info/METADATA,sha256=s6dAfe3HCeh9LIPNLLoYY3VGXfGT6b8ZW0pZbERa6X0,12620
64
- lcode_cli-0.30.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
65
- lcode_cli-0.30.0.dist-info/entry_points.txt,sha256=Hxsn_itbnbgft3U0FwxWdmEt0yoAV2LB-nJDnL9WT20,172
66
- lcode_cli-0.30.0.dist-info/licenses/LICENSE,sha256=JWjmoL7nNIC1XUa-dZm2hwLUgjUQMFo6k8APb0idNzs,1074
67
- lcode_cli-0.30.0.dist-info/RECORD,,
63
+ lcode_cli-0.31.0.dist-info/METADATA,sha256=kGzeEZoj0ZAZxYBHC30JRJAKERgqAq4h9XObVzJYPms,12620
64
+ lcode_cli-0.31.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
65
+ lcode_cli-0.31.0.dist-info/entry_points.txt,sha256=Hxsn_itbnbgft3U0FwxWdmEt0yoAV2LB-nJDnL9WT20,172
66
+ lcode_cli-0.31.0.dist-info/licenses/LICENSE,sha256=JWjmoL7nNIC1XUa-dZm2hwLUgjUQMFo6k8APb0idNzs,1074
67
+ lcode_cli-0.31.0.dist-info/RECORD,,