lcode-cli 0.26.0__py3-none-any.whl → 0.27.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.26.0"
6
+ __version__ = "0.27.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
lcode/agent.py CHANGED
@@ -234,6 +234,7 @@ class Settings:
234
234
  notify_after: int = 30 # seconds: a request this long notifies when it's done or waits for an answer
235
235
  max_parallel_agents: int = 1
236
236
  secret_check: bool = True # scan what git commit and git push would add (lcode.secretscan)
237
+ compact_at: int = 0 # tokens: make room at this size even when the window is larger (0: only at 85%)
237
238
  repeat_limit: int = 3 # identical calls with identical results before the model is told (lcode.repeats)
238
239
 
239
240
 
@@ -270,6 +271,7 @@ class Agent:
270
271
  self.agent_runs: list[subagents.Record] = [] # subagents run in this session, for /agents
271
272
  self.plan = "" # the plan the user approved; kept through compaction
272
273
  self.pruned = self.compacted = 0 # how often the context was pruned or summarized automatically
274
+ self.summarized_size = 0 # tokens right after the last summary (0: none in this conversation)
273
275
  self._extensions: tuple[Path, extensions.Extensions] | None = None
274
276
  self.skills_loaded: set[str] = set() # skills whose instructions are in the conversation
275
277
  # Set on subagents (see lcode.subagents):
@@ -514,6 +516,7 @@ class Agent:
514
516
  return WEB_PROMPT.format(tools=tools, date=dt.date.today().isoformat())
515
517
 
516
518
  def reset(self) -> None:
519
+ self.summarized_size = 0
517
520
  self.messages = [{"role": "system", "content": self.system_prompt()}]
518
521
  self._mcp_prompt = ""
519
522
  self.tools.read_mtimes.clear()
@@ -1046,13 +1049,27 @@ class Agent:
1046
1049
  return text + ("\n\n" + "\n\n".join(attached) if attached else "")
1047
1050
 
1048
1051
  # -- context management
1052
+ def compact_limit(self) -> int:
1053
+ """The tokens in use at which lcode makes room: 85% of the window, or `compact_at` when that's lower.
1054
+ Small models lose track in long contexts well before a large window is full."""
1055
+ limit = int(AUTO_COMPACT_RATIO * self.settings.context)
1056
+ if self.settings.compact_at > 0:
1057
+ limit = min(limit, self.settings.compact_at)
1058
+ return limit
1059
+
1049
1060
  def maybe_compact(self) -> None:
1050
- if self.ctx_used <= AUTO_COMPACT_RATIO * self.settings.context:
1061
+ limit = self.compact_limit()
1062
+ if self.ctx_used <= limit:
1051
1063
  return
1064
+ enough = int(limit * PRUNED_ENOUGH / AUTO_COMPACT_RATIO) # after pruning, a summary isn't needed below this
1052
1065
  if self.settings.prune:
1053
- saved = context_tools.prune(self.messages) # tool output from before the last two requests
1054
- if self.ctx_used - saved // 3 > PRUNED_ENOUGH * self.settings.context:
1055
- saved += context_tools.prune(self.messages, keep=1) # not enough: before the last request
1066
+ removed: list[str] = [] # files and pages whose only copy goes
1067
+ saved = context_tools.prune(self.messages, removed=removed) # tool output from before the last two requests
1068
+ if self.ctx_used - saved // 3 > enough:
1069
+ saved += context_tools.prune(
1070
+ self.messages, keep=1, removed=removed
1071
+ ) # not enough: before the last request
1072
+ context_tools.note_removed(self.messages, removed)
1056
1073
  if saved:
1057
1074
  self.pruned += 1
1058
1075
  self.ctx_used = max(len(self.messages[0]["content"]) // 3, self.ctx_used - saved // 3)
@@ -1060,9 +1077,17 @@ class Agent:
1060
1077
  f"[dim]Removed old tool output from the conversation (~{format_tokens(saved // 3)} tokens) to "
1061
1078
  "make room.[/]"
1062
1079
  )
1063
- if self.ctx_used <= PRUNED_ENOUGH * self.settings.context:
1080
+ if self.ctx_used <= enough:
1064
1081
  return
1065
- self.console.print("[yellow]Context is nearly full — compacting the conversation…[/]")
1082
+ window_full = self.ctx_used > AUTO_COMPACT_RATIO * self.settings.context
1083
+ if not window_full and self.summarized_size and self.ctx_used - self.summarized_size < limit // 4:
1084
+ return # summarized not long ago: summarizing again so soon would only lose more
1085
+ if limit < AUTO_COMPACT_RATIO * self.settings.context:
1086
+ self.console.print(
1087
+ f"[yellow]The conversation reached {format_tokens(limit)} tokens (compact_at) — summarizing it…[/]"
1088
+ )
1089
+ else:
1090
+ self.console.print("[yellow]Context is nearly full — compacting the conversation…[/]")
1066
1091
  self.compacted += 1
1067
1092
  self.compact()
1068
1093
 
@@ -1083,9 +1108,13 @@ class Agent:
1083
1108
  with self.console.status("[cyan]Compacting…[/]"):
1084
1109
  for chunk in self.chat([*self.messages, {"role": "user", "content": instructions}], None, False):
1085
1110
  summary += chunk.get("message", {}).get("content", "")
1111
+ requests = context_tools.user_requests(self.messages)
1086
1112
  self.messages = [
1087
1113
  {"role": "system", "content": self.system_prompt()},
1088
- {"role": "user", "content": f"[Summary of our conversation so far]\n\n{summary}"},
1114
+ {
1115
+ "role": "user",
1116
+ "content": f"{sessions.SUMMARY_PREFIX}\n\n{summary}" + context_tools.requests_block(requests),
1117
+ },
1089
1118
  {"role": "assistant", "content": "Got it — I have the context from the summary and will continue."},
1090
1119
  ]
1091
1120
  if self.plan: # the approved plan stays word for word
@@ -1097,6 +1126,7 @@ class Agent:
1097
1126
  )
1098
1127
  self.skills_loaded.clear()
1099
1128
  self.ctx_used = sum(len(m["content"]) for m in self.messages) // 3
1129
+ self.summarized_size = self.ctx_used
1100
1130
  self.reflected = len(self.messages)
1101
1131
  self.console.print(Panel(Markdown(summary), title="Compacted summary", border_style="blue"))
1102
1132
 
lcode/api.py CHANGED
@@ -196,6 +196,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
196
196
  notify_after=cfg["notify_after"],
197
197
  secret_check=cfg["secret_check"],
198
198
  repeat_limit=cfg["repeat_limit"],
199
+ compact_at=cfg["compact_at"],
199
200
  )
200
201
  agent = Agent(ollama, settings, cwd, console=console)
201
202
  agent.interactive = options.interactive
lcode/cli.py CHANGED
@@ -579,6 +579,7 @@ def cmd_bench(args) -> None:
579
579
  web="off",
580
580
  checkpoints=False,
581
581
  prune=not args.no_prune,
582
+ compact_at=cfg["compact_at"] if args.compact_at is None else args.compact_at,
582
583
  repo_map=args.repo_map,
583
584
  embed_model=cfg["embed_model"] if args.code_search else "off",
584
585
  )
@@ -678,6 +679,14 @@ def cmd_chat(args) -> None:
678
679
  sys.exit(exit_code)
679
680
 
680
681
 
682
+ def compact_tokens(value: str) -> int:
683
+ """`--compact-at`: a token count such as 64k, or 0."""
684
+ try:
685
+ return config.parse_compact_at(value)
686
+ except ConfigError as e:
687
+ raise argparse.ArgumentTypeError(str(e)) from e
688
+
689
+
681
690
  def build_parser() -> argparse.ArgumentParser:
682
691
  parser = argparse.ArgumentParser(
683
692
  prog="lcode",
@@ -765,6 +774,12 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
765
774
  p.add_argument("--repo-map", action="store_true", help="give the model the repository map (off by default)")
766
775
  p.add_argument("--code-search", action="store_true", help="index each task's folder and offer semantic code search")
767
776
  p.add_argument("--no-prune", action="store_true", help="don't remove old tool output (to compare, with --session)")
777
+ p.add_argument(
778
+ "--compact-at",
779
+ type=compact_tokens,
780
+ metavar="TOKENS",
781
+ help="summarize at this many tokens, e.g. 64k (default: the compact_at setting; 0: only at 85%%)",
782
+ )
768
783
  subs["bench"] = p
769
784
  p = argparse.ArgumentParser(
770
785
  prog="lcode index",
lcode/config.py CHANGED
@@ -61,6 +61,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
61
61
  "notify_after": (30, int, "seconds a request runs before it notifies (with notify on)"),
62
62
  "repeat_limit": (3, int, "identical tool calls with identical results before lcode steps in (0: never)"),
63
63
  "secret_check": (True, bool, "before git commit and git push, look for passwords, keys and tokens they'd add"),
64
+ "compact_at": (65536, int, "summarize at this many tokens even when the window is larger (0: only at 85%)"),
64
65
  "max_parallel_agents": (1, int, "subagents that may run at the same time (more needs OLLAMA_NUM_PARALLEL)"),
65
66
  }
66
67
  ENV_OVERRIDES = {
@@ -81,6 +82,19 @@ class ConfigError(ValueError):
81
82
  pass
82
83
 
83
84
 
85
+ MIN_COMPACT_AT = 16384 # below this, the system prompt and a summary alone come close to the ceiling
86
+
87
+
88
+ def parse_compact_at(value: str | int) -> int:
89
+ """compact_at: a token count like parse_context's, at least 16K, or 0 (off)."""
90
+ if str(value).strip().lower() in ("0", "off", "false", "none"):
91
+ return 0
92
+ tokens = parse_context(value)
93
+ if tokens < MIN_COMPACT_AT:
94
+ raise ConfigError(f"compact_at {tokens} is too small: use at least 16k, or 0 to turn it off")
95
+ return tokens
96
+
97
+
84
98
  def parse_context(value: str | int) -> int:
85
99
  """Parse '131072', '128k', '128K' or '1m' into a token count (k = 1024)."""
86
100
  if isinstance(value, int):
@@ -118,6 +132,8 @@ def coerce(key: str, value: object) -> object:
118
132
  _, typ, _ = SETTINGS[key]
119
133
  if key == "context":
120
134
  return parse_context(value) # type: ignore[arg-type]
135
+ if key == "compact_at":
136
+ return parse_compact_at(value) # type: ignore[arg-type]
121
137
  if key in ("ollama_host", "searxng_url"):
122
138
  return normalize_host(str(value))
123
139
  if key == "base_url":
lcode/context.py CHANGED
@@ -29,6 +29,10 @@ if TYPE_CHECKING:
29
29
  KEEP_RECENT_REQUESTS = 2 # tool results of the last requests stay as they are
30
30
  MIN_PRUNE_CHARS = 400 # shorter results aren't worth replacing
31
31
  PRUNED = "[lcode: " # how stubs start, so they're never pruned twice
32
+ GONE = ( # a removed file read: firm, because models answer from the stub instead of reading it again
33
+ "earlier; its content was removed to save context and isn't in this conversation any more. Read it again "
34
+ "before answering anything about it.]"
35
+ )
32
36
  KEEP_TOOLS = {"agent", "skill", "memory", "todo_write", "present_plan"} # short, or still guiding the work
33
37
  ERROR_LINE = re.compile(
34
38
  r"error|exception|traceback|failed|failure|fatal|panic|assert|denied|not found|cannot|can't|undefined", re.I
@@ -37,6 +41,56 @@ MAX_ERROR_LINES = 40
37
41
  OUTPUT_DAYS = 7 # saved full outputs are deleted after this
38
42
 
39
43
 
44
+ # ----------------------------------------------------------------------------- the user's requests
45
+
46
+ REQUESTS_HEADER = "[The user's requests in this conversation, word for word, oldest first]"
47
+ MAX_REQUESTS = 8 # the most recent ones are kept through a summary
48
+ MAX_REQUEST_CHARS = 1500
49
+ # What lcode appends to the user's words: attached files, images and folders, and its own notes.
50
+ ATTACHED = re.compile(r"\n\n(?:<file path=|<image path=|<directory path=|\[lcode[:\]] |\[The user wants the )")
51
+
52
+
53
+ def request_text(content: str) -> str:
54
+ """The user's own words in a message, without what lcode attached to it."""
55
+ cut = ATTACHED.search(content)
56
+ return (content[: cut.start()] if cut else content).strip()
57
+
58
+
59
+ def user_requests(messages: list[dict]) -> list[str]:
60
+ """What the user asked in the conversation, oldest first, including the requests an earlier summary kept."""
61
+ requests: list[str] = []
62
+ for message in messages:
63
+ content = message.get("content") or ""
64
+ if message.get("role") != "user":
65
+ continue
66
+ if content.startswith(SUMMARY_PREFIX):
67
+ start = content.find(REQUESTS_HEADER)
68
+ if start >= 0:
69
+ rest = content[start + len(REQUESTS_HEADER) :]
70
+ try:
71
+ kept, _ = json.JSONDecoder().raw_decode(rest[rest.index("[") :])
72
+ requests += [str(r) for r in kept]
73
+ except ValueError:
74
+ pass
75
+ continue
76
+ if content.startswith("[lcode]"):
77
+ continue # lcode's own nudges
78
+ text = request_text(content)
79
+ if text:
80
+ requests.append(text)
81
+ return requests
82
+
83
+
84
+ def requests_block(requests: list[str]) -> str:
85
+ """The user's latest requests, word for word, to keep the goal through a summary."""
86
+ if not requests:
87
+ return ""
88
+ kept = [r if len(r) <= MAX_REQUEST_CHARS else f"{r[: MAX_REQUEST_CHARS - 200]} […] {r[-150:]}" for r in requests]
89
+ older = len(kept) - MAX_REQUESTS
90
+ note = f" (and {older} earlier, in the summary)" if older > 0 else ""
91
+ return f"\n\n{REQUESTS_HEADER}{note}\n{json.dumps(kept[-MAX_REQUESTS:], ensure_ascii=False, indent=1)}"
92
+
93
+
40
94
  # ----------------------------------------------------------------------------- pruning
41
95
 
42
96
 
@@ -63,7 +117,7 @@ def stub(name: str, args: dict, content: str) -> str:
63
117
  if args.get("offset") or args.get("limit"):
64
118
  start = int(args.get("offset") or 1)
65
119
  span = f" from line {start}"
66
- return f"{PRUNED}read {args.get('path', 'a file')}{span} earlier; removed to save context. Read it again if you need it.]"
120
+ return f"{PRUNED}read {args.get('path', 'a file')}{span} {GONE}"
67
121
  if name == "bash":
68
122
  command = " ".join(str(args.get("command", "")).split())
69
123
  command = command if len(command) <= 80 else command[:79] + "…"
@@ -92,8 +146,9 @@ def boundary(messages: list[dict], keep: int = KEEP_RECENT_REQUESTS) -> int:
92
146
  return requests[-keep] if len(requests) >= keep else 0
93
147
 
94
148
 
95
- def prune(messages: list[dict], keep: int = KEEP_RECENT_REQUESTS) -> int:
96
- """Replace stale tool results with stubs, in place. Returns the number of characters saved."""
149
+ def prune(messages: list[dict], keep: int = KEEP_RECENT_REQUESTS, removed: list[str] | None = None) -> int:
150
+ """Replace stale tool results with stubs, in place. Returns the number of characters saved; the files and
151
+ pages whose only copy went are added to `removed`."""
97
152
  calls = calls_for_results(messages)
98
153
  edge = boundary(messages, keep)
99
154
  # A read is superseded by a later read of the whole file, or of the same lines: not by a later
@@ -118,12 +173,34 @@ def prune(messages: list[dict], keep: int = KEEP_RECENT_REQUESTS) -> int:
118
173
  continue
119
174
  new = stub(name, args, content)
120
175
  if reread:
121
- new = new.replace("earlier; removed", "earlier (read again later); removed")
176
+ new = new.replace(
177
+ GONE, "earlier (read again later); removed to save context. Read it again if you need it.]"
178
+ )
179
+ elif removed is not None and name in ("read_file", "web_fetch"):
180
+ what = str(args.get("path") or args.get("url") or "")
181
+ if what and what not in removed:
182
+ removed.append(what)
122
183
  messages[i]["content"] = new
123
184
  saved += len(content) - len(new)
124
185
  return saved
125
186
 
126
187
 
188
+ def note_removed(messages: list[dict], removed: list[str]) -> None:
189
+ """Tell the model, next to the request it's working on, which files it no longer has: a stub deep in the
190
+ conversation is easy to overlook, and models then answer from memory instead of reading again."""
191
+ if not removed:
192
+ return
193
+ for message in reversed(messages):
194
+ content = str(message.get("content", ""))
195
+ if message.get("role") == "user" and not content.startswith(("[lcode]", SUMMARY_PREFIX)):
196
+ names = ", ".join(removed[:10]) + (f" and {len(removed) - 10} more" if len(removed) > 10 else "")
197
+ message["content"] = (
198
+ f"{content}\n\n[lcode] To make room, lcode removed what you read earlier from: {names}. It isn't in "
199
+ "this conversation any more: read it again before answering anything about it, don't answer from memory."
200
+ )
201
+ return
202
+
203
+
127
204
  # ----------------------------------------------------------------------------- long output
128
205
 
129
206
 
lcode/repl.py CHANGED
@@ -242,7 +242,8 @@ def context_command(agent: Agent, arg: str, hardware: Hardware) -> None:
242
242
  c.print(
243
243
  f"In use: {format_tokens(agent.ctx_used)} of {format_tokens(s.context)} tokens "
244
244
  f"({100 * agent.ctx_used / s.context:.0f}%) in {len(agent.messages)} message"
245
- f"{'' if len(agent.messages) == 1 else 's'}; auto-compacts at 85%."
245
+ f"{'' if len(agent.messages) == 1 else 's'}; makes room at {format_tokens(agent.compact_limit())} "
246
+ + ("(compact_at)." if agent.compact_limit() < AUTO_COMPACT_RATIO * s.context else "(85%).")
246
247
  )
247
248
  if agent.mcp and agent.mcp.ready():
248
249
  how = "found on demand" if agent.mcp.searching(s.context) else "sent with every request"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.26.0
3
+ Version: 0.27.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,20 +1,20 @@
1
- lcode/__init__.py,sha256=xjCrbnr5jMJNipwGklkGM9ZGwF83DFfVEQPHFTwk3IY,473
1
+ lcode/__init__.py,sha256=JUcNjfBPUQX6hKCFSklgC04DhC6xrDnysFFhCU7p5Xc,473
2
2
  lcode/__main__.py,sha256=s5N5pryX2FAu93GGZ3FxUrIXHWGzxz66lKUoaV_h8Ws,35
3
3
  lcode/acp.py,sha256=Tpc8lsvffZSYbTRI4t7X4E2chahqZIPcKeYIcL5bV3Y,29035
4
4
  lcode/action.py,sha256=wuT5F9EJCdEligPeytzmEi8FpahWBhXer528tpuTj_k,16711
5
- lcode/agent.py,sha256=kAjvCq2XrXqNVyrwbgLhG0XOtXdY0TVk-ty2Oou6hnk,56297
6
- lcode/api.py,sha256=i6mxPCkD1ObrVQQeJ7LacEtsjuhQFdV21fM9FTj3hrg,15418
5
+ lcode/agent.py,sha256=tGNjbs8cw2X7IUBb70juI3JH5IpXmmIXOcteTsMhOtA,57936
6
+ lcode/api.py,sha256=aqWnGCy1jNuIktJEHo9ncbHNTXn2QO3BoNiggyo9ZmQ,15456
7
7
  lcode/backends.py,sha256=dtJHrC6ys73bRDg64z0tQz1PavmeAkiODZu1fOO9ExQ,20635
8
8
  lcode/bench.py,sha256=7ICipu_2GXW6vE519XeZucc2GSWA44votT8JvqcNv1k,40964
9
9
  lcode/bench_backend.py,sha256=X2fo-zRX92Xp3KMxSsFlWioc2vRupBTtowj5kLME8mw,14287
10
10
  lcode/bench_runtime.py,sha256=EjzNgIvUkdbT8cbSy1TpmlyeIk6e-ipjlDFpM7Dnqx0,6835
11
11
  lcode/catalog.py,sha256=pZ1azX9OH2r4JE2c6E5OfwywpgS39-W2oXzoCHJhmtM,4141
12
12
  lcode/checkpoints.py,sha256=eqPtmPe44-aweWqc2H21ROogwzIt3NpvcUobsaCHoGY,18354
13
- lcode/cli.py,sha256=cH5mcYi54Aofcv0PDNgFmFFQX8OQjhwTGdbWMUM_FG8,40289
13
+ lcode/cli.py,sha256=M6Mjd5BROSGlZj4J6TrSZMJqI-OVfUEWPoC0TQyOQ28,40831
14
14
  lcode/clipboard.py,sha256=nTSdJyCXPdtjH947RjvB3QvRqnL5HZsS9KB8EkC42cc,5138
15
15
  lcode/codesearch.py,sha256=Io8leN1nb_Y-Dvlm9xaGtwUWYXX-ooswuznJV6dGU1s,10766
16
- lcode/config.py,sha256=g7oT5ElKonmbR7_4itgJllTp81wGEYZy1fRZJnAvzOw,10481
17
- lcode/context.py,sha256=qJymLE3eYeaF6qVOzlUwDURqlky__8cskl_6vXeU3AA,10701
16
+ lcode/config.py,sha256=q1zGzH3p5U23ai6O9Jai3dS-s30CuQ9gc5cWX0_yxWE,11195
17
+ lcode/context.py,sha256=_f0NbXyjA4SyH2RCr49AiWwtAfr_PVDaqyFQ5fZDhkg,14404
18
18
  lcode/extensions.py,sha256=W0HFDl0BvROzr7i_qtabHdIbacBgP5o5qIJLzoM2oNE,16806
19
19
  lcode/frontmatter.py,sha256=Bx0n4vLPhp7ZCWx-g7xqzqNy5VoKBxyeeikh5UFB_gA,2441
20
20
  lcode/gitflow.py,sha256=6j_nnCBZXkX1qhTI7K4CyZ5D0NAZLpH96weFEo64AkQ,27913
@@ -31,7 +31,7 @@ lcode/permissions.py,sha256=i04YLtqYCxwApgawomPp4d35uk9HAJtpKY__gfw8j9s,6347
31
31
  lcode/planning.py,sha256=Q2upaoZPnXDMQT2F-ETxX1lk_wDntTzhSKNVYIQSoQA,6540
32
32
  lcode/render.py,sha256=gMvf4W3qkLUJ4Vot3az_hP_zRDAItJC9pP-68dHFkfA,1375
33
33
  lcode/repeats.py,sha256=uzyAewX03nJf5dKwYFH4y9ZDnh0Lvy2k0qC2vdntM5Y,4061
34
- lcode/repl.py,sha256=ZKd4l4wDGKHVaGz8bxjLGc2gRc2W-RpHPQYG9M2A_Zw,49441
34
+ lcode/repl.py,sha256=Y53tTvdoTP6CZOtq96CzbvPcwVlYNc9fppMV0hAlRSY,49573
35
35
  lcode/repomap.py,sha256=1GRJgEUjBl9XHSXsF7tSGXBCEgsY-3CzwMohaTudZ_M,13969
36
36
  lcode/sandbox.py,sha256=u_pkrVRXhCGhh-wFDPwuZ3jGG7UeXxXRAeBNsm37ZvE,8608
37
37
  lcode/secretscan.py,sha256=6Euvfk2gCtK7lZbuQD84zgWuSECi6OnSw8V5rFu1tfU,13575
@@ -57,8 +57,8 @@ lcode/servers/__init__.py,sha256=qrIhxEodEB8cjKEQ4Cicc0GsQhE2OH-9qlVHzr3-tq8,77
57
57
  lcode/servers/encord.py,sha256=WSauawL1UKYZCUAG1oalcKL_An3DVof-JM6xIgFyQSE,12769
58
58
  lcode/servers/valohai.py,sha256=e5xZGIoa4cja4f5i85q1GpL9c7cglUjV8jN2co1CFMY,14430
59
59
  lcode/servers/xai.py,sha256=h2emAvHSnkuB9WmJInlynsDhvxJwNVD2AHFX7vqMXNc,13627
60
- lcode_cli-0.26.0.dist-info/METADATA,sha256=iHy2mJa4Bk0LVY3hiDAkHxhBs2uqk21--IGY27MaePw,12620
61
- lcode_cli-0.26.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
62
- lcode_cli-0.26.0.dist-info/entry_points.txt,sha256=Hxsn_itbnbgft3U0FwxWdmEt0yoAV2LB-nJDnL9WT20,172
63
- lcode_cli-0.26.0.dist-info/licenses/LICENSE,sha256=JWjmoL7nNIC1XUa-dZm2hwLUgjUQMFo6k8APb0idNzs,1074
64
- lcode_cli-0.26.0.dist-info/RECORD,,
60
+ lcode_cli-0.27.0.dist-info/METADATA,sha256=pASpYtQ_MeN-50aD2sdpTfxMKuX2Tz00LaJa2pT1DmM,12620
61
+ lcode_cli-0.27.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
62
+ lcode_cli-0.27.0.dist-info/entry_points.txt,sha256=Hxsn_itbnbgft3U0FwxWdmEt0yoAV2LB-nJDnL9WT20,172
63
+ lcode_cli-0.27.0.dist-info/licenses/LICENSE,sha256=JWjmoL7nNIC1XUa-dZm2hwLUgjUQMFo6k8APb0idNzs,1074
64
+ lcode_cli-0.27.0.dist-info/RECORD,,