meshagent-cli 0.5.18__py3-none-any.whl → 0.6.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.

Potentially problematic release.


This version of meshagent-cli might be problematic. Click here for more details.

meshagent/cli/exec.py CHANGED
@@ -1,11 +1,10 @@
1
1
  import sys
2
- import tty as _tty
3
- import termios
2
+ import os
4
3
  from meshagent.api.websocket_protocol import WebSocketClientProtocol
5
4
  from meshagent.api import RoomClient
6
5
  from meshagent.api.helpers import websocket_room_url
7
6
  from typing import Annotated, Optional
8
- from meshagent.cli.common_options import ProjectIdOption, ApiKeyIdOption, RoomOption
7
+ from meshagent.cli.common_options import ProjectIdOption, RoomOption
9
8
  import asyncio
10
9
  import typer
11
10
  from rich import print
@@ -15,18 +14,69 @@ import signal
15
14
  import shutil
16
15
  import json
17
16
  from urllib.parse import quote
17
+ import threading
18
+ import time
18
19
 
19
- from meshagent.api import ParticipantToken
20
20
 
21
21
  import logging
22
22
 
23
23
  from meshagent.cli.helper import (
24
24
  get_client,
25
25
  resolve_project_id,
26
- resolve_api_key,
27
26
  resolve_room,
28
27
  )
29
28
 
29
+ if os.name == "nt":
30
+ import msvcrt
31
+ import ctypes
32
+ from ctypes import wintypes
33
+
34
+ _kernel32 = ctypes.windll.kernel32
35
+ _ENABLE_ECHO_INPUT = 0x0004
36
+ _ENABLE_LINE_INPUT = 0x0002
37
+
38
+ def set_raw(f):
39
+ """Disable line and echo mode for the given file handle."""
40
+ handle = msvcrt.get_osfhandle(f.fileno())
41
+ original_mode = wintypes.DWORD()
42
+ if not _kernel32.GetConsoleMode(handle, ctypes.byref(original_mode)):
43
+ return None
44
+ new_mode = original_mode.value & ~(_ENABLE_ECHO_INPUT | _ENABLE_LINE_INPUT)
45
+ _kernel32.SetConsoleMode(handle, new_mode)
46
+ return handle, original_mode.value
47
+
48
+ def restore(f, state):
49
+ if state is None:
50
+ return None
51
+ handle, mode = state
52
+ _kernel32.SetConsoleMode(handle, mode)
53
+ return None
54
+
55
+ else:
56
+ import termios
57
+ import tty as _tty
58
+
59
+ def set_raw(fd):
60
+ old = termios.tcgetattr(fd)
61
+ _tty.setraw(fd)
62
+ return old
63
+
64
+ def restore(fd, old_settings):
65
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
66
+
67
+
68
+ class _StdWriter:
69
+ """Simple asyncio-friendly wrapper for standard streams on Windows."""
70
+
71
+ def __init__(self, file):
72
+ self._file = file
73
+
74
+ def write(self, data: bytes) -> None:
75
+ self._file.buffer.write(data)
76
+
77
+ async def drain(self) -> None:
78
+ await asyncio.get_running_loop().run_in_executor(None, self._file.flush)
79
+
30
80
 
31
81
  def register(app: typer.Typer):
32
82
  @app.async_command("exec")
@@ -36,7 +86,6 @@ def register(app: typer.Typer):
36
86
  room: RoomOption,
37
87
  name: Annotated[Optional[str], typer.Option()] = None,
38
88
  image: Annotated[Optional[str], typer.Option()] = None,
39
- api_key_id: ApiKeyIdOption = None,
40
89
  command: Annotated[list[str], typer.Argument(...)] = None,
41
90
  tty: bool = False,
42
91
  room_storage_path: str = "/data",
@@ -45,27 +94,12 @@ def register(app: typer.Typer):
45
94
  client = await get_client()
46
95
  try:
47
96
  project_id = await resolve_project_id(project_id=project_id)
48
- api_key_id = await resolve_api_key(
49
- project_id=project_id, api_key_id=api_key_id
50
- )
51
97
  room = resolve_room(room)
52
98
 
53
- token = ParticipantToken(
54
- name="tty", project_id=project_id, api_key_id=api_key_id
55
- )
56
-
57
- key = (
58
- await client.decrypt_project_api_key(
59
- project_id=project_id, id=api_key_id
60
- )
61
- )["token"]
62
-
63
- token.add_role_grant(role="user")
64
- token.add_room_grant(room)
99
+ connection = await client.connect_room(project_id=project_id, room=room)
65
100
 
66
101
  ws_url = (
67
- websocket_room_url(room_name=room)
68
- + f"/exec?token={token.to_jwt(token=key)}"
102
+ websocket_room_url(room_name=room) + f"/exec?token={connection.jwt}"
69
103
  )
70
104
 
71
105
  if image:
@@ -98,13 +132,12 @@ def register(app: typer.Typer):
98
132
 
99
133
  if tty:
100
134
  # Save current terminal settings so we can restore them later.
101
- old_tty_settings = termios.tcgetattr(sys.stdin)
102
- _tty.setraw(sys.stdin)
135
+ old_tty_settings = set_raw(sys.stdin)
103
136
 
104
137
  async with RoomClient(
105
138
  protocol=WebSocketClientProtocol(
106
139
  url=websocket_room_url(room_name=room),
107
- token=token.to_jwt(token=key),
140
+ token=connection.jwt,
108
141
  )
109
142
  ):
110
143
  try:
@@ -113,25 +146,29 @@ def register(app: typer.Typer):
113
146
  send_queue = asyncio.Queue[bytes]()
114
147
 
115
148
  loop = asyncio.get_running_loop()
116
- (
117
- stdout_transport,
118
- stdout_protocol,
119
- ) = await loop.connect_write_pipe(
120
- asyncio.streams.FlowControlMixin, sys.stdout
121
- )
122
- stdout_writer = asyncio.StreamWriter(
123
- stdout_transport, stdout_protocol, None, loop
124
- )
149
+ if os.name == "nt":
150
+ stdout_writer = _StdWriter(sys.stdout)
151
+ stderr_writer = _StdWriter(sys.stderr)
152
+ else:
153
+ (
154
+ stdout_transport,
155
+ stdout_protocol,
156
+ ) = await loop.connect_write_pipe(
157
+ asyncio.streams.FlowControlMixin, sys.stdout
158
+ )
159
+ stdout_writer = asyncio.StreamWriter(
160
+ stdout_transport, stdout_protocol, None, loop
161
+ )
125
162
 
126
- (
127
- stderr_transport,
128
- stderr_protocol,
129
- ) = await loop.connect_write_pipe(
130
- asyncio.streams.FlowControlMixin, sys.stderr
131
- )
132
- stderr_writer = asyncio.StreamWriter(
133
- stderr_transport, stderr_protocol, None, loop
134
- )
163
+ (
164
+ stderr_transport,
165
+ stderr_protocol,
166
+ ) = await loop.connect_write_pipe(
167
+ asyncio.streams.FlowControlMixin, sys.stderr
168
+ )
169
+ stderr_writer = asyncio.StreamWriter(
170
+ stderr_transport, stderr_protocol, None, loop
171
+ )
135
172
 
136
173
  async def recv_from_websocket():
137
174
  while True:
@@ -206,46 +243,97 @@ def register(app: typer.Typer):
206
243
 
207
244
  task.add_done_callback(on_done)
208
245
 
209
- loop.add_signal_handler(signal.SIGWINCH, on_sigwinch)
246
+ if hasattr(signal, "SIGWINCH"):
247
+ loop.add_signal_handler(signal.SIGWINCH, on_sigwinch)
210
248
 
211
249
  async def read_stdin():
212
250
  loop = asyncio.get_running_loop()
213
251
 
214
- reader = asyncio.StreamReader()
215
- protocol = asyncio.StreamReaderProtocol(reader)
216
- await loop.connect_read_pipe(
217
- lambda: protocol, sys.stdin
218
- )
252
+ if os.name == "nt":
253
+ queue: asyncio.Queue[bytes] = asyncio.Queue()
254
+ stop_event = threading.Event()
255
+
256
+ if sys.stdin.isatty():
257
+
258
+ def reader() -> None:
259
+ try:
260
+ while not stop_event.is_set():
261
+ if msvcrt.kbhit():
262
+ data = msvcrt.getch()
263
+ loop.call_soon_threadsafe(
264
+ queue.put_nowait, data
265
+ )
266
+ else:
267
+ time.sleep(0.01)
268
+ finally:
269
+ loop.call_soon_threadsafe(
270
+ queue.put_nowait, b""
271
+ )
272
+ else:
219
273
 
220
- while True:
221
- # Read one character at a time from stdin without blocking the event loop.
222
- done, pending = await asyncio.wait(
223
- [
224
- asyncio.create_task(reader.read(1)),
225
- websocket_recv_task,
226
- ],
227
- return_when=asyncio.FIRST_COMPLETED,
274
+ def reader() -> None:
275
+ try:
276
+ while not stop_event.is_set():
277
+ data = sys.stdin.buffer.read(1)
278
+ loop.call_soon_threadsafe(
279
+ queue.put_nowait, data
280
+ )
281
+ if not data:
282
+ break
283
+ finally:
284
+ loop.call_soon_threadsafe(
285
+ queue.put_nowait, b""
286
+ )
287
+
288
+ thread = threading.Thread(target=reader)
289
+ thread.start()
290
+
291
+ async def reader_task() -> bytes:
292
+ return await queue.get()
293
+ else:
294
+ reader = asyncio.StreamReader()
295
+ protocol = asyncio.StreamReaderProtocol(reader)
296
+ await loop.connect_read_pipe(
297
+ lambda: protocol, sys.stdin
228
298
  )
229
299
 
230
- first = done.pop()
231
- if first == websocket_recv_task:
232
- break
233
-
234
- data = first.result()
235
- if not data:
236
- break
300
+ async def reader_task():
301
+ return await reader.read(1)
302
+
303
+ try:
304
+ while True:
305
+ # Read one character at a time from stdin without blocking the event loop.
306
+ done, pending = await asyncio.wait(
307
+ [
308
+ asyncio.create_task(reader_task()),
309
+ websocket_recv_task,
310
+ ],
311
+ return_when=asyncio.FIRST_COMPLETED,
312
+ )
313
+
314
+ first = done.pop()
315
+ if first == websocket_recv_task:
316
+ break
237
317
 
238
- if websocket.closed:
239
- break
318
+ data = first.result()
319
+ if not data:
320
+ break
240
321
 
241
- if tty:
242
- if data == b"\x04":
322
+ if websocket.closed:
243
323
  break
244
324
 
245
- if data:
246
- send_queue.put_nowait(b"\0" + data)
247
- else:
248
- break
325
+ if tty:
326
+ if data == b"\x04":
327
+ break
328
+
329
+ if data:
330
+ send_queue.put_nowait(b"\0" + data)
331
+ else:
332
+ break
333
+ finally:
334
+ if os.name == "nt":
335
+ stop_event.set()
336
+ thread.join()
249
337
 
250
338
  send_queue.put_nowait(b"\0")
251
339
 
@@ -283,9 +371,7 @@ def register(app: typer.Typer):
283
371
  finally:
284
372
  if not sys.stdin.closed and tty:
285
373
  # Restore original terminal settings even if the coroutine is cancelled.
286
- termios.tcsetattr(
287
- sys.stdin, termios.TCSADRAIN, old_tty_settings
288
- )
374
+ restore(sys.stdin, old_tty_settings)
289
375
 
290
376
  except Exception as e:
291
377
  print(f"[red]{e}[/red]")
meshagent/cli/helper.py CHANGED
@@ -7,10 +7,9 @@ from typing import Optional
7
7
  from meshagent.cli import auth_async
8
8
  from meshagent.cli import async_typer
9
9
  from meshagent.api.helpers import meshagent_base_url
10
- from meshagent.api.accounts_client import AccountsClient
11
- from meshagent.api.participant_token import ParticipantToken
12
-
10
+ from meshagent.api.client import Meshagent
13
11
  import os
12
+ from rich import print
14
13
 
15
14
  SETTINGS_FILE = Path.home() / ".meshagent" / "project.json"
16
15
 
@@ -50,25 +49,38 @@ async def set_active_project(project_id: str | None):
50
49
  _save_settings(settings)
51
50
 
52
51
 
53
- async def get_active_api_key(project_id: str):
52
+ async def set_active_api_key(project_id: str, key: str):
54
53
  settings = _load_settings()
55
- if settings is None:
56
- return None
57
- return settings.active_api_keys.get(project_id, None)
54
+ settings.active_api_keys[project_id] = key
55
+ _save_settings(settings)
58
56
 
59
57
 
60
- async def set_active_api_key(project_id: str, api_key_id: str | None):
58
+ async def get_active_api_key(project_id: str):
61
59
  settings = _load_settings()
62
- settings.active_api_keys[project_id] = api_key_id
63
- _save_settings(settings)
60
+ key: str = settings.active_api_keys.get(project_id)
61
+ # Ignore old keys, API key format changed
62
+ if key is not None and key.startswith("ma-"):
63
+ return key
64
+ else:
65
+ return None
64
66
 
65
67
 
66
68
  app = async_typer.AsyncTyper()
67
69
 
68
70
 
69
71
  async def get_client():
70
- access_token = await auth_async.get_access_token()
71
- return AccountsClient(base_url=meshagent_base_url(), token=access_token)
72
+ key = os.getenv("MESHAGENT_API_KEY")
73
+ if key is not None:
74
+ return Meshagent(
75
+ base_url=meshagent_base_url(),
76
+ token=key,
77
+ )
78
+ else:
79
+ access_token = await auth_async.get_access_token()
80
+ return Meshagent(
81
+ base_url=meshagent_base_url(),
82
+ token=access_token,
83
+ )
72
84
 
73
85
 
74
86
  def print_json_table(records: list, *cols):
@@ -118,62 +130,18 @@ async def resolve_project_id(project_id: Optional[str] = None):
118
130
  return project_id
119
131
 
120
132
 
121
- async def resolve_api_key(project_id: str, api_key_id: Optional[str] = None):
122
- if api_key_id is None:
123
- api_key_id = await get_active_api_key(project_id=project_id)
133
+ async def resolve_key(project_id: str | None, key: str | None):
134
+ project_id = await resolve_project_id(project_id=project_id)
135
+ if key is None:
136
+ key = await get_active_api_key(project_id=project_id)
137
+
138
+ if key is None:
139
+ key = os.getenv("MESHAGENT_API_KEY")
124
140
 
125
- if api_key_id is None:
141
+ if key is None:
126
142
  print(
127
- "[red]API Key ID not specified, activate an api key or pass an api key id on the command line[/red]"
143
+ "[red]--key is required if MESHGENT_API_KEY is not set. You can use meshagent api-key create to create a new api key."
128
144
  )
129
- raise typer.Exit(code=1)
145
+ raise typer.Exit(1)
130
146
 
131
- return api_key_id
132
-
133
-
134
- async def resolve_token_jwt(
135
- *,
136
- project_id: str,
137
- api_key_id: Optional[str] = None,
138
- token_path: Optional[str] = None,
139
- name: Optional[str] = None,
140
- role: Optional[str] = None,
141
- room: Optional[str] = None,
142
- ) -> str:
143
- jwt = None
144
-
145
- if api_key_id is None:
146
- if token_path is not None:
147
- if token_path is None:
148
- token_path = os.getenv(
149
- "MESHAGENT_TOKEN_PATH",
150
- (Path.home() / ".meshagent" / "token").as_posix(),
151
- )
152
-
153
- p = Path(token_path)
154
- jwt = p.read_text().strip()
155
-
156
- else:
157
- jwt = os.getenv("MESHAGENT_TOKEN", None)
158
-
159
- if jwt is None:
160
- account_client = await get_client()
161
- try:
162
- key = (
163
- await account_client.decrypt_project_api_key(
164
- project_id=project_id, id=api_key_id
165
- )
166
- )["token"]
167
-
168
- token = ParticipantToken(
169
- name=name, project_id=project_id, api_key_id=api_key_id
170
- )
171
-
172
- token.add_role_grant(role=role)
173
- token.add_room_grant(room)
174
-
175
- jwt = token.to_jwt(token=key)
176
- finally:
177
- await account_client.close()
178
-
179
- return jwt
147
+ return key
@@ -0,0 +1,131 @@
1
+ from meshagent.cli import async_typer
2
+
3
+
4
+ from meshagent.api import SchemaRegistry, SchemaRegistration
5
+
6
+
7
+ import logging
8
+
9
+ app = async_typer.AsyncTyper(help="Join a mailbot to a room")
10
+
11
+
12
+ @app.async_command("service")
13
+ async def helpers_service():
14
+ from meshagent.agents.planning import DynamicPlanningResponder, PlanningResponder
15
+ from meshagent.openai.tools import OpenAIResponsesAdapter
16
+ from meshagent.tools.storage import StorageToolkit
17
+ from meshagent.api.services import ServiceHost
18
+
19
+ from meshagent.agents.schemas.gallery import gallery_schema
20
+ from meshagent.agents.schemas.document import document_schema
21
+ from meshagent.agents.schemas.transcript import transcript_schema
22
+ from meshagent.agents.schemas.super_editor_document import (
23
+ super_editor_document_schema,
24
+ )
25
+ from meshagent.agents.schemas.presentation import presentation_schema
26
+ from meshagent.agents import thread_schema
27
+
28
+ logging.getLogger("openai").setLevel(logging.ERROR)
29
+ logging.getLogger("httpx").setLevel(logging.ERROR)
30
+
31
+ service = ServiceHost(port=9000)
32
+
33
+ @service.path("/planner")
34
+ class Planner(PlanningResponder):
35
+ def __init__(self, **kwargs):
36
+ super().__init__(
37
+ name="meshagent.planner",
38
+ title="Generic Task Runner",
39
+ description="an agent that will perform a task with the selected tools",
40
+ llm_adapter=OpenAIResponsesAdapter(model="gpt-4.1"),
41
+ supports_tools=True,
42
+ input_prompt=True,
43
+ output_schema={
44
+ "type": "object",
45
+ "required": ["result"],
46
+ "additionalProperties": False,
47
+ "properties": {"result": {"type": "string"}},
48
+ },
49
+ )
50
+
51
+ @service.path("/schema_planner")
52
+ class DynamicPlanner(DynamicPlanningResponder):
53
+ def __init__(self, **kwargs):
54
+ super().__init__(
55
+ name="meshagent.schema_planner",
56
+ title="Schema Task Runner",
57
+ description="an agent that can produces output that matches a schema",
58
+ llm_adapter=OpenAIResponsesAdapter(model="gpt-4.1"),
59
+ )
60
+
61
+ @service.path("/schemas/document")
62
+ class DocumentSchemaRegistry(SchemaRegistry):
63
+ def __init__(self):
64
+ name = "document"
65
+ schema = document_schema
66
+ super().__init__(
67
+ name=f"meshagent.schema.{name}",
68
+ validate_webhook_secret=False,
69
+ schemas=[SchemaRegistration(name=name, schema=schema)],
70
+ )
71
+
72
+ @service.path("/schemas/superdoc")
73
+ class SuperdocDocumentSchemaRegistry(SchemaRegistry):
74
+ def __init__(self):
75
+ name = "superdoc"
76
+ schema = super_editor_document_schema
77
+ super().__init__(
78
+ name=f"meshagent.schema.{name}",
79
+ validate_webhook_secret=False,
80
+ schemas=[SchemaRegistration(name=name, schema=schema)],
81
+ )
82
+
83
+ @service.path("/schemas/gallery")
84
+ class GalleryDocumentSchemaRegistry(SchemaRegistry):
85
+ def __init__(self):
86
+ name = "gallery"
87
+ schema = gallery_schema
88
+ super().__init__(
89
+ name=f"meshagent.schema.{name}",
90
+ validate_webhook_secret=False,
91
+ schemas=[SchemaRegistration(name=name, schema=schema)],
92
+ )
93
+
94
+ @service.path("/schemas/thread")
95
+ class ThreadDocumentSchemaRegistry(SchemaRegistry):
96
+ def __init__(self):
97
+ name = "thread"
98
+ schema = thread_schema
99
+ super().__init__(
100
+ name=f"meshagent.schema.{name}",
101
+ validate_webhook_secret=False,
102
+ schemas=[SchemaRegistration(name=name, schema=schema)],
103
+ )
104
+
105
+ @service.path("/schemas/presentation")
106
+ class PresentationDocumentSchemaRegistry(SchemaRegistry):
107
+ def __init__(presentation):
108
+ name = "presentation"
109
+ schema = presentation_schema
110
+ super().__init__(
111
+ name=f"meshagent.schema.{name}",
112
+ validate_webhook_secret=False,
113
+ schemas=[SchemaRegistration(name=name, schema=schema)],
114
+ )
115
+
116
+ @service.path("/schemas/transcript")
117
+ class TranscriptRegistry(SchemaRegistry):
118
+ def __init__(self):
119
+ name = "transcript"
120
+ schema = transcript_schema
121
+ super().__init__(
122
+ name=f"meshagent.schema.{name}",
123
+ validate_webhook_secret=False,
124
+ schemas=[SchemaRegistration(name=name, schema=schema)],
125
+ )
126
+
127
+ @service.path("/toolkits/storage")
128
+ class HostedStorageToolkit(StorageToolkit):
129
+ pass
130
+
131
+ await service.run()