meshagent-cli 0.7.0__py3-none-any.whl → 0.23.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.
Files changed (42) hide show
  1. meshagent/cli/agent.py +23 -13
  2. meshagent/cli/api_keys.py +4 -4
  3. meshagent/cli/async_typer.py +52 -4
  4. meshagent/cli/call.py +27 -36
  5. meshagent/cli/chatbot.py +1559 -177
  6. meshagent/cli/cli.py +23 -22
  7. meshagent/cli/cli_mcp.py +92 -28
  8. meshagent/cli/cli_secrets.py +10 -10
  9. meshagent/cli/common_options.py +19 -4
  10. meshagent/cli/containers.py +164 -16
  11. meshagent/cli/database.py +997 -0
  12. meshagent/cli/developer.py +3 -3
  13. meshagent/cli/exec.py +22 -6
  14. meshagent/cli/helper.py +101 -12
  15. meshagent/cli/helpers.py +65 -11
  16. meshagent/cli/host.py +41 -0
  17. meshagent/cli/mailbot.py +1104 -79
  18. meshagent/cli/mailboxes.py +223 -0
  19. meshagent/cli/meeting_transcriber.py +29 -15
  20. meshagent/cli/messaging.py +7 -10
  21. meshagent/cli/multi.py +357 -0
  22. meshagent/cli/oauth2.py +192 -40
  23. meshagent/cli/participant_token.py +5 -3
  24. meshagent/cli/port.py +70 -0
  25. meshagent/cli/queue.py +2 -2
  26. meshagent/cli/room.py +24 -212
  27. meshagent/cli/rooms.py +214 -0
  28. meshagent/cli/services.py +269 -37
  29. meshagent/cli/sessions.py +5 -5
  30. meshagent/cli/storage.py +5 -5
  31. meshagent/cli/sync.py +434 -0
  32. meshagent/cli/task_runner.py +1317 -0
  33. meshagent/cli/version.py +1 -1
  34. meshagent/cli/voicebot.py +544 -98
  35. meshagent/cli/webhook.py +7 -7
  36. meshagent/cli/worker.py +1403 -0
  37. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.23.0.dist-info}/METADATA +15 -13
  38. meshagent_cli-0.23.0.dist-info/RECORD +45 -0
  39. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.23.0.dist-info}/WHEEL +1 -1
  40. meshagent_cli-0.7.0.dist-info/RECORD +0 -36
  41. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.23.0.dist-info}/entry_points.txt +0 -0
  42. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.23.0.dist-info}/top_level.txt +0 -0
meshagent/cli/sync.py ADDED
@@ -0,0 +1,434 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ import typer
9
+ from rich import print
10
+
11
+ from meshagent.api import RoomClient, RoomException, WebSocketClientProtocol
12
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
13
+ from meshagent.api.runtime import RuntimeDocument
14
+ from meshagent.api.schema import MeshSchema
15
+ from meshagent.api.schema_document import Element
16
+ from meshagent.cli import async_typer
17
+ from meshagent.cli.common_options import ProjectIdOption, RoomOption
18
+ from meshagent.cli.helper import get_client, resolve_project_id, resolve_room
19
+
20
+ app = async_typer.AsyncTyper(help="Inspect and update mesh documents in a room")
21
+
22
+
23
+ def _parse_json_arg(json_str: Optional[str], *, name: str) -> Any:
24
+ if json_str is None:
25
+ return None
26
+ try:
27
+ return json.loads(json_str)
28
+ except Exception as exc:
29
+ raise typer.BadParameter(f"Invalid JSON for {name}: {exc}") from exc
30
+
31
+
32
+ def _load_json_file(path: Optional[Path], *, name: str) -> Any:
33
+ if path is None:
34
+ return None
35
+ try:
36
+ return json.loads(path.read_text())
37
+ except Exception as exc:
38
+ raise typer.BadParameter(f"Unable to read {name} from {path}: {exc}") from exc
39
+
40
+
41
+ def _decode_pointer(path: str) -> list[str]:
42
+ if path == "":
43
+ return []
44
+ if not path.startswith("/"):
45
+ raise typer.BadParameter(f"Invalid JSON pointer: {path}")
46
+ tokens = path.lstrip("/").split("/")
47
+ return [token.replace("~1", "/").replace("~0", "~") for token in tokens]
48
+
49
+
50
+ def _resolve_target(document: Any, tokens: list[str]) -> Any:
51
+ current = document
52
+ for token in tokens:
53
+ if isinstance(current, list):
54
+ if token == "-":
55
+ raise typer.BadParameter("JSON pointer '-' is not valid here")
56
+ try:
57
+ index = int(token)
58
+ except ValueError as exc:
59
+ raise typer.BadParameter(f"Invalid list index: {token}") from exc
60
+ try:
61
+ current = current[index]
62
+ except IndexError as exc:
63
+ raise typer.BadParameter(f"List index out of range: {token}") from exc
64
+ elif isinstance(current, dict):
65
+ if token not in current:
66
+ raise typer.BadParameter(f"Path not found: {token}")
67
+ current = current[token]
68
+ else:
69
+ raise typer.BadParameter("JSON pointer targets a non-container value")
70
+ return current
71
+
72
+
73
+ def _resolve_parent(document: Any, tokens: list[str]) -> tuple[Any, str]:
74
+ if not tokens:
75
+ raise typer.BadParameter("JSON pointer must target a child value")
76
+ parent = _resolve_target(document, tokens[:-1]) if len(tokens) > 1 else document
77
+ return parent, tokens[-1]
78
+
79
+
80
+ def _add_value(document: Any, tokens: list[str], value: Any) -> Any:
81
+ if not tokens:
82
+ return value
83
+ parent, key = _resolve_parent(document, tokens)
84
+ if isinstance(parent, list):
85
+ if key == "-":
86
+ parent.append(value)
87
+ else:
88
+ try:
89
+ index = int(key)
90
+ except ValueError as exc:
91
+ raise typer.BadParameter(f"Invalid list index: {key}") from exc
92
+ if index < 0 or index > len(parent):
93
+ raise typer.BadParameter(f"List index out of range: {key}")
94
+ parent.insert(index, value)
95
+ elif isinstance(parent, dict):
96
+ parent[key] = value
97
+ else:
98
+ raise typer.BadParameter("JSON pointer targets a non-container value")
99
+ return document
100
+
101
+
102
+ def _replace_value(document: Any, tokens: list[str], value: Any) -> Any:
103
+ if not tokens:
104
+ return value
105
+ parent, key = _resolve_parent(document, tokens)
106
+ if isinstance(parent, list):
107
+ try:
108
+ index = int(key)
109
+ except ValueError as exc:
110
+ raise typer.BadParameter(f"Invalid list index: {key}") from exc
111
+ if index < 0 or index >= len(parent):
112
+ raise typer.BadParameter(f"List index out of range: {key}")
113
+ parent[index] = value
114
+ elif isinstance(parent, dict):
115
+ if key not in parent:
116
+ raise typer.BadParameter(f"Path not found: {key}")
117
+ parent[key] = value
118
+ else:
119
+ raise typer.BadParameter("JSON pointer targets a non-container value")
120
+ return document
121
+
122
+
123
+ def _remove_value(document: Any, tokens: list[str]) -> tuple[Any, Any]:
124
+ parent, key = _resolve_parent(document, tokens)
125
+ if isinstance(parent, list):
126
+ try:
127
+ index = int(key)
128
+ except ValueError as exc:
129
+ raise typer.BadParameter(f"Invalid list index: {key}") from exc
130
+ if index < 0 or index >= len(parent):
131
+ raise typer.BadParameter(f"List index out of range: {key}")
132
+ value = parent.pop(index)
133
+ elif isinstance(parent, dict):
134
+ if key not in parent:
135
+ raise typer.BadParameter(f"Path not found: {key}")
136
+ value = parent.pop(key)
137
+ else:
138
+ raise typer.BadParameter("JSON pointer targets a non-container value")
139
+ return document, value
140
+
141
+
142
+ def _apply_json_patch(document: Any, patch_ops: list[dict[str, Any]]) -> Any:
143
+ updated = copy.deepcopy(document)
144
+
145
+ for op in patch_ops:
146
+ operation = op.get("op")
147
+ path = op.get("path")
148
+ if operation is None or path is None:
149
+ raise typer.BadParameter("Patch entries must include 'op' and 'path'")
150
+
151
+ tokens = _decode_pointer(path)
152
+
153
+ if operation == "add":
154
+ updated = _add_value(updated, tokens, op.get("value"))
155
+ elif operation == "replace":
156
+ updated = _replace_value(updated, tokens, op.get("value"))
157
+ elif operation == "remove":
158
+ if not tokens:
159
+ raise typer.BadParameter("Cannot remove the document root")
160
+ updated, _ = _remove_value(updated, tokens)
161
+ elif operation == "move":
162
+ from_path = op.get("from")
163
+ if from_path is None:
164
+ raise typer.BadParameter("Move operations require 'from'")
165
+ from_tokens = _decode_pointer(from_path)
166
+ updated, value = _remove_value(updated, from_tokens)
167
+ updated = _add_value(updated, tokens, value)
168
+ elif operation == "copy":
169
+ from_path = op.get("from")
170
+ if from_path is None:
171
+ raise typer.BadParameter("Copy operations require 'from'")
172
+ from_tokens = _decode_pointer(from_path)
173
+ value = copy.deepcopy(_resolve_target(updated, from_tokens))
174
+ updated = _add_value(updated, tokens, value)
175
+ elif operation == "test":
176
+ expected = op.get("value")
177
+ actual = _resolve_target(updated, tokens)
178
+ if actual != expected:
179
+ raise typer.BadParameter(f"Test operation failed at {path}")
180
+ else:
181
+ raise typer.BadParameter(f"Unsupported patch op: {operation}")
182
+
183
+ return updated
184
+
185
+
186
+ def _extract_element_payload(element_json: dict) -> tuple[str, dict]:
187
+ if len(element_json) != 1:
188
+ raise typer.BadParameter("Element JSON must have a single key")
189
+ tag_name = next(iter(element_json))
190
+ payload = element_json[tag_name] or {}
191
+ if not isinstance(payload, dict):
192
+ raise typer.BadParameter("Element payload must be an object")
193
+ return tag_name, payload
194
+
195
+
196
+ def _apply_element_json(element: Element, element_json: dict) -> None:
197
+ tag_name, payload = _extract_element_payload(element_json)
198
+ if tag_name != element.tag_name:
199
+ raise typer.BadParameter(
200
+ f"Patch root tag '{tag_name}' does not match document root '{element.tag_name}'"
201
+ )
202
+
203
+ child_property = element.schema.child_property_name
204
+ children_json = []
205
+ if child_property is not None and child_property in payload:
206
+ children_json = payload.get(child_property) or []
207
+ if not isinstance(children_json, list):
208
+ raise typer.BadParameter("Child property must be a list")
209
+
210
+ attributes = {key: value for key, value in payload.items() if key != child_property}
211
+
212
+ for key in list(element._data["attributes"].keys()):
213
+ if key == "$id":
214
+ continue
215
+ if key not in attributes:
216
+ element._remove_attribute(key)
217
+
218
+ for key, value in attributes.items():
219
+ element.set_attribute(key, value)
220
+
221
+ if child_property is None:
222
+ if children_json:
223
+ raise typer.BadParameter("Element does not support children")
224
+ return
225
+
226
+ for child in list(element.get_children()):
227
+ if isinstance(child, Element):
228
+ child.delete()
229
+
230
+ for child_json in children_json:
231
+ element.append_json(child_json)
232
+
233
+
234
+ def _apply_document_json(doc: RuntimeDocument, updated_json: dict) -> None:
235
+ _apply_element_json(doc.root, updated_json)
236
+
237
+
238
+ def _render_json(payload: Any, pretty: bool) -> None:
239
+ indent = 2 if pretty else None
240
+ print(json.dumps(payload, indent=indent))
241
+
242
+
243
+ async def _connect_room(project_id: ProjectIdOption, room: RoomOption):
244
+ account_client = await get_client()
245
+ room_name = resolve_room(room)
246
+ if not room_name:
247
+ print("[red]Room name is required.[/red]")
248
+ raise typer.Exit(1)
249
+
250
+ try:
251
+ project_id = await resolve_project_id(project_id=project_id)
252
+ connection = await account_client.connect_room(
253
+ project_id=project_id, room=room_name
254
+ )
255
+ client = RoomClient(
256
+ protocol=WebSocketClientProtocol(
257
+ url=websocket_room_url(
258
+ room_name=room_name, base_url=meshagent_base_url()
259
+ ),
260
+ token=connection.jwt,
261
+ )
262
+ )
263
+ await client.__aenter__()
264
+ return account_client, client
265
+ except Exception:
266
+ await account_client.close()
267
+ raise
268
+
269
+
270
+ @app.async_command("show", help="Print the full document JSON")
271
+ async def sync_show(
272
+ *,
273
+ project_id: ProjectIdOption,
274
+ room: RoomOption,
275
+ path: str,
276
+ include_ids: bool = typer.Option(
277
+ False, "--include-ids", help="Include $id attributes in output"
278
+ ),
279
+ pretty: bool = typer.Option(True, "--pretty/--compact", help="Pretty-print JSON"),
280
+ ):
281
+ account_client, client = await _connect_room(project_id, room)
282
+ try:
283
+ doc = await client.sync.open(path=path, create=False)
284
+ try:
285
+ payload = doc.root.to_json(include_ids=include_ids)
286
+ _render_json(payload, pretty)
287
+ finally:
288
+ await client.sync.close(path=path)
289
+ except RoomException as exc:
290
+ print(f"[red]{exc}[/red]")
291
+ raise typer.Exit(1)
292
+ finally:
293
+ await client.__aexit__(None, None, None)
294
+ await account_client.close()
295
+
296
+
297
+ @app.async_command("grep", help="Search the document for matching content")
298
+ async def sync_grep(
299
+ *,
300
+ project_id: ProjectIdOption,
301
+ room: RoomOption,
302
+ path: str,
303
+ pattern: str = typer.Argument(..., help="Regex pattern to match"),
304
+ ignore_case: bool = typer.Option(False, "--ignore-case", help="Ignore case"),
305
+ before: int = typer.Option(0, "--before", min=0, help="Include siblings before"),
306
+ after: int = typer.Option(0, "--after", min=0, help="Include siblings after"),
307
+ include_ids: bool = typer.Option(
308
+ False, "--include-ids", help="Include $id attributes in output"
309
+ ),
310
+ pretty: bool = typer.Option(True, "--pretty/--compact", help="Pretty-print JSON"),
311
+ ):
312
+ account_client, client = await _connect_room(project_id, room)
313
+ try:
314
+ doc = await client.sync.open(path=path, create=False)
315
+ try:
316
+ matches = doc.root.grep(
317
+ pattern, ignore_case=ignore_case, before=before, after=after
318
+ )
319
+ payload = [match.to_json(include_ids=include_ids) for match in matches]
320
+ _render_json(payload, pretty)
321
+ finally:
322
+ await client.sync.close(path=path)
323
+ except RoomException as exc:
324
+ print(f"[red]{exc}[/red]")
325
+ raise typer.Exit(1)
326
+ finally:
327
+ await client.__aexit__(None, None, None)
328
+ await account_client.close()
329
+
330
+
331
+ @app.async_command("inspect", help="Print the document schema JSON")
332
+ async def sync_inspect(
333
+ *,
334
+ project_id: ProjectIdOption,
335
+ room: RoomOption,
336
+ path: str,
337
+ pretty: bool = typer.Option(True, "--pretty/--compact", help="Pretty-print JSON"),
338
+ ):
339
+ account_client, client = await _connect_room(project_id, room)
340
+ try:
341
+ doc = await client.sync.open(path=path, create=False)
342
+ try:
343
+ _render_json(doc.schema.to_json(), pretty)
344
+ finally:
345
+ await client.sync.close(path=path)
346
+ except RoomException as exc:
347
+ print(f"[red]{exc}[/red]")
348
+ raise typer.Exit(1)
349
+ finally:
350
+ await client.__aexit__(None, None, None)
351
+ await account_client.close()
352
+
353
+
354
+ @app.async_command("create", help="Create a new document at a path")
355
+ async def sync_create(
356
+ *,
357
+ project_id: ProjectIdOption,
358
+ room: RoomOption,
359
+ path: str,
360
+ schema: Path = typer.Option(..., "--schema", help="Schema JSON file"),
361
+ json_payload: Optional[str] = typer.Option(
362
+ None, "--json", help="Initial JSON payload"
363
+ ),
364
+ json_file: Optional[Path] = typer.Option(
365
+ None, "--json-file", help="Path to initial JSON payload"
366
+ ),
367
+ ):
368
+ initial_json = _load_json_file(json_file, name="json")
369
+ if initial_json is None:
370
+ initial_json = _parse_json_arg(json_payload, name="json")
371
+
372
+ schema_json = _load_json_file(schema, name="schema")
373
+ if schema_json is None:
374
+ raise typer.BadParameter("--schema is required")
375
+
376
+ account_client, client = await _connect_room(project_id, room)
377
+ try:
378
+ if await client.storage.exists(path=path):
379
+ print(f"[red]Document already exists at {path}.[/red]")
380
+ raise typer.Exit(1)
381
+
382
+ await client.sync.open(
383
+ path=path,
384
+ create=True,
385
+ initial_json=initial_json,
386
+ schema=MeshSchema.from_json(schema_json),
387
+ )
388
+ await client.sync.close(path=path)
389
+ print(f"[green]Created document at {path}[/green]")
390
+ except RoomException as exc:
391
+ print(f"[red]{exc}[/red]")
392
+ raise typer.Exit(1)
393
+ finally:
394
+ await client.__aexit__(None, None, None)
395
+ await account_client.close()
396
+
397
+
398
+ @app.async_command("update", help="Apply a JSON patch to a document")
399
+ async def sync_update(
400
+ *,
401
+ project_id: ProjectIdOption,
402
+ room: RoomOption,
403
+ path: str,
404
+ patch: Optional[str] = typer.Option(None, "--patch", help="JSON patch array"),
405
+ patch_file: Optional[Path] = typer.Option(
406
+ None, "--patch-file", help="Path to JSON patch array"
407
+ ),
408
+ ):
409
+ patch_ops = _load_json_file(patch_file, name="patch")
410
+ if patch_ops is None:
411
+ patch_ops = _parse_json_arg(patch, name="patch")
412
+ if patch_ops is None:
413
+ raise typer.BadParameter("Provide --patch or --patch-file")
414
+ if not isinstance(patch_ops, list):
415
+ raise typer.BadParameter("Patch must be a JSON array")
416
+
417
+ account_client, client = await _connect_room(project_id, room)
418
+ try:
419
+ doc = await client.sync.open(path=path, create=False)
420
+ try:
421
+ current_json = doc.root.to_json()
422
+ updated_json = _apply_json_patch(current_json, patch_ops)
423
+ if not isinstance(updated_json, dict):
424
+ raise typer.BadParameter("Patch must produce a JSON object")
425
+ _apply_document_json(doc, updated_json)
426
+ print(f"[green]Updated document at {path}[/green]")
427
+ finally:
428
+ await client.sync.close(path=path)
429
+ except RoomException as exc:
430
+ print(f"[red]{exc}[/red]")
431
+ raise typer.Exit(1)
432
+ finally:
433
+ await client.__aexit__(None, None, None)
434
+ await account_client.close()