meshagent-tools 0.25.0__py3-none-any.whl → 0.25.2__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.
@@ -31,6 +31,19 @@ from .hosting import (
31
31
  )
32
32
  from .multi_tool import MultiTool, MultiToolkit
33
33
  from .version import __version__
34
+ from .web_toolkit import (
35
+ WebFetchConfig,
36
+ WebFetchTool,
37
+ WebFetchToolkitBuilder,
38
+ WebToolkit,
39
+ )
40
+ from .container_shell import (
41
+ ContainerShellToolConfig,
42
+ ContainerShellToolkitBuilder,
43
+ ContainerShellTool,
44
+ )
45
+
46
+ from .script import ScriptTool, ScriptToolConfig, ScriptToolkitBuilder
34
47
 
35
48
  from meshagent.api import websocket_protocol, RoomClient, ParticipantToken
36
49
  from meshagent.api.websocket_protocol import WebSocketClientProtocol
@@ -66,5 +79,15 @@ __all__ = [
66
79
  make_toolkits,
67
80
  ToolkitConfig,
68
81
  get_bytes_from_url,
82
+ WebFetchConfig,
83
+ WebFetchTool,
84
+ WebFetchToolkitBuilder,
85
+ WebToolkit,
86
+ ContainerShellToolConfig,
87
+ ContainerShellToolkitBuilder,
88
+ ContainerShellTool,
89
+ ScriptTool,
90
+ ScriptToolConfig,
91
+ ScriptToolkitBuilder,
69
92
  __version__,
70
93
  ]
@@ -0,0 +1,226 @@
1
+ from meshagent.api import RoomClient
2
+ from .tool import ToolContext, Tool
3
+ from .toolkit import Toolkit, ToolkitBuilder
4
+
5
+ from meshagent.api.specs.service import ContainerMountSpec, RoomStorageMountSpec
6
+ from typing import Literal, Optional
7
+ import os
8
+
9
+ import logging
10
+ import asyncio
11
+ from pydantic import BaseModel
12
+
13
+ logger = logging.getLogger("container_shell_tool")
14
+
15
+
16
+ DEFAULT_CONTAINER_MOUNT_SPEC = ContainerMountSpec(
17
+ room=[RoomStorageMountSpec(path="/data")]
18
+ )
19
+
20
+
21
+ class ContainerShellToolConfig(BaseModel):
22
+ name: Literal["container_shell"] = "container_shell"
23
+
24
+
25
+ class ContainerShellToolkitBuilder(ToolkitBuilder):
26
+ def __init__(
27
+ self,
28
+ *,
29
+ name: str = "container_shell",
30
+ working_directory: Optional[str] = None,
31
+ image: Optional[str] = "python:3.13",
32
+ mounts: Optional[ContainerMountSpec] = DEFAULT_CONTAINER_MOUNT_SPEC,
33
+ env: Optional[dict[str, str]] = None,
34
+ ):
35
+ super().__init__(name=name, type=ContainerShellToolConfig)
36
+
37
+ self.working_directory = working_directory
38
+ self.image = image
39
+ self.mounts = mounts
40
+ self.env = env
41
+
42
+ async def make(
43
+ self, *, room: RoomClient, model: str, config: ContainerShellToolConfig
44
+ ) -> Toolkit:
45
+ return Toolkit(
46
+ name=self.name,
47
+ tools=[
48
+ ContainerShellTool(
49
+ name=self.name,
50
+ working_directory=self.working_directory,
51
+ image=self.image,
52
+ mounts=self.mounts,
53
+ env=self.env,
54
+ )
55
+ ],
56
+ )
57
+
58
+
59
+ class ContainerShellTool(Tool):
60
+ def __init__(
61
+ self,
62
+ *,
63
+ name: str = "container_shell",
64
+ description: Optional[str] = None,
65
+ title: Optional[str] = None,
66
+ working_directory: Optional[str] = None,
67
+ image: Optional[str] = "python:3.13",
68
+ mounts: Optional[ContainerMountSpec] = DEFAULT_CONTAINER_MOUNT_SPEC,
69
+ env: Optional[dict[str, str]] = None,
70
+ ):
71
+ self.working_directory = working_directory
72
+ self.image = image
73
+ self.mounts = mounts
74
+ self._container_id = None
75
+ self.env = env
76
+
77
+ super().__init__(
78
+ name=name,
79
+ description=description
80
+ or "execute shell commands in a container and return the result",
81
+ title=title,
82
+ input_schema={
83
+ "type": "object",
84
+ "required": ["commands"],
85
+ "additionalProperties": False,
86
+ "properties": {
87
+ "commands": {"type": "array", "items": {"type": "string"}},
88
+ "max_output_length": {"type": "integer"},
89
+ "timeout_ms": {"type": "integer"},
90
+ },
91
+ },
92
+ )
93
+
94
+ async def execute(
95
+ self,
96
+ context: ToolContext,
97
+ **kwargs,
98
+ ):
99
+ commands = kwargs.get("commands") or []
100
+ max_output_length = kwargs.get("max_output_length")
101
+ timeout_ms = kwargs.get("timeout_ms")
102
+
103
+ if not commands:
104
+ raise Exception("commands is required")
105
+
106
+ if self.image is None:
107
+ raise Exception("container_shell requires an image")
108
+
109
+ results = []
110
+ encoding = os.device_encoding(1) or "utf-8"
111
+
112
+ left = max_output_length
113
+
114
+ def limit(s: str):
115
+ nonlocal left
116
+ if left is not None:
117
+ s = s[0:left]
118
+ left -= len(s)
119
+ return s
120
+ else:
121
+ return s
122
+
123
+ timeout = float(timeout_ms) / 1000.0 if timeout_ms else 20 * 1000.0
124
+
125
+ running = False
126
+
127
+ if self._container_id:
128
+ for c in await context.room.containers.list():
129
+ if c.id == self._container_id:
130
+ running = True
131
+
132
+ if not running:
133
+ self._container_id = await context.room.containers.run(
134
+ command="sleep infinity",
135
+ image=self.image,
136
+ mounts=self.mounts,
137
+ writable_root_fs=True,
138
+ env=self.env,
139
+ )
140
+
141
+ container_id = self._container_id
142
+
143
+ try:
144
+ logger.info(
145
+ "executing shell commands in container %s with timeout %s: %s",
146
+ container_id,
147
+ timeout,
148
+ commands,
149
+ )
150
+ import shlex
151
+
152
+ for command in commands:
153
+ command_to_run = command
154
+ if self.working_directory:
155
+ command_to_run = (
156
+ f"cd {shlex.quote(self.working_directory)} && {command}"
157
+ )
158
+ exec = await context.room.containers.exec(
159
+ container_id=container_id,
160
+ command=shlex.join(["bash", "-lc", command_to_run]),
161
+ tty=False,
162
+ )
163
+
164
+ stdout = bytearray()
165
+ stderr = bytearray()
166
+
167
+ try:
168
+ async with asyncio.timeout(timeout):
169
+ async for se in exec.stderr():
170
+ stderr.extend(se)
171
+
172
+ async for so in exec.stdout():
173
+ stdout.extend(so)
174
+
175
+ exit_code = await exec.result
176
+
177
+ results.append(
178
+ {
179
+ "outcome": {
180
+ "type": "exit",
181
+ "exit_code": exit_code,
182
+ },
183
+ "stdout": stdout.decode(),
184
+ "stderr": stderr.decode(),
185
+ }
186
+ )
187
+
188
+ except asyncio.TimeoutError:
189
+ logger.info("The command timed out after %ss", timeout)
190
+ await exec.kill()
191
+
192
+ results.append(
193
+ {
194
+ "outcome": {"type": "timeout"},
195
+ "stdout": limit(stdout.decode(encoding, errors="replace")),
196
+ "stderr": limit(stderr.decode(encoding, errors="replace")),
197
+ }
198
+ )
199
+ break
200
+
201
+ except Exception as ex:
202
+ results.append(
203
+ {
204
+ "outcome": {
205
+ "type": "exit",
206
+ "exit_code": 1,
207
+ },
208
+ "stdout": "",
209
+ "stderr": f"{ex}",
210
+ }
211
+ )
212
+ break
213
+
214
+ except Exception as ex:
215
+ results.append(
216
+ {
217
+ "outcome": {
218
+ "type": "exit",
219
+ "exit_code": 1,
220
+ },
221
+ "stdout": "",
222
+ "stderr": f"{ex}",
223
+ }
224
+ )
225
+
226
+ return {"results": results}
meshagent/tools/script.py CHANGED
@@ -1,9 +1,7 @@
1
1
  from meshagent.api import RoomClient
2
- from meshagent.tools import Toolkit, ToolContext, Tool
2
+ from meshagent.tools.tool import Tool, ToolContext
3
+ from meshagent.tools.toolkit import Toolkit, ToolkitBuilder
3
4
 
4
- from meshagent.agents.adapter import (
5
- ToolkitBuilder,
6
- )
7
5
 
8
6
  from meshagent.api.specs.service import ContainerMountSpec, RoomStorageMountSpec
9
7
  from typing import Literal
@@ -36,6 +36,31 @@ async def test_decorated_tool_executes_with_toolkit():
36
36
  assert result.json == {"name": "alpha", "count": 2, "flag": True}
37
37
 
38
38
 
39
+ class Greeter:
40
+ def __init__(self, prefix: str) -> None:
41
+ self._prefix = prefix
42
+
43
+ @tool(name="greet")
44
+ def greet(self, *, name: str) -> str:
45
+ return f"{self._prefix}{name}"
46
+
47
+
48
+ @pytest.mark.asyncio
49
+ async def test_decorated_method_executes_with_toolkit():
50
+ greeter = Greeter("hello ")
51
+ toolkit = Toolkit(name="test", tools=[greeter.greet])
52
+ context = ToolContext(room=object(), caller=object())
53
+
54
+ result = await toolkit.execute(
55
+ context=context,
56
+ name="greet",
57
+ arguments={"name": "mesh"},
58
+ )
59
+
60
+ assert isinstance(result, JsonResponse)
61
+ assert result.json == "hello mesh"
62
+
63
+
39
64
  def test_decorator_schema_is_strict():
40
65
  schema = make_payload.input_schema
41
66
 
meshagent/tools/tool.py CHANGED
@@ -147,7 +147,16 @@ def tool(
147
147
  supports_context = False
148
148
  fields: dict[str, tuple[Any, Any]] = {}
149
149
 
150
- for param_name, param in signature.parameters.items():
150
+ parameters = list(signature.parameters.items())
151
+ bound_param_name = None
152
+ if parameters:
153
+ first_param_name, _first_param = parameters[0]
154
+ if first_param_name in ("self", "cls"):
155
+ bound_param_name = first_param_name
156
+
157
+ for param_name, param in parameters:
158
+ if bound_param_name == param_name:
159
+ continue
151
160
  annotation = hints.get(param_name, Any)
152
161
  if annotation is ToolContext:
153
162
  supports_context = True
@@ -167,7 +176,7 @@ def tool(
167
176
  )
168
177
 
169
178
  class FunctionTool(Tool):
170
- def __init__(self):
179
+ def __init__(self, bound_instance: Optional[object] = None):
171
180
  super().__init__(
172
181
  name=tool_name,
173
182
  title=tool_title,
@@ -178,6 +187,20 @@ def tool(
178
187
  supports_context=supports_context,
179
188
  )
180
189
  self.strict = True
190
+ self._bound_instance = bound_instance
191
+
192
+ def __get__(self, instance, owner):
193
+ if instance is None:
194
+ if bound_param_name == "cls" and owner is not None:
195
+ if self._bound_instance is owner:
196
+ return self
197
+ return FunctionTool(bound_instance=owner)
198
+ return self
199
+
200
+ if self._bound_instance is instance:
201
+ return self
202
+
203
+ return FunctionTool(bound_instance=instance)
181
204
 
182
205
  async def invoke(
183
206
  self,
@@ -188,10 +211,18 @@ def tool(
188
211
  data = InputModel.model_validate(arguments)
189
212
  parsed_args = {field: getattr(data, field) for field in fields}
190
213
 
214
+ bound_instance = self._bound_instance
215
+
191
216
  if supports_context:
192
- result = fn(context, **parsed_args)
217
+ if bound_instance is not None:
218
+ result = fn(bound_instance, context, **parsed_args)
219
+ else:
220
+ result = fn(context, **parsed_args)
193
221
  else:
194
- result = fn(**parsed_args)
222
+ if bound_instance is not None:
223
+ result = fn(bound_instance, **parsed_args)
224
+ else:
225
+ result = fn(**parsed_args)
195
226
 
196
227
  if inspect.isawaitable(result):
197
228
  result = await result
@@ -1 +1 @@
1
- __version__ = "0.25.0"
1
+ __version__ = "0.25.2"
@@ -1,19 +1,29 @@
1
- from meshagent.api.messaging import TextResponse
2
- from meshagent.tools import Toolkit, Tool, ToolContext
3
- from aiohttp import ClientSession
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import mimetypes
5
+ import os
6
+ from urllib.parse import urlparse
7
+
8
+ from meshagent.api.http import new_client_session
9
+ from meshagent.api.messaging import FileResponse, JsonResponse, Response, TextResponse
10
+ from meshagent.tools.config import ToolkitConfig
11
+ from meshagent.tools.tool import Tool, ToolContext
12
+ from meshagent.tools.toolkit import Toolkit, ToolkitBuilder
13
+ from meshagent.api.room_server_client import RoomClient
4
14
 
5
15
 
6
16
  class WebToolkit(Toolkit):
7
17
  def __init__(self):
8
- super().__init__(tools=[WebFetchTool()])
18
+ super().__init__(name="web_fetch", tools=[WebFetchTool()])
9
19
 
10
20
 
11
21
  class WebFetchTool(Tool):
12
22
  def __init__(self):
13
23
  super().__init__(
14
- name="get_web_page_text",
15
- title="get web page text",
16
- description="gets the text of a web page",
24
+ name="web_fetch",
25
+ title="web fetch",
26
+ description="fetches a url and returns text, json, or file content",
17
27
  input_schema={
18
28
  "type": "object",
19
29
  "properties": {
@@ -22,21 +32,104 @@ class WebFetchTool(Tool):
22
32
  "description": "the url of the web page (always start it with a proper scheme like https://)",
23
33
  }
24
34
  },
35
+ "required": ["url"],
25
36
  "additionalProperties": False,
26
37
  },
27
38
  )
28
39
 
29
- async def execute(
30
- self,
31
- *,
32
- context: ToolContext,
33
- url: str,
34
- ) -> str:
35
- async with ClientSession() as session:
40
+ async def execute(self, context: ToolContext, **kwargs: object) -> Response:
41
+ url = str(kwargs.get("url", ""))
42
+ if not url:
43
+ raise ValueError("url is required")
44
+ async with new_client_session() as session:
36
45
  async with session.get(url) as resp:
37
- body = await resp.text()
46
+ if resp.status >= 400:
47
+ raise Exception(f"web fetch failed with status {resp.status}")
48
+
49
+ content_type = (resp.content_type or "").lower()
50
+ data = await resp.read()
51
+
52
+ if _is_json_content_type(content_type):
53
+ text = _decode_text(data=data, charset=resp.charset)
54
+ try:
55
+ parsed = json.loads(text)
56
+ except json.JSONDecodeError:
57
+ return TextResponse(text=text)
58
+
59
+ if isinstance(parsed, dict):
60
+ return JsonResponse(json=parsed)
61
+ return JsonResponse(json={"data": parsed})
62
+
63
+ if _is_text_content_type(content_type):
64
+ text = _decode_text(data=data, charset=resp.charset)
65
+ if content_type == "text/html":
66
+ from html_to_markdown import convert
67
+
68
+ text = convert(text)
69
+ return TextResponse(text=text)
70
+
71
+ if _is_file_content_type(content_type):
72
+ filename = _infer_filename(url=url, content_type=content_type)
73
+ return FileResponse(
74
+ name=filename,
75
+ mime_type=content_type or "application/octet-stream",
76
+ data=data,
77
+ )
78
+
79
+ filename = _infer_filename(url=url, content_type=content_type)
80
+ return FileResponse(
81
+ name=filename,
82
+ mime_type=content_type or "application/octet-stream",
83
+ data=data,
84
+ )
85
+
86
+
87
+ def _decode_text(*, data: bytes, charset: str | None) -> str:
88
+ encoding = charset or "utf-8"
89
+ return data.decode(encoding, errors="replace")
90
+
38
91
 
39
- from bs4 import BeautifulSoup
92
+ def _is_json_content_type(content_type: str) -> bool:
93
+ if content_type in {"application/json", "text/json"}:
94
+ return True
95
+ return content_type.endswith("+json")
96
+
97
+
98
+ def _is_text_content_type(content_type: str) -> bool:
99
+ if content_type.startswith("text/"):
100
+ return True
101
+ return content_type in {
102
+ "application/xml",
103
+ "application/xhtml+xml",
104
+ "application/javascript",
105
+ "application/x-javascript",
106
+ }
107
+
108
+
109
+ def _is_file_content_type(content_type: str) -> bool:
110
+ if content_type.startswith("image/"):
111
+ return True
112
+ return content_type == "application/pdf"
113
+
114
+
115
+ def _infer_filename(*, url: str, content_type: str) -> str:
116
+ parsed = urlparse(url)
117
+ basename = os.path.basename(parsed.path)
118
+ if basename:
119
+ return basename
120
+ extension = mimetypes.guess_extension(content_type or "") or ""
121
+ return f"downloaded-content{extension}"
122
+
123
+
124
+ class WebFetchConfig(ToolkitConfig):
125
+ name: str = "web_fetch"
126
+
127
+
128
+ class WebFetchToolkitBuilder(ToolkitBuilder):
129
+ def __init__(self):
130
+ super().__init__(name="web_fetch", type=WebFetchConfig)
40
131
 
41
- soup = BeautifulSoup(body, "html.parser")
42
- return TextResponse(text=soup.get_text())
132
+ async def make(
133
+ self, *, room: RoomClient, model: str, config: WebFetchConfig
134
+ ) -> Toolkit:
135
+ return WebToolkit()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-tools
3
- Version: 0.25.0
3
+ Version: 0.25.2
4
4
  Summary: Tools for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -12,7 +12,8 @@ License-File: LICENSE
12
12
  Requires-Dist: pyjwt~=2.10
13
13
  Requires-Dist: pytest~=8.4
14
14
  Requires-Dist: pytest-asyncio~=0.26
15
- Requires-Dist: meshagent-api~=0.25.0
15
+ Requires-Dist: meshagent-api~=0.25.2
16
+ Requires-Dist: html-to-markdown~=2.24.3
16
17
  Requires-Dist: aiohttp[speedups]~=3.13.0
17
18
  Requires-Dist: opentelemetry-distro~=0.54b1
18
19
  Dynamic: license-file
@@ -1,6 +1,7 @@
1
- meshagent/tools/__init__.py,sha256=hjbQMnyowCiTZ9gS0rb1EP3AfeuPxRRjnUhtCCtzfyc,1305
1
+ meshagent/tools/__init__.py,sha256=2qAAi4eUVq4q6gXyAGnjQze2Rs9Q9kIJfdm4oEheRPQ,1843
2
2
  meshagent/tools/blob.py,sha256=Liw1zdtDc-Viy6Hcgrb4XZ8ULUPEubzShWAdIci0wWI,1505
3
3
  meshagent/tools/config.py,sha256=zH2xGxg28K7Tg-aYor6LXdzf0LRxS9iE0679H1FuWhE,79
4
+ meshagent/tools/container_shell.py,sha256=BMcTi_j0-yUPk9ZkVux1j8CxDFmyrCYFn-XeT3S77Ik,7056
4
5
  meshagent/tools/database.py,sha256=HWH7_Fm_8GexDJv_T-D6o0yJVUPmE-j31TU2AfcgaPs,18438
5
6
  meshagent/tools/datetime.py,sha256=2pOUOWopYIsc5y4EoFo_1PdBaBcTSkeOOs_EqdqYTk0,17503
6
7
  meshagent/tools/discovery.py,sha256=f7DJtwIiBQCxByTepsvGM2NRn-9KGxZTZMoTRCKYQ7E,1251
@@ -8,17 +9,17 @@ meshagent/tools/document_tools.py,sha256=LMULXOSBjsvhKjqzxUxe8586t0Vol0v1Btu5v6o
8
9
  meshagent/tools/hosting.py,sha256=l1BCgnSrCJQsWU9Kycq3hEI4ZlYxffDfde6QeJUfko0,10678
9
10
  meshagent/tools/multi_tool.py,sha256=hmWZO18Y2tuFG_7rvUed9er29aXleAC-r3YpXBCZWUY,4040
10
11
  meshagent/tools/pydantic.py,sha256=n-MD0gC-oRtHSTUDD5IV2dP-xIk-zjcDgHfgjqMgiqM,1161
11
- meshagent/tools/script.py,sha256=CoDfIRw-XPCcYO9bl8NZcPZpGsR-HGkmOc75q-xdU6k,11816
12
+ meshagent/tools/script.py,sha256=eyQiufoc2ZkTUBTO58VQURnkkQA2lboED5s0-BvAvgM,11811
12
13
  meshagent/tools/storage.py,sha256=NVpi9CZKSZUh8PTxxCdJhJy7Gzmdp55-zo2yHYGod_E,23340
13
14
  meshagent/tools/strict_schema.py,sha256=IytdAANa6lsfrsg5FsJuqYrxH9D_fayl-Lc9EwgLJSM,6277
14
- meshagent/tools/tool.py,sha256=HgvlOlz2wMrmD5aaV49fRpnGyXJwnVcH9j4wKaaPbWo,5935
15
+ meshagent/tools/tool.py,sha256=eokq9Sh6aGao7w2PlExx1WJcfy4OovfMaQUluwsulnA,7221
15
16
  meshagent/tools/toolkit.py,sha256=iVcCvhwWqmahDow9FN-VRWGo9MEj669Vw7TAB7Mx9Ww,4066
16
17
  meshagent/tools/uuid.py,sha256=mzRwDmXy39U5lHhd9wqV4r-ZdS8jPfDTTs4UfW4KHJQ,1342
17
- meshagent/tools/version.py,sha256=Mu4JbSLl5nr-J2figk5hmW2mrw4skf_oeIzxbnpcgwY,23
18
- meshagent/tools/web_toolkit.py,sha256=IoOYjOBmcbQsqWT14xYg02jjWpWmGOkDSxt2U-LQoaA,1258
19
- meshagent/tools/tests/tool_decorator_test.py,sha256=Fd4uvoefU8hpdSWaiYen15tqnlwoY092G6hjYafaMGE,1190
20
- meshagent_tools-0.25.0.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
21
- meshagent_tools-0.25.0.dist-info/METADATA,sha256=umwXcVTELSTcxThf_aCwo4vnwjTwlcwET60o5RqxOM0,2890
22
- meshagent_tools-0.25.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
- meshagent_tools-0.25.0.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
24
- meshagent_tools-0.25.0.dist-info/RECORD,,
18
+ meshagent/tools/version.py,sha256=NMzPG-AisbqV0qHNDcM-oIMtdXA6rLhjzeATPyNq6Nw,23
19
+ meshagent/tools/web_toolkit.py,sha256=Seju8gpdUoPku7Yfar_s-cVOnlweFzKj-bFqrQVup8o,4603
20
+ meshagent/tools/tests/tool_decorator_test.py,sha256=8o9JwhOU6rdbJJdyCYfT4B8m9JgmMSAkU8QTpo4OmGg,1826
21
+ meshagent_tools-0.25.2.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
22
+ meshagent_tools-0.25.2.dist-info/METADATA,sha256=X2n1kOJ-WFqsT45x1EcKcDsY3PqCiZkKwW0Y2g-V3o8,2930
23
+ meshagent_tools-0.25.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
24
+ meshagent_tools-0.25.2.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
25
+ meshagent_tools-0.25.2.dist-info/RECORD,,