meshagent-cli 0.28.2__tar.gz → 0.28.4__tar.gz

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.
Files changed (59) hide show
  1. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/PKG-INFO +13 -13
  2. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/chatbot.py +141 -4
  3. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/helper.py +16 -0
  4. meshagent_cli-0.28.4/meshagent/cli/helper_test.py +33 -0
  5. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/mailbot.py +83 -2
  6. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/port.py +42 -4
  7. meshagent_cli-0.28.4/meshagent/cli/port_test.py +38 -0
  8. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/task_runner.py +99 -1
  9. meshagent_cli-0.28.4/meshagent/cli/version.py +1 -0
  10. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/worker.py +82 -0
  11. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent_cli.egg-info/PKG-INFO +13 -13
  12. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent_cli.egg-info/SOURCES.txt +2 -0
  13. meshagent_cli-0.28.4/meshagent_cli.egg-info/requires.txt +29 -0
  14. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/pyproject.toml +12 -12
  15. meshagent_cli-0.28.2/meshagent/cli/version.py +0 -1
  16. meshagent_cli-0.28.2/meshagent_cli.egg-info/requires.txt +0 -29
  17. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/README.md +0 -0
  18. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/__init__.py +0 -0
  19. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/agent.py +0 -0
  20. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/api_keys.py +0 -0
  21. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/async_typer.py +0 -0
  22. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/auth.py +0 -0
  23. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/auth_async.py +0 -0
  24. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/call.py +0 -0
  25. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/cli.py +0 -0
  26. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/cli_mcp.py +0 -0
  27. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/cli_secrets.py +0 -0
  28. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/codex.py +0 -0
  29. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/common_options.py +0 -0
  30. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/containers.py +0 -0
  31. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/database.py +0 -0
  32. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/developer.py +0 -0
  33. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/helpers.py +0 -0
  34. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/host.py +0 -0
  35. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/mailboxes.py +0 -0
  36. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/meeting_transcriber.py +0 -0
  37. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/memory.py +0 -0
  38. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/messaging.py +0 -0
  39. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/multi.py +0 -0
  40. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/oauth2.py +0 -0
  41. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/participant_token.py +0 -0
  42. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/projects.py +0 -0
  43. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/queue.py +0 -0
  44. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/room.py +0 -0
  45. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/room_services.py +0 -0
  46. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/rooms.py +0 -0
  47. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/routes.py +0 -0
  48. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/services.py +0 -0
  49. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/sessions.py +0 -0
  50. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/storage.py +0 -0
  51. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/sync.py +0 -0
  52. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/test.py +0 -0
  53. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/voicebot.py +0 -0
  54. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/webhook.py +0 -0
  55. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent/cli/webserver.py +0 -0
  56. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent_cli.egg-info/dependency_links.txt +0 -0
  57. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent_cli.egg-info/entry_points.txt +0 -0
  58. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/meshagent_cli.egg-info/top_level.txt +0 -0
  59. {meshagent_cli-0.28.2 → meshagent_cli-0.28.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-cli
3
- Version: 0.28.2
3
+ Version: 0.28.4
4
4
  Summary: CLI for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -19,21 +19,21 @@ Requires-Dist: rich~=14.3.0
19
19
  Requires-Dist: textual<2.0,>=0.50
20
20
  Requires-Dist: prompt-toolkit~=3.0.52
21
21
  Provides-Extra: all
22
- Requires-Dist: meshagent-agents[all]~=0.28.2; extra == "all"
23
- Requires-Dist: meshagent-api[all]~=0.28.2; extra == "all"
24
- Requires-Dist: meshagent-computers~=0.28.2; extra == "all"
25
- Requires-Dist: meshagent-openai~=0.28.2; extra == "all"
26
- Requires-Dist: meshagent-anthropic~=0.28.2; extra == "all"
27
- Requires-Dist: meshagent-codex~=0.28.2; extra == "all"
28
- Requires-Dist: meshagent-mcp~=0.28.2; extra == "all"
29
- Requires-Dist: meshagent-tools~=0.28.2; extra == "all"
22
+ Requires-Dist: meshagent-agents[all]~=0.28.4; extra == "all"
23
+ Requires-Dist: meshagent-api[all]~=0.28.4; extra == "all"
24
+ Requires-Dist: meshagent-computers~=0.28.4; extra == "all"
25
+ Requires-Dist: meshagent-openai~=0.28.4; extra == "all"
26
+ Requires-Dist: meshagent-anthropic~=0.28.4; extra == "all"
27
+ Requires-Dist: meshagent-codex~=0.28.4; extra == "all"
28
+ Requires-Dist: meshagent-mcp~=0.28.4; extra == "all"
29
+ Requires-Dist: meshagent-tools~=0.28.4; extra == "all"
30
30
  Requires-Dist: supabase-auth~=2.28.0; extra == "all"
31
31
  Requires-Dist: prompt-toolkit~=3.0.52; extra == "all"
32
32
  Provides-Extra: mcp-service
33
- Requires-Dist: meshagent-agents[all]~=0.28.2; extra == "mcp-service"
34
- Requires-Dist: meshagent-api~=0.28.2; extra == "mcp-service"
35
- Requires-Dist: meshagent-mcp~=0.28.2; extra == "mcp-service"
36
- Requires-Dist: meshagent-tools~=0.28.2; extra == "mcp-service"
33
+ Requires-Dist: meshagent-agents[all]~=0.28.4; extra == "mcp-service"
34
+ Requires-Dist: meshagent-api~=0.28.4; extra == "mcp-service"
35
+ Requires-Dist: meshagent-mcp~=0.28.4; extra == "mcp-service"
36
+ Requires-Dist: meshagent-tools~=0.28.4; extra == "mcp-service"
37
37
  Requires-Dist: supabase-auth~=2.28.0; extra == "mcp-service"
38
38
 
39
39
  # [Meshagent](https://www.meshagent.com)
@@ -7,6 +7,7 @@ from meshagent.tools import (
7
7
  WebFetchTool,
8
8
  WebFetchToolkitBuilder,
9
9
  ContainerShellTool,
10
+ MemoriesToolkit,
10
11
  )
11
12
  from meshagent.tools.storage import (
12
13
  StorageToolMount,
@@ -40,6 +41,7 @@ from meshagent.cli.helper import (
40
41
  cleanup_args,
41
42
  get_client,
42
43
  parse_shell_tool_mounts,
44
+ parse_memory_selector,
43
45
  parse_storage_tool_mounts,
44
46
  resolve_key,
45
47
  resolve_project_id,
@@ -250,6 +252,8 @@ def build_chatbot(
250
252
  require_read_only_storage: Optional[str] = None,
251
253
  require_time: bool = True,
252
254
  require_uuid: bool = False,
255
+ use_memory: Optional[str] = None,
256
+ memory_model: Optional[str] = None,
253
257
  rules_file: Optional[list[str]] = None,
254
258
  room_rules_path: Optional[list[str]] = None,
255
259
  require_discovery: Optional[str] = None,
@@ -316,6 +320,10 @@ def build_chatbot(
316
320
  )
317
321
  raise typer.Exit(1)
318
322
 
323
+ memory_selection: Optional[tuple[str, Optional[list[str]]]] = None
324
+ if use_memory is not None:
325
+ memory_selection = parse_memory_selector(use_memory)
326
+
319
327
  BaseClass = ChatBot
320
328
  decision_model = None
321
329
  if llm_participant:
@@ -539,6 +547,16 @@ def build_chatbot(
539
547
  if require_uuid:
540
548
  providers.extend((UUIDToolkit()).tools)
541
549
 
550
+ if memory_selection is not None:
551
+ memory_name, memory_namespace = memory_selection
552
+ providers.extend(
553
+ MemoriesToolkit(
554
+ memory_name=memory_name,
555
+ namespace=memory_namespace,
556
+ llm_model=memory_model,
557
+ ).tools
558
+ )
559
+
542
560
  if len(require_table_write) > 0:
543
561
  providers.extend(
544
562
  (
@@ -845,6 +863,20 @@ async def join(
845
863
  help="Enable UUID generation tools",
846
864
  ),
847
865
  ] = False,
866
+ use_memory: Annotated[
867
+ Optional[str],
868
+ typer.Option(
869
+ "--use-memory",
870
+ help="Use memories toolkit for <name> or <namespace>/<name>",
871
+ ),
872
+ ] = None,
873
+ memory_model: Annotated[
874
+ Optional[str],
875
+ typer.Option(
876
+ "--memory-model",
877
+ help="Model name for memory LLM ingestion",
878
+ ),
879
+ ] = None,
848
880
  require_document_authoring: Annotated[
849
881
  Optional[bool],
850
882
  typer.Option(..., help="Enable MeshDocument authoring"),
@@ -916,7 +948,6 @@ async def join(
916
948
  token = ParticipantToken(
917
949
  name=agent_name,
918
950
  )
919
-
920
951
  token.add_api_grant(ApiScope.agent_default(tunnels=require_computer_use))
921
952
 
922
953
  token.add_role_grant(role=role)
@@ -969,6 +1000,8 @@ async def join(
969
1000
  require_read_only_storage=require_read_only_storage,
970
1001
  require_time=require_time,
971
1002
  require_uuid=require_uuid,
1003
+ use_memory=use_memory,
1004
+ memory_model=memory_model,
972
1005
  room_rules_path=room_rules,
973
1006
  require_document_authoring=require_document_authoring,
974
1007
  require_discovery=require_discovery,
@@ -1190,6 +1223,20 @@ async def service(
1190
1223
  help="Enable UUID generation tools",
1191
1224
  ),
1192
1225
  ] = False,
1226
+ use_memory: Annotated[
1227
+ Optional[str],
1228
+ typer.Option(
1229
+ "--use-memory",
1230
+ help="Use memories toolkit for <name> or <namespace>/<name>",
1231
+ ),
1232
+ ] = None,
1233
+ memory_model: Annotated[
1234
+ Optional[str],
1235
+ typer.Option(
1236
+ "--memory-model",
1237
+ help="Model name for memory LLM ingestion",
1238
+ ),
1239
+ ] = None,
1193
1240
  working_dir: WorkingDirOption = None,
1194
1241
  working_directory: WorkingDirectoryAliasOption = None,
1195
1242
  require_document_authoring: Annotated[
@@ -1302,6 +1349,8 @@ async def service(
1302
1349
  require_read_only_storage=require_read_only_storage,
1303
1350
  require_time=require_time,
1304
1351
  require_uuid=require_uuid,
1352
+ use_memory=use_memory,
1353
+ memory_model=memory_model,
1305
1354
  room_rules_path=room_rules,
1306
1355
  working_dir=working_dir,
1307
1356
  require_document_authoring=require_document_authoring,
@@ -1501,6 +1550,20 @@ async def spec(
1501
1550
  help="Enable UUID generation tools",
1502
1551
  ),
1503
1552
  ] = False,
1553
+ use_memory: Annotated[
1554
+ Optional[str],
1555
+ typer.Option(
1556
+ "--use-memory",
1557
+ help="Use memories toolkit for <name> or <namespace>/<name>",
1558
+ ),
1559
+ ] = None,
1560
+ memory_model: Annotated[
1561
+ Optional[str],
1562
+ typer.Option(
1563
+ "--memory-model",
1564
+ help="Model name for memory LLM ingestion",
1565
+ ),
1566
+ ] = None,
1504
1567
  working_dir: WorkingDirOption = None,
1505
1568
  working_directory: WorkingDirectoryAliasOption = None,
1506
1569
  require_document_authoring: Annotated[
@@ -1612,6 +1675,8 @@ async def spec(
1612
1675
  require_read_only_storage=require_read_only_storage,
1613
1676
  require_time=require_time,
1614
1677
  require_uuid=require_uuid,
1678
+ use_memory=use_memory,
1679
+ memory_model=memory_model,
1615
1680
  room_rules_path=room_rules,
1616
1681
  working_dir=working_dir,
1617
1682
  require_document_authoring=require_document_authoring,
@@ -1822,6 +1887,20 @@ async def deploy(
1822
1887
  help="Enable UUID generation tools",
1823
1888
  ),
1824
1889
  ] = False,
1890
+ use_memory: Annotated[
1891
+ Optional[str],
1892
+ typer.Option(
1893
+ "--use-memory",
1894
+ help="Use memories toolkit for <name> or <namespace>/<name>",
1895
+ ),
1896
+ ] = None,
1897
+ memory_model: Annotated[
1898
+ Optional[str],
1899
+ typer.Option(
1900
+ "--memory-model",
1901
+ help="Model name for memory LLM ingestion",
1902
+ ),
1903
+ ] = None,
1825
1904
  working_dir: WorkingDirOption = None,
1826
1905
  working_directory: WorkingDirectoryAliasOption = None,
1827
1906
  require_document_authoring: Annotated[
@@ -1940,6 +2019,8 @@ async def deploy(
1940
2019
  require_read_only_storage=require_read_only_storage,
1941
2020
  require_time=require_time,
1942
2021
  require_uuid=require_uuid,
2022
+ use_memory=use_memory,
2023
+ memory_model=memory_model,
1943
2024
  room_rules_path=room_rules,
1944
2025
  working_dir=working_dir,
1945
2026
  require_document_authoring=require_document_authoring,
@@ -2835,7 +2916,11 @@ async def chat_with(
2835
2916
  return
2836
2917
 
2837
2918
  items = message_nodes[0].get_children()
2838
- self._has_active_events = self._thread_has_active_events(items)
2919
+ has_active_event_nodes = self._thread_has_active_event_nodes(items)
2920
+ thread_status_text = self._active_thread_status_text()
2921
+ self._has_active_events = (
2922
+ has_active_event_nodes or thread_status_text is not None
2923
+ )
2839
2924
  selected_before = self._selected_pending_approval()
2840
2925
  selected_id = selected_before[0] if selected_before is not None else None
2841
2926
  self._pending_approval_items = self._collect_pending_approvals(items)
@@ -2863,6 +2948,11 @@ async def chat_with(
2863
2948
  ):
2864
2949
  rendered_items.append(renderable)
2865
2950
 
2951
+ if thread_status_text is not None and not has_active_event_nodes:
2952
+ rendered_items.append(
2953
+ self._render_thread_status_item(thread_status_text)
2954
+ )
2955
+
2866
2956
  if len(rendered_items) == 0:
2867
2957
  self._messages_view.update("")
2868
2958
  else:
@@ -2871,7 +2961,7 @@ async def chat_with(
2871
2961
  if self._messages_scroll is not None:
2872
2962
  self._messages_scroll.scroll_end(animate=False)
2873
2963
 
2874
- def _thread_has_active_events(self, items) -> bool:
2964
+ def _thread_has_active_event_nodes(self, items) -> bool:
2875
2965
  for item in items:
2876
2966
  if getattr(item, "tag_name", None) != "event":
2877
2967
  continue
@@ -2880,6 +2970,37 @@ async def chat_with(
2880
2970
  return True
2881
2971
  return False
2882
2972
 
2973
+ def _active_thread_status_text(self) -> str | None:
2974
+ participant = self._chat_client._participant
2975
+ if participant is None:
2976
+ return None
2977
+
2978
+ status_attr = f"thread.status.text.{self._chat_client.thread_path}"
2979
+ status = participant.get_attribute(status_attr)
2980
+ if not isinstance(status, str):
2981
+ return None
2982
+
2983
+ normalized = status.strip()
2984
+ if normalized == "":
2985
+ return None
2986
+
2987
+ return normalized
2988
+
2989
+ def _render_thread_status_item(self, status_text: str) -> RenderableType:
2990
+ table = Table.grid(expand=True, padding=(0, 0))
2991
+ table.add_column(width=2, no_wrap=True)
2992
+ table.add_column(ratio=1)
2993
+ table.add_column(width=2, no_wrap=True)
2994
+
2995
+ table.add_row(Text(" "), Text(" "), Text(" "))
2996
+ table.add_row(
2997
+ Text(" "),
2998
+ Text(f"{self._event_spinner()} {status_text}", style="bold magenta"),
2999
+ Text(" "),
3000
+ )
3001
+ table.add_row(Text(" "), Text(" "), Text(" "))
3002
+ return table
3003
+
2883
3004
  def _collect_pending_approvals(self, items) -> list[tuple[str, str, str]]:
2884
3005
  approvals: list[tuple[str, str, str]] = []
2885
3006
  seen: set[str] = set()
@@ -3615,6 +3736,20 @@ async def run(
3615
3736
  help="Enable UUID generation tools",
3616
3737
  ),
3617
3738
  ] = False,
3739
+ use_memory: Annotated[
3740
+ Optional[str],
3741
+ typer.Option(
3742
+ "--use-memory",
3743
+ help="Use memories toolkit for <name> or <namespace>/<name>",
3744
+ ),
3745
+ ] = None,
3746
+ memory_model: Annotated[
3747
+ Optional[str],
3748
+ typer.Option(
3749
+ "--memory-model",
3750
+ help="Model name for memory LLM ingestion",
3751
+ ),
3752
+ ] = None,
3618
3753
  require_document_authoring: Annotated[
3619
3754
  Optional[bool],
3620
3755
  typer.Option(..., help="Enable MeshDocument authoring"),
@@ -3687,7 +3822,7 @@ async def run(
3687
3822
  working_dir=working_dir,
3688
3823
  working_directory=working_directory,
3689
3824
  )
3690
- if not verbose:
3825
+ if not verbose and not log_llm_requests:
3691
3826
  root = logging.getLogger()
3692
3827
  root.setLevel(logging.ERROR)
3693
3828
 
@@ -3767,6 +3902,8 @@ async def run(
3767
3902
  require_read_only_storage=require_read_only_storage,
3768
3903
  require_time=require_time,
3769
3904
  require_uuid=require_uuid,
3905
+ use_memory=use_memory,
3906
+ memory_model=memory_model,
3770
3907
  room_rules_path=room_rules,
3771
3908
  require_document_authoring=require_document_authoring,
3772
3909
  require_discovery=require_discovery,
@@ -213,6 +213,22 @@ async def resolve_key(project_id: str | None, key: str | None):
213
213
  return key
214
214
 
215
215
 
216
+ def parse_memory_selector(value: str) -> tuple[str, Optional[list[str]]]:
217
+ cleaned = value.strip()
218
+ if cleaned == "":
219
+ raise typer.BadParameter("--use-memory cannot be empty")
220
+
221
+ segments = [segment.strip() for segment in cleaned.split("/")]
222
+ if any(segment == "" for segment in segments):
223
+ raise typer.BadParameter(
224
+ "--use-memory must be '<name>' or '<namespace>/<name>' with no empty segments"
225
+ )
226
+
227
+ memory_name = segments[-1]
228
+ namespace = segments[:-1]
229
+ return memory_name, namespace or None
230
+
231
+
216
232
  def _split_mount_value(
217
233
  value: str, option_name: str, default_read_only: bool
218
234
  ) -> tuple[str, str, bool]:
@@ -0,0 +1,33 @@
1
+ import pytest
2
+ import typer
3
+
4
+ from meshagent.cli.helper import parse_memory_selector
5
+
6
+
7
+ def test_parse_memory_selector_name_only() -> None:
8
+ memory_name, namespace = parse_memory_selector("graph")
9
+
10
+ assert memory_name == "graph"
11
+ assert namespace is None
12
+
13
+
14
+ def test_parse_memory_selector_with_namespace() -> None:
15
+ memory_name, namespace = parse_memory_selector("team/shared/graph")
16
+
17
+ assert memory_name == "graph"
18
+ assert namespace == ["team", "shared"]
19
+
20
+
21
+ @pytest.mark.parametrize(
22
+ "value",
23
+ [
24
+ "",
25
+ " ",
26
+ "/graph",
27
+ "team/",
28
+ "team//graph",
29
+ ],
30
+ )
31
+ def test_parse_memory_selector_rejects_empty_segments(value: str) -> None:
32
+ with pytest.raises(typer.BadParameter):
33
+ parse_memory_selector(value)
@@ -9,13 +9,14 @@ from meshagent.cli.common_options import (
9
9
  ProjectIdOption,
10
10
  RoomOption,
11
11
  )
12
- from meshagent.tools import Toolkit, WebFetchTool, ContainerShellTool
12
+ from meshagent.tools import Toolkit, WebFetchTool, ContainerShellTool, MemoriesToolkit
13
13
  from meshagent.api import RoomClient, WebSocketClientProtocol, ApiScope
14
14
  from meshagent.api.helpers import websocket_room_url
15
15
  from meshagent.cli.helper import (
16
16
  cleanup_args,
17
17
  get_client,
18
18
  parse_shell_tool_mounts,
19
+ parse_memory_selector,
19
20
  parse_storage_tool_mounts,
20
21
  resolve_key,
21
22
  resolve_project_id,
@@ -212,6 +213,8 @@ def build_mailbot(
212
213
  shell_tool_mounts: Optional[ContainerMountSpec] = None,
213
214
  require_time: bool = True,
214
215
  require_uuid: bool = False,
216
+ use_memory: Optional[str] = None,
217
+ memory_model: Optional[str] = None,
215
218
  require_table_read: bool,
216
219
  require_table_write: bool,
217
220
  require_computer_use: bool,
@@ -257,6 +260,10 @@ def build_mailbot(
257
260
  supports_openai_tools = llm_participant is None and not is_claude_model
258
261
  base_shell_env = _copy_shell_env_vars(copy_env=shell_copy_env)
259
262
  base_shell_env.update(_set_shell_env_vars(set_env=shell_set_env))
263
+ memory_selection: Optional[tuple[str, Optional[list[str]]]] = None
264
+ if use_memory is not None:
265
+ memory_selection = parse_memory_selector(use_memory)
266
+
260
267
  if not supports_openai_tools:
261
268
  if image_generation:
262
269
  print("image generation tool is only supported by openai models")
@@ -506,6 +513,16 @@ def build_mailbot(
506
513
  if require_uuid:
507
514
  thread_toolkit.tools.extend(UUIDToolkit().tools)
508
515
 
516
+ if memory_selection is not None:
517
+ memory_name, memory_namespace = memory_selection
518
+ thread_toolkit.tools.extend(
519
+ MemoriesToolkit(
520
+ memory_name=memory_name,
521
+ namespace=memory_namespace,
522
+ llm_model=memory_model,
523
+ ).tools
524
+ )
525
+
509
526
  if require_computer_use:
510
527
  from meshagent.computers.agent import ComputerToolkit
511
528
 
@@ -670,6 +687,20 @@ async def join(
670
687
  help="Enable UUID generation tools",
671
688
  ),
672
689
  ] = False,
690
+ use_memory: Annotated[
691
+ Optional[str],
692
+ typer.Option(
693
+ "--use-memory",
694
+ help="Use memories toolkit for <name> or <namespace>/<name>",
695
+ ),
696
+ ] = None,
697
+ memory_model: Annotated[
698
+ Optional[str],
699
+ typer.Option(
700
+ "--memory-model",
701
+ help="Model name for memory LLM ingestion",
702
+ ),
703
+ ] = None,
673
704
  database_namespace: Annotated[
674
705
  Optional[str],
675
706
  typer.Option(..., help="Use a specific database namespace"),
@@ -792,6 +823,8 @@ async def join(
792
823
  shell_tool_mounts=shell_tool_mounts,
793
824
  require_time=require_time,
794
825
  require_uuid=require_uuid,
826
+ use_memory=use_memory,
827
+ memory_model=memory_model,
795
828
  require_table_read=require_table_read,
796
829
  require_table_write=require_table_write,
797
830
  require_computer_use=require_computer_use,
@@ -978,6 +1011,20 @@ async def service(
978
1011
  help="Enable UUID generation tools",
979
1012
  ),
980
1013
  ] = False,
1014
+ use_memory: Annotated[
1015
+ Optional[str],
1016
+ typer.Option(
1017
+ "--use-memory",
1018
+ help="Use memories toolkit for <name> or <namespace>/<name>",
1019
+ ),
1020
+ ] = None,
1021
+ memory_model: Annotated[
1022
+ Optional[str],
1023
+ typer.Option(
1024
+ "--memory-model",
1025
+ help="Model name for memory LLM ingestion",
1026
+ ),
1027
+ ] = None,
981
1028
  database_namespace: Annotated[
982
1029
  Optional[str],
983
1030
  typer.Option(..., help="Use a specific database namespace"),
@@ -1081,6 +1128,8 @@ async def service(
1081
1128
  shell_tool_mounts=shell_tool_mounts,
1082
1129
  require_time=require_time,
1083
1130
  require_uuid=require_uuid,
1131
+ use_memory=use_memory,
1132
+ memory_model=memory_model,
1084
1133
  require_table_read=require_table_read,
1085
1134
  require_table_write=require_table_write,
1086
1135
  require_computer_use=require_computer_use,
@@ -1254,6 +1303,20 @@ async def spec(
1254
1303
  help="Enable UUID generation tools",
1255
1304
  ),
1256
1305
  ] = False,
1306
+ use_memory: Annotated[
1307
+ Optional[str],
1308
+ typer.Option(
1309
+ "--use-memory",
1310
+ help="Use memories toolkit for <name> or <namespace>/<name>",
1311
+ ),
1312
+ ] = None,
1313
+ memory_model: Annotated[
1314
+ Optional[str],
1315
+ typer.Option(
1316
+ "--memory-model",
1317
+ help="Model name for memory LLM ingestion",
1318
+ ),
1319
+ ] = None,
1257
1320
  database_namespace: Annotated[
1258
1321
  Optional[str],
1259
1322
  typer.Option(..., help="Use a specific database namespace"),
@@ -1357,6 +1420,8 @@ async def spec(
1357
1420
  shell_tool_mounts=shell_tool_mounts,
1358
1421
  require_time=require_time,
1359
1422
  require_uuid=require_uuid,
1423
+ use_memory=use_memory,
1424
+ memory_model=memory_model,
1360
1425
  require_table_read=require_table_read,
1361
1426
  require_table_write=require_table_write,
1362
1427
  require_computer_use=require_computer_use,
@@ -1541,6 +1606,20 @@ async def deploy(
1541
1606
  help="Enable UUID generation tools",
1542
1607
  ),
1543
1608
  ] = False,
1609
+ use_memory: Annotated[
1610
+ Optional[str],
1611
+ typer.Option(
1612
+ "--use-memory",
1613
+ help="Use memories toolkit for <name> or <namespace>/<name>",
1614
+ ),
1615
+ ] = None,
1616
+ memory_model: Annotated[
1617
+ Optional[str],
1618
+ typer.Option(
1619
+ "--memory-model",
1620
+ help="Model name for memory LLM ingestion",
1621
+ ),
1622
+ ] = None,
1544
1623
  database_namespace: Annotated[
1545
1624
  Optional[str],
1546
1625
  typer.Option(..., help="Use a specific database namespace"),
@@ -1651,6 +1730,8 @@ async def deploy(
1651
1730
  shell_tool_mounts=shell_tool_mounts,
1652
1731
  require_time=require_time,
1653
1732
  require_uuid=require_uuid,
1733
+ use_memory=use_memory,
1734
+ memory_model=memory_model,
1654
1735
  require_table_read=require_table_read,
1655
1736
  require_table_write=require_table_write,
1656
1737
  require_computer_use=require_computer_use,
@@ -1677,7 +1758,7 @@ async def deploy(
1677
1758
  spec.metadata.description = service_description
1678
1759
  spec.container.image = "meshagent/cli:default"
1679
1760
  spec.container.command = shlex.join(
1680
- ["meshagent", "mailbot", *cleanup_args(sys.argv[:2])]
1761
+ ["meshagent", "mailbot", "service", *cleanup_args(sys.argv[2:])]
1681
1762
  )
1682
1763
 
1683
1764
  client = await get_client()
@@ -8,6 +8,7 @@ from meshagent.api.helpers import websocket_room_url
8
8
  from typing import Annotated, Optional
9
9
 
10
10
  import typer
11
+ from rich import print
11
12
 
12
13
  from meshagent.cli import async_typer
13
14
  from meshagent.cli.common_options import ProjectIdOption
@@ -18,6 +19,41 @@ from meshagent.api.port_forward import port_forward
18
19
  app = async_typer.AsyncTyper(help="Port forwarding into room containers")
19
20
 
20
21
 
22
+ def _parse_port_mapping(value: str) -> tuple[int, int]:
23
+ raw = value.strip()
24
+ if raw == "":
25
+ raise typer.BadParameter("--port cannot be empty")
26
+
27
+ parts = [part.strip() for part in raw.split(":")]
28
+ if len(parts) != 2:
29
+ raise typer.BadParameter("Expected --port in LOCAL:REMOTE format")
30
+
31
+ local_raw, remote_raw = parts
32
+ if local_raw == "" or remote_raw == "":
33
+ raise typer.BadParameter("Expected --port in LOCAL:REMOTE format")
34
+
35
+ try:
36
+ local_port = int(local_raw)
37
+ except ValueError as exc:
38
+ raise typer.BadParameter(
39
+ f"LOCAL port must be an integer, got: {local_raw}"
40
+ ) from exc
41
+
42
+ try:
43
+ remote_port = int(remote_raw)
44
+ except ValueError as exc:
45
+ raise typer.BadParameter(
46
+ f"REMOTE port must be an integer, got: {remote_raw}"
47
+ ) from exc
48
+
49
+ if not (0 <= local_port <= 65535):
50
+ raise typer.BadParameter("LOCAL port must be between 0 and 65535")
51
+ if not (1 <= remote_port <= 65535):
52
+ raise typer.BadParameter("REMOTE port must be between 1 and 65535")
53
+
54
+ return local_port, remote_port
55
+
56
+
21
57
  @app.async_command("forward", help="Forward a container port to localhost")
22
58
  async def forward(
23
59
  *,
@@ -59,12 +95,11 @@ async def forward(
59
95
 
60
96
  client = await get_client()
61
97
  try:
98
+ local_port, remote_port = _parse_port_mapping(port)
62
99
  project_id = await resolve_project_id(project_id)
63
100
 
64
101
  connection = await client.connect_room(project_id=project_id, room=room)
65
102
 
66
- ports = port.split(":")
67
-
68
103
  if name is not None:
69
104
  async with RoomClient(
70
105
  protocol=WebSocketClientProtocol(
@@ -86,8 +121,8 @@ async def forward(
86
121
  raise typer.Exit(1)
87
122
 
88
123
  handler = await port_forward(
89
- listen_port=int(ports[0]),
90
- port=int(ports[1]),
124
+ listen_port=local_port,
125
+ port=remote_port,
91
126
  container_id=container_id,
92
127
  token=connection.jwt,
93
128
  )
@@ -95,6 +130,9 @@ async def forward(
95
130
  await asyncio.sleep(10000)
96
131
 
97
132
  await handler.close()
133
+ except typer.BadParameter as exc:
134
+ print(f"[red]{exc}[/red]")
135
+ raise typer.Exit(1) from exc
98
136
 
99
137
  finally:
100
138
  await client.close()
@@ -0,0 +1,38 @@
1
+ import pytest
2
+ import typer
3
+
4
+ from meshagent.cli.port import _parse_port_mapping
5
+
6
+
7
+ @pytest.mark.parametrize(
8
+ ("value", "expected"),
9
+ [
10
+ ("3000:3000", (3000, 3000)),
11
+ ("0:3000", (0, 3000)),
12
+ (" 8080 : 80 ", (8080, 80)),
13
+ ],
14
+ )
15
+ def test_parse_port_mapping_accepts_valid_values(
16
+ value: str, expected: tuple[int, int]
17
+ ) -> None:
18
+ assert _parse_port_mapping(value) == expected
19
+
20
+
21
+ @pytest.mark.parametrize(
22
+ "value",
23
+ [
24
+ "",
25
+ "3000",
26
+ ":3000",
27
+ "3000:",
28
+ "abc:3000",
29
+ "3000:abc",
30
+ "-1:3000",
31
+ "70000:3000",
32
+ "3000:0",
33
+ "3000:70000",
34
+ ],
35
+ )
36
+ def test_parse_port_mapping_rejects_invalid_values(value: str) -> None:
37
+ with pytest.raises(typer.BadParameter):
38
+ _parse_port_mapping(value)