letscode 0.5.0__py3-none-any.whl → 0.6.1__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.
- letscode/agent/loop.py +3 -8
- letscode/agent/tools.py +3 -40
- letscode/cli/rpc.py +6 -4
- letscode/plugins/registries.py +54 -83
- {letscode-0.5.0.dist-info → letscode-0.6.1.dist-info}/METADATA +5 -4
- {letscode-0.5.0.dist-info → letscode-0.6.1.dist-info}/RECORD +8 -8
- {letscode-0.5.0.dist-info → letscode-0.6.1.dist-info}/WHEEL +1 -1
- {letscode-0.5.0.dist-info → letscode-0.6.1.dist-info}/entry_points.txt +0 -0
letscode/agent/loop.py
CHANGED
|
@@ -108,7 +108,9 @@ async def agent_loop( # noqa: PLR0913, C901 # central turn state machine — b
|
|
|
108
108
|
``env`` defaults to a fresh :class:`~letscode.agent.execution_env.LocalEnv`
|
|
109
109
|
so callers that don't care about the abstraction keep working.
|
|
110
110
|
"""
|
|
111
|
-
registry =
|
|
111
|
+
registry = ToolRegistry()
|
|
112
|
+
for tool_impl in state.tools:
|
|
113
|
+
registry.add(tool_impl)
|
|
112
114
|
resolved_env: ExecutionEnv = env if env is not None else LocalEnv()
|
|
113
115
|
|
|
114
116
|
state.messages.extend(inputs)
|
|
@@ -630,13 +632,6 @@ async def _close_aborted_run( # noqa: RUF029 # async generator: consumed via `
|
|
|
630
632
|
yield AgentEnd(messages=_snapshot(state.messages), error="aborted")
|
|
631
633
|
|
|
632
634
|
|
|
633
|
-
def _build_tool_registry(tools: list[Tool]) -> ToolRegistry:
|
|
634
|
-
registry = ToolRegistry()
|
|
635
|
-
for tool_impl in tools:
|
|
636
|
-
registry.add(tool_impl)
|
|
637
|
-
return registry
|
|
638
|
-
|
|
639
|
-
|
|
640
635
|
def _assemble_tool_calls(
|
|
641
636
|
names: dict[str, str],
|
|
642
637
|
args_strs: dict[str, str],
|
letscode/agent/tools.py
CHANGED
|
@@ -26,10 +26,11 @@ from pydantic import BaseModel, Field
|
|
|
26
26
|
from letscode.llm.types import (
|
|
27
27
|
ContentPart, # noqa: TC001 # Pydantic resolves at runtime
|
|
28
28
|
)
|
|
29
|
+
from letscode.plugins.registries import Registry
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
import asyncio
|
|
32
|
-
from collections.abc import Awaitable, Callable
|
|
33
|
+
from collections.abc import Awaitable, Callable
|
|
33
34
|
from pathlib import Path
|
|
34
35
|
|
|
35
36
|
from letscode.agent.execution_env import ExecutionEnv
|
|
@@ -164,13 +165,6 @@ def tool(
|
|
|
164
165
|
|
|
165
166
|
|
|
166
167
|
def _callable_name(fn: Callable[..., object]) -> str:
|
|
167
|
-
"""Best-effort name for any callable.
|
|
168
|
-
|
|
169
|
-
``Callable`` is broader than "function with ``__name__``" — a
|
|
170
|
-
``functools.partial``, bound-method-on-an-instance, or callable
|
|
171
|
-
class instance may not have it. Falls back to ``repr`` so error
|
|
172
|
-
messages stay informative.
|
|
173
|
-
"""
|
|
174
168
|
return getattr(fn, "__name__", repr(fn))
|
|
175
169
|
|
|
176
170
|
|
|
@@ -210,36 +204,5 @@ def _extract_params_model(
|
|
|
210
204
|
# ---------------------------------------------------------------------------
|
|
211
205
|
|
|
212
206
|
|
|
213
|
-
class ToolRegistry:
|
|
207
|
+
class ToolRegistry(Registry["Tool"]):
|
|
214
208
|
"""Mutable, name-keyed collection of registered tools."""
|
|
215
|
-
|
|
216
|
-
def __init__(self) -> None:
|
|
217
|
-
self._tools: dict[str, Tool] = {}
|
|
218
|
-
|
|
219
|
-
def add(self, tool: Tool, *, replace: bool = False) -> None:
|
|
220
|
-
"""Register ``tool`` under its ``name``.
|
|
221
|
-
|
|
222
|
-
Raises ``ValueError`` on duplicate names unless ``replace=True``.
|
|
223
|
-
"""
|
|
224
|
-
if tool.name in self._tools and not replace:
|
|
225
|
-
msg = (
|
|
226
|
-
f"Tool {tool.name!r} already registered. Pass replace=True to override."
|
|
227
|
-
)
|
|
228
|
-
raise ValueError(msg)
|
|
229
|
-
self._tools[tool.name] = tool
|
|
230
|
-
|
|
231
|
-
def get(self, name: str) -> Tool:
|
|
232
|
-
"""Look up a tool by name. Raises ``KeyError`` if absent."""
|
|
233
|
-
if name not in self._tools:
|
|
234
|
-
msg = f"Tool {name!r} not registered"
|
|
235
|
-
raise KeyError(msg)
|
|
236
|
-
return self._tools[name]
|
|
237
|
-
|
|
238
|
-
def __contains__(self, name: object) -> bool:
|
|
239
|
-
return name in self._tools
|
|
240
|
-
|
|
241
|
-
def __iter__(self) -> Iterator[Tool]:
|
|
242
|
-
return iter(self._tools.values())
|
|
243
|
-
|
|
244
|
-
def __len__(self) -> int:
|
|
245
|
-
return len(self._tools)
|
letscode/cli/rpc.py
CHANGED
|
@@ -14,7 +14,7 @@ import contextlib
|
|
|
14
14
|
import json
|
|
15
15
|
import sys
|
|
16
16
|
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
17
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Self
|
|
18
18
|
|
|
19
19
|
from letscode.agent.agent import Agent
|
|
20
20
|
from letscode.agent.events import MessageEnd
|
|
@@ -23,7 +23,7 @@ from letscode.llm.client import LLMClient
|
|
|
23
23
|
from letscode.session.store import save_message
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from collections.abc import
|
|
26
|
+
from collections.abc import Callable, Coroutine, Sequence
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
from typing import TextIO
|
|
29
29
|
|
|
@@ -154,7 +154,7 @@ class _RpcSession:
|
|
|
154
154
|
if handler is None:
|
|
155
155
|
self._error(cid, f"unknown command: {name!r}")
|
|
156
156
|
return False
|
|
157
|
-
return await handler(self, cmd, cid)
|
|
157
|
+
return await handler(self, cmd, cid) # type: ignore[arg-type] # mypy: Self vs _RpcSession in the table
|
|
158
158
|
|
|
159
159
|
async def _h_hello(self, cmd: dict[str, Any], cid: str | None) -> bool:
|
|
160
160
|
proto = cmd.get("protocol")
|
|
@@ -218,7 +218,9 @@ class _RpcSession:
|
|
|
218
218
|
return True
|
|
219
219
|
|
|
220
220
|
_HANDLERS: ClassVar[
|
|
221
|
-
dict[
|
|
221
|
+
dict[
|
|
222
|
+
str, Callable[[Self, dict[str, Any], str | None], Coroutine[Any, Any, bool]]
|
|
223
|
+
]
|
|
222
224
|
] = {
|
|
223
225
|
"hello": _h_hello,
|
|
224
226
|
"prompt": _h_prompt,
|
letscode/plugins/registries.py
CHANGED
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
-
from typing import TYPE_CHECKING
|
|
17
|
+
from typing import TYPE_CHECKING, Protocol
|
|
18
18
|
|
|
19
19
|
from letscode.skills.discovery import iter_skill_files
|
|
20
20
|
from letscode.skills.loader import Skill, parse_skill
|
|
@@ -30,6 +30,54 @@ if TYPE_CHECKING:
|
|
|
30
30
|
|
|
31
31
|
_logger = logging.getLogger(__name__)
|
|
32
32
|
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Generic name-keyed registry
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _HasName(Protocol):
|
|
39
|
+
"""Structural bound: any item with a ``name: str`` attribute."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Registry[T: _HasName]:
|
|
45
|
+
"""Name-keyed collection of registered items sharing a ``.name`` attribute.
|
|
46
|
+
|
|
47
|
+
Concrete registries (:class:`CommandRegistry`, :class:`ToolRegistry`,
|
|
48
|
+
:class:`FrontendRegistry`, :class:`SkillRegistry`) subclass this
|
|
49
|
+
to get their own type for ``isinstance`` checks and imports.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self._items: dict[str, T] = {}
|
|
54
|
+
|
|
55
|
+
def add(self, item: T, *, replace: bool = False) -> None:
|
|
56
|
+
"""Register ``item`` under its ``name``.
|
|
57
|
+
|
|
58
|
+
Raises ``ValueError`` on duplicate names unless ``replace=True``.
|
|
59
|
+
"""
|
|
60
|
+
if item.name in self._items and not replace:
|
|
61
|
+
msg = f"{item.name!r} already registered. Pass replace=True to override."
|
|
62
|
+
raise ValueError(msg)
|
|
63
|
+
self._items[item.name] = item
|
|
64
|
+
|
|
65
|
+
def get(self, name: str) -> T:
|
|
66
|
+
"""Look up an item by name. Raises ``KeyError`` if absent."""
|
|
67
|
+
if name not in self._items:
|
|
68
|
+
msg = f"{name!r} not registered"
|
|
69
|
+
raise KeyError(msg)
|
|
70
|
+
return self._items[name]
|
|
71
|
+
|
|
72
|
+
def __contains__(self, name: object) -> bool:
|
|
73
|
+
return name in self._items
|
|
74
|
+
|
|
75
|
+
def __iter__(self) -> Iterator[T]:
|
|
76
|
+
return iter(self._items.values())
|
|
77
|
+
|
|
78
|
+
def __len__(self) -> int:
|
|
79
|
+
return len(self._items)
|
|
80
|
+
|
|
33
81
|
|
|
34
82
|
# ---------------------------------------------------------------------------
|
|
35
83
|
# Value types
|
|
@@ -77,6 +125,7 @@ __all__ = [
|
|
|
77
125
|
"CommandRegistry",
|
|
78
126
|
"FrontendFactory",
|
|
79
127
|
"FrontendRegistry",
|
|
128
|
+
"Registry",
|
|
80
129
|
"Skill",
|
|
81
130
|
"SkillRegistry",
|
|
82
131
|
]
|
|
@@ -87,38 +136,11 @@ __all__ = [
|
|
|
87
136
|
# ---------------------------------------------------------------------------
|
|
88
137
|
|
|
89
138
|
|
|
90
|
-
class CommandRegistry:
|
|
139
|
+
class CommandRegistry(Registry[Command]):
|
|
91
140
|
"""Name-keyed registry of slash commands."""
|
|
92
141
|
|
|
93
|
-
def __init__(self) -> None:
|
|
94
|
-
self._items: dict[str, Command] = {}
|
|
95
|
-
|
|
96
|
-
def add(self, command: Command, *, replace: bool = False) -> None:
|
|
97
|
-
if command.name in self._items and not replace:
|
|
98
|
-
msg = (
|
|
99
|
-
f"Command {command.name!r} already registered. "
|
|
100
|
-
"Pass replace=True to override."
|
|
101
|
-
)
|
|
102
|
-
raise ValueError(msg)
|
|
103
|
-
self._items[command.name] = command
|
|
104
142
|
|
|
105
|
-
|
|
106
|
-
if name not in self._items:
|
|
107
|
-
msg = f"Command {name!r} not registered"
|
|
108
|
-
raise KeyError(msg)
|
|
109
|
-
return self._items[name]
|
|
110
|
-
|
|
111
|
-
def __contains__(self, name: object) -> bool:
|
|
112
|
-
return name in self._items
|
|
113
|
-
|
|
114
|
-
def __iter__(self) -> Iterator[Command]:
|
|
115
|
-
return iter(self._items.values())
|
|
116
|
-
|
|
117
|
-
def __len__(self) -> int:
|
|
118
|
-
return len(self._items)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class SkillRegistry:
|
|
143
|
+
class SkillRegistry(Registry[Skill]):
|
|
122
144
|
"""Name-keyed registry of skills plus a list of source directories.
|
|
123
145
|
|
|
124
146
|
Plugins typically call :meth:`add_source` to register a directory
|
|
@@ -127,18 +149,9 @@ class SkillRegistry:
|
|
|
127
149
|
"""
|
|
128
150
|
|
|
129
151
|
def __init__(self) -> None:
|
|
130
|
-
|
|
152
|
+
super().__init__()
|
|
131
153
|
self._sources: list[Path] = []
|
|
132
154
|
|
|
133
|
-
def add(self, skill: Skill, *, replace: bool = False) -> None:
|
|
134
|
-
if skill.name in self._items and not replace:
|
|
135
|
-
msg = (
|
|
136
|
-
f"Skill {skill.name!r} already registered. "
|
|
137
|
-
"Pass replace=True to override."
|
|
138
|
-
)
|
|
139
|
-
raise ValueError(msg)
|
|
140
|
-
self._items[skill.name] = skill
|
|
141
|
-
|
|
142
155
|
def add_source(self, path: Path) -> None:
|
|
143
156
|
"""Register a directory the loader should scan for ``SKILL.md``."""
|
|
144
157
|
if path not in self._sources:
|
|
@@ -192,51 +205,9 @@ class SkillRegistry:
|
|
|
192
205
|
"""Read-only view of registered source directories (insertion order)."""
|
|
193
206
|
return tuple(self._sources)
|
|
194
207
|
|
|
195
|
-
def get(self, name: str) -> Skill:
|
|
196
|
-
if name not in self._items:
|
|
197
|
-
msg = f"Skill {name!r} not registered"
|
|
198
|
-
raise KeyError(msg)
|
|
199
|
-
return self._items[name]
|
|
200
|
-
|
|
201
|
-
def __contains__(self, name: object) -> bool:
|
|
202
|
-
return name in self._items
|
|
203
|
-
|
|
204
|
-
def __iter__(self) -> Iterator[Skill]:
|
|
205
|
-
return iter(self._items.values())
|
|
206
|
-
|
|
207
|
-
def __len__(self) -> int:
|
|
208
|
-
return len(self._items)
|
|
209
|
-
|
|
210
208
|
|
|
211
|
-
class FrontendRegistry:
|
|
209
|
+
class FrontendRegistry(Registry[FrontendFactory]):
|
|
212
210
|
"""Name-keyed registry of frontend factories.
|
|
213
211
|
|
|
214
212
|
The CLI's ``--frontend NAME`` flag (added in T20) selects from here.
|
|
215
213
|
"""
|
|
216
|
-
|
|
217
|
-
def __init__(self) -> None:
|
|
218
|
-
self._items: dict[str, FrontendFactory] = {}
|
|
219
|
-
|
|
220
|
-
def add(self, frontend: FrontendFactory, *, replace: bool = False) -> None:
|
|
221
|
-
if frontend.name in self._items and not replace:
|
|
222
|
-
msg = (
|
|
223
|
-
f"Frontend {frontend.name!r} already registered. "
|
|
224
|
-
"Pass replace=True to override."
|
|
225
|
-
)
|
|
226
|
-
raise ValueError(msg)
|
|
227
|
-
self._items[frontend.name] = frontend
|
|
228
|
-
|
|
229
|
-
def get(self, name: str) -> FrontendFactory:
|
|
230
|
-
if name not in self._items:
|
|
231
|
-
msg = f"Frontend {name!r} not registered"
|
|
232
|
-
raise KeyError(msg)
|
|
233
|
-
return self._items[name]
|
|
234
|
-
|
|
235
|
-
def __contains__(self, name: object) -> bool:
|
|
236
|
-
return name in self._items
|
|
237
|
-
|
|
238
|
-
def __iter__(self) -> Iterator[FrontendFactory]:
|
|
239
|
-
return iter(self._items.values())
|
|
240
|
-
|
|
241
|
-
def __len__(self) -> int:
|
|
242
|
-
return len(self._items)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: letscode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: A minimal coding agent for the terminal.
|
|
5
|
-
Project-URL: Homepage, https://git.sr.ht/~sfermigier/letscode
|
|
6
|
-
Project-URL: Source Code, https://git.sr.ht/~sfermigier/letscode
|
|
7
5
|
Author-email: Stefane Fermigier <sf@abilian.com>
|
|
8
6
|
Requires-Python: >=3.12
|
|
9
7
|
Requires-Dist: aiofiles>=24.0
|
|
@@ -13,6 +11,7 @@ Requires-Dist: openai>=1.0
|
|
|
13
11
|
Requires-Dist: pluggy>=1.5
|
|
14
12
|
Requires-Dist: prompt-toolkit>=3.0
|
|
15
13
|
Requires-Dist: pydantic>=2.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
15
|
Requires-Dist: rich>=13.0
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
17
|
|
|
@@ -20,10 +19,12 @@ Description-Content-Type: text/markdown
|
|
|
20
19
|
|
|
21
20
|
A minimal, OpenAI-compatible coding agent for the terminal — written in Python. Point it at any OpenAI-API-compatible endpoint (Ollama, Fireworks, OpenRouter, vLLM, llama.cpp's `llama-server`, …) and get a streaming agent loop with the four tools that cover 95% of coding sessions — `read`, `write`, `edit`, `bash` — plus skills, slash commands, and a plugin system you can extend.
|
|
22
21
|
|
|
23
|
-
**Status:** v0.
|
|
22
|
+
**Status:** v0.6, alpha.
|
|
24
23
|
|
|
25
24
|
The full docs live in [`docs/`](docs/) and build via [Zensical](https://zensical.org) — run `make docs-serve` for a live preview, or `make docs` for a static build.
|
|
26
25
|
|
|
26
|
+
Or go to <https://letscode.hop3.abilian.com> for the online doc.
|
|
27
|
+
|
|
27
28
|
## What it does
|
|
28
29
|
|
|
29
30
|
- **Streaming agent loop** with parallel tool execution.
|
|
@@ -6,13 +6,13 @@ letscode/agent/events.py,sha256=FC0ZjvBF4NofxG57O-6Hc_XruqwurRLBF-sUzUd0P7g,3926
|
|
|
6
6
|
letscode/agent/execution_env.py,sha256=QCFOeDi-mxRvgLKG_Zm--v7Ns1FbESi7birlLVaNWgI,11438
|
|
7
7
|
letscode/agent/hooks.py,sha256=5XnFYZbM3rBHlnLBYjylM58j588_11Tsms1o4LPXMx8,1479
|
|
8
8
|
letscode/agent/hookspecs.py,sha256=LwAjGdJXdbQmx5fEW-XLWt8BKry-e39ekZSvJmprmkY,7929
|
|
9
|
-
letscode/agent/loop.py,sha256=
|
|
9
|
+
letscode/agent/loop.py,sha256=2XyUADNDdfabilvqG4uH1DrMvOQ22Gcp7lKbyldiWNQ,24076
|
|
10
10
|
letscode/agent/messages.py,sha256=ceBMb8k_uJDskqCCbZuKlRWmyL2Pwuc_LlYt7iaMn6c,3290
|
|
11
11
|
letscode/agent/state.py,sha256=O4cUWs6nu51tGIZ93qCRNyIhpfWETT3IKAtqUV5MvEI,2052
|
|
12
|
-
letscode/agent/tools.py,sha256=
|
|
12
|
+
letscode/agent/tools.py,sha256=B5Q3nhkv2-VRYWVSsXm7WOY1NDFT0RtWjGrfmiSExtQ,6953
|
|
13
13
|
letscode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
letscode/cli/app.py,sha256=sfDaTO_6vNOHTvIYzR6U5MA_BQQbd-ELTpZZ1OkoCPY,21159
|
|
15
|
-
letscode/cli/rpc.py,sha256=
|
|
15
|
+
letscode/cli/rpc.py,sha256=N8h1vW8Ira-GkIORsxHZY4XWOr4iXD4MnT0ZZG5KfSw,10976
|
|
16
16
|
letscode/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
letscode/commands/builtin.py,sha256=2-Kqu0l-eW6b3-NYRjAqKrb5iz5d2dmS53qRAVH_XRU,12615
|
|
18
18
|
letscode/commands/dispatch.py,sha256=sxr3z5NlErG4W5fwdTVIjTEK9gzID3NitwHRDHKHDcs,4080
|
|
@@ -34,7 +34,7 @@ letscode/llm/types.py,sha256=yjPyZzZ983vbRTOWTD5hexTuHCNeWMVJtwzvRpAvfAg,1377
|
|
|
34
34
|
letscode/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
35
|
letscode/plugins/builtin.py,sha256=JRIlv2fD3i0rliKSgzTEXI37my_HcVffkmsZfXGdPDc,4552
|
|
36
36
|
letscode/plugins/manager.py,sha256=u2hgbt5nIW8_MKeofy-XmPyDAN0bcrtrVcWDIC9jFO4,2760
|
|
37
|
-
letscode/plugins/registries.py,sha256=
|
|
37
|
+
letscode/plugins/registries.py,sha256=S9igLcStCbHCGeg6d4IPN-j9z5J3K0ssNS90-dh8G-k,7496
|
|
38
38
|
letscode/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
39
|
letscode/session/format.py,sha256=XIqQ7Z4yRzNghoXgI2V7J6uyKXL4oLIZmHg8rSM7CAM,2714
|
|
40
40
|
letscode/session/store.py,sha256=m0llMEq6qhT4vMMRy2kjgpGD4YdXX_7hwalPf4OdnH4,6732
|
|
@@ -48,7 +48,7 @@ letscode/tools/builtin.py,sha256=bx-nPUwESAHTam4ts13BXgJYoZbx8KBC8estBe6YuqM,653
|
|
|
48
48
|
letscode/tools/edit.py,sha256=QyCeouEco9NsO7aAZao3xzQcYTi4um8RCpHXawtuJHU,4319
|
|
49
49
|
letscode/tools/read.py,sha256=TJ4GFBu-AahNu7fByAB1x9mFW3O-j_ikh3usF3znHQk,3129
|
|
50
50
|
letscode/tools/write.py,sha256=JSBoKqwZ6OZ0Ut34__WiCqVzwREx3vxYOGsRNpClZm8,2445
|
|
51
|
-
letscode-0.
|
|
52
|
-
letscode-0.
|
|
53
|
-
letscode-0.
|
|
54
|
-
letscode-0.
|
|
51
|
+
letscode-0.6.1.dist-info/METADATA,sha256=Ui51BAgKqJy7M7pHqYTxomORl-UW3PRxnB4bHZxkYVQ,11934
|
|
52
|
+
letscode-0.6.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
53
|
+
letscode-0.6.1.dist-info/entry_points.txt,sha256=7wTui6fYLJ-_V7rwBMFloXcRbc7rYTPkcvVwZ7SV9p8,51
|
|
54
|
+
letscode-0.6.1.dist-info/RECORD,,
|
|
File without changes
|