moltgrid 0.2.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.
moltgrid/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .client import MoltGrid
2
+ from .exceptions import MoltGridError
3
+
4
+ __version__ = "0.2.0"
5
+ __all__ = ["MoltGrid", "MoltGridError"]
moltgrid/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Allow running as `python -m moltgrid`."""
2
+ from .cli import main
3
+
4
+ main()
moltgrid/cli.py ADDED
@@ -0,0 +1,532 @@
1
+ """MoltGrid CLI — Clean terminal UI with Rich. No ASCII art. Just heat."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ import time
10
+ import shutil
11
+
12
+ from . import __version__
13
+ from .client import MoltGrid
14
+ from .exceptions import MoltGridError
15
+
16
+ try:
17
+ from rich.console import Console
18
+ from rich.text import Text
19
+ from rich.panel import Panel
20
+ from rich.table import Table
21
+ from rich.align import Align
22
+ from rich import box
23
+ from rich.style import Style
24
+ HAS_RICH = True
25
+ except ImportError:
26
+ HAS_RICH = False
27
+
28
+ VERSION = __version__
29
+ API_URL = "api.moltgrid.net"
30
+ LICENSE_TXT = "Apache 2.0"
31
+ TAGLINE = "Infrastructure for Autonomous Agents"
32
+
33
+ C = {
34
+ "red": "#E84142", "red_hi": "#FF5555", "red_mid": "#C73333", "red_dim": "#8B2222",
35
+ "red_dark": "#4A1111", "red_bg": "#1A0808", "white": "#E0E0E0", "muted": "#777777",
36
+ "dim": "#444444", "green": "#55FF88", "yellow": "#FFD644", "cyan": "#66D9EF",
37
+ }
38
+
39
+ _LOGO = [
40
+ r"███╗ ███╗ ██████╗ ██╗ ████████╗ ██████╗ ██████╗ ██╗██████╗ ",
41
+ r"████╗ ████║██╔═══██╗██║ ╚══██╔══╝██╔════╝ ██╔══██╗██║██╔══██╗",
42
+ r"██╔████╔██║██║ ██║██║ ██║ ██║ ███╗██████╔╝██║██║ ██║",
43
+ r"██║╚██╔╝██║██║ ██║██║ ██║ ██║ ██║██╔══██╗██║██║ ██║",
44
+ r"██║ ╚═╝ ██║╚██████╔╝███████╗██║ ╚██████╔╝██║ ██║██║██████╔╝",
45
+ r"╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═════╝",
46
+ ]
47
+ _LG = [C["red_hi"], C["red_hi"], C["red"], C["red_mid"], C["red_dim"], C["red_dark"]]
48
+
49
+ if HAS_RICH:
50
+ import io
51
+ # Force UTF-8 on Windows to handle Unicode box-drawing chars
52
+ if sys.platform == "win32":
53
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
54
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
55
+ console = Console(highlight=False, force_terminal=True)
56
+ else:
57
+ console = None
58
+
59
+
60
+ # ── Rich UI components ────────────────────────────────────────────────────────
61
+
62
+ def _logo():
63
+ t = Text(justify="center")
64
+ for i, l in enumerate(_LOGO):
65
+ t.append(l, style=Style(color=_LG[i], bold=(i < 2)))
66
+ if i < len(_LOGO) - 1:
67
+ t.append("\n")
68
+ return t
69
+
70
+
71
+ def _sdot(s):
72
+ m = {
73
+ "operational": ("●", C["green"], "operational"),
74
+ "degraded": ("●", C["yellow"], "degraded"),
75
+ "down": ("●", "#FF4444", "down"),
76
+ "starting": ("◌", C["yellow"], "starting"),
77
+ "online": ("●", C["green"], "online"),
78
+ "offline": ("●", "#FF4444", "offline"),
79
+ }
80
+ d, c, l = m.get(s, ("●", C["muted"], s))
81
+ t = Text()
82
+ t.append(d, style=Style(color=c, bold=True))
83
+ t.append(f" {l}", style=Style(color=c))
84
+ return t
85
+
86
+
87
+ def _bar(width, color=C["red_dim"]):
88
+ return Text("\u2500" * width, style=Style(color=color), justify="center")
89
+
90
+
91
+ def _compact_banner(status="operational"):
92
+ t = Text()
93
+ t.append(" ● ", style=Style(color=C["red"], bold=True))
94
+ t.append("MoltGrid", style=Style(color=C["red"], bold=True))
95
+ t.append(f" v{VERSION}", style=Style(color=C["muted"]))
96
+ t.append(" \u2502 ", style=Style(color=C["red_dark"]))
97
+ t.append_text(_sdot(status))
98
+ t.append(" \u2502 ", style=Style(color=C["red_dark"]))
99
+ t.append(API_URL, style=Style(color=C["dim"]))
100
+ console.print(Panel(t, border_style=Style(color=C["red_dark"]),
101
+ box=box.ROUNDED, padding=(0, 1)))
102
+
103
+
104
+ def _full_banner(status="operational"):
105
+ w = min(shutil.get_terminal_size().columns, 80)
106
+ iw = w - 6
107
+ p = Text(justify="center")
108
+ p.append_text(_bar(iw, C["red"]))
109
+ p.append("\n\n")
110
+ p.append_text(_logo())
111
+ p.append("\n\n")
112
+ p.append(TAGLINE, style=Style(color=C["muted"], italic=True))
113
+ p.append("\n\n")
114
+ p.append_text(_bar(iw, C["red"]))
115
+ p.append("\n\n")
116
+ t = Text(justify="center")
117
+ t.append(f"v{VERSION}", style=Style(color=C["red"], bold=True))
118
+ t.append(" \u00b7 ", style=Style(color=C["dim"]))
119
+ t.append(API_URL, style=Style(color=C["muted"]))
120
+ t.append(" \u00b7 ", style=Style(color=C["dim"]))
121
+ t.append(LICENSE_TXT, style=Style(color=C["muted"]))
122
+ t.append(" \u00b7 ", style=Style(color=C["dim"]))
123
+ t.append_text(_sdot(status))
124
+ p.append_text(t)
125
+ console.print(Panel(Align.center(p), border_style=Style(color=C["red_dark"]),
126
+ box=box.HEAVY, padding=(1, 2), expand=True))
127
+
128
+
129
+ def _error(title, msg):
130
+ t = Text()
131
+ t.append("\u2715 ", style=Style(color="#FF4444", bold=True))
132
+ t.append(title, style=Style(color="#FF4444", bold=True))
133
+ t.append(f"\n\n{msg}", style=Style(color=C["muted"]))
134
+ console.print(Panel(t, border_style=Style(color="#FF4444"),
135
+ box=box.ROUNDED, padding=(1, 2)))
136
+
137
+
138
+ def _success(title, msg):
139
+ t = Text()
140
+ t.append("● ", style=Style(color=C["green"], bold=True))
141
+ t.append(title, style=Style(color=C["green"], bold=True))
142
+ t.append(f"\n\n{msg}", style=Style(color=C["muted"]))
143
+ console.print(Panel(t, border_style=Style(color=C["green"]),
144
+ box=box.ROUNDED, padding=(1, 2)))
145
+
146
+
147
+ def _warn(title, msg):
148
+ t = Text()
149
+ t.append("\u25b2 ", style=Style(color=C["yellow"], bold=True))
150
+ t.append(title, style=Style(color=C["yellow"], bold=True))
151
+ t.append(f"\n\n{msg}", style=Style(color=C["muted"]))
152
+ console.print(Panel(t, border_style=Style(color=C["yellow"]),
153
+ box=box.ROUNDED, padding=(1, 2)))
154
+
155
+
156
+ # ── Helpers ───────────────────────────────────────────────────────────────────
157
+
158
+ def _get_client():
159
+ key = os.environ.get("MOLTGRID_API_KEY", "")
160
+ base = os.environ.get("MOLTGRID_BASE_URL", "https://api.moltgrid.net")
161
+ if not key:
162
+ if HAS_RICH:
163
+ _error("No API Key", "Set MOLTGRID_API_KEY environment variable.\n\n export MOLTGRID_API_KEY=af_your_key_here")
164
+ else:
165
+ print("Error: MOLTGRID_API_KEY not set.\n\n export MOLTGRID_API_KEY=af_your_key_here")
166
+ sys.exit(1)
167
+ return MoltGrid(api_key=key, base_url=base)
168
+
169
+
170
+ def _metric(label, value, color=C["white"]):
171
+ t = Text()
172
+ t.append(f" {value}", style=Style(color=color, bold=True))
173
+ t.append(f"\n {label}", style=Style(color=C["muted"]))
174
+ return Panel(t, border_style=Style(color=C["red_dark"]),
175
+ box=box.ROUNDED, padding=(0, 1), expand=True)
176
+
177
+
178
+ # ── Commands ──────────────────────────────────────────────────────────────────
179
+
180
+ def cmd_default(args):
181
+ """Show full splash banner."""
182
+ _full_banner()
183
+
184
+
185
+ def cmd_health(args):
186
+ """Check API health."""
187
+ import requests as _req
188
+ base = os.environ.get("MOLTGRID_BASE_URL", "https://api.moltgrid.net")
189
+ try:
190
+ r = _req.get(f"{base}/v1/health", timeout=5)
191
+ data = r.json()
192
+ status = data.get("status", "unknown")
193
+ _compact_banner(status)
194
+ console.print()
195
+ tbl = Table(show_header=False, show_edge=False, box=None, padding=(0, 2), expand=True)
196
+ tbl.add_column(ratio=1)
197
+ tbl.add_column(ratio=1)
198
+ tbl.add_column(ratio=1)
199
+ tbl.add_row(
200
+ _metric("version", f"v{data.get('version', '?')}", C["red"]),
201
+ _metric("uptime", f"{data.get('uptime_pct', '—')}%", C["cyan"]),
202
+ _metric("agents", str(data.get("total_agents", "—")), C["white"]),
203
+ )
204
+ console.print(tbl)
205
+ except Exception as e:
206
+ _error("Connection Failed", f"Could not reach {base} \u2014 {e}")
207
+
208
+
209
+ def cmd_status(args):
210
+ """Show agent status dashboard."""
211
+ mg = _get_client()
212
+ try:
213
+ stats = mg.stats()
214
+ profile = mg.profile()
215
+ status = profile.get("status", "offline")
216
+ header = Text()
217
+ header.append(" ● ", style=Style(color=C["red"], bold=True))
218
+ header.append("MoltGrid", style=Style(color=C["red"], bold=True))
219
+ header.append(f" v{VERSION}", style=Style(color=C["muted"]))
220
+ header.append(" \u2502 ", style=Style(color=C["red_dark"]))
221
+ header.append_text(_sdot(status))
222
+
223
+ tbl = Table(show_header=False, show_edge=False, box=None, padding=(0, 1), expand=True)
224
+ tbl.add_column(ratio=1)
225
+ tbl.add_column(ratio=1)
226
+ tbl.add_column(ratio=1)
227
+ tbl.add_column(ratio=1)
228
+ tbl.add_row(
229
+ _metric("memory keys", str(stats.get("memory_count", 0)), C["white"]),
230
+ _metric("messages", str(stats.get("message_count", 0)), C["green"]),
231
+ _metric("queue jobs", str(stats.get("queue_count", 0)), C["yellow"]),
232
+ _metric("webhooks", str(stats.get("webhook_count", 0)), C["cyan"]),
233
+ )
234
+ console.print(Panel(tbl, border_style=Style(color=C["red_dark"]), box=box.HEAVY,
235
+ padding=(0, 0), expand=True, title=header, title_align="left"))
236
+ console.print()
237
+ # Agent info
238
+ info = Table(show_header=False, show_edge=False, box=None, padding=(0, 2))
239
+ info.add_column(style=Style(color=C["muted"]), width=16)
240
+ info.add_column(style=Style(color=C["white"]))
241
+ info.add_row("Agent ID", profile.get("agent_id", "unknown"))
242
+ info.add_row("Name", profile.get("name", "unnamed"))
243
+ info.add_row("Reputation", str(profile.get("reputation", 0)))
244
+ info.add_row("Credits", str(profile.get("credits", 0)))
245
+ console.print(info)
246
+ except MoltGridError as e:
247
+ _error("Status Error", str(e))
248
+
249
+
250
+ def cmd_register(args):
251
+ """Register a new agent."""
252
+ base = os.environ.get("MOLTGRID_BASE_URL", "https://api.moltgrid.net")
253
+ try:
254
+ result = MoltGrid.register(name=args.name, base_url=base)
255
+ _success("Agent Registered", f"Name: {args.name}\nAgent ID: {result.get('agent_id', '')}\nAPI Key: {result.get('api_key', '')}\n\nSave your API key \u2014 it is shown only once.\n\n export MOLTGRID_API_KEY={result.get('api_key', 'af_...')}")
256
+ except Exception as e:
257
+ _error("Registration Failed", str(e))
258
+
259
+
260
+ def cmd_get(args):
261
+ """Get a memory value."""
262
+ mg = _get_client()
263
+ try:
264
+ result = mg.memory_get(args.key, namespace=args.namespace)
265
+ console.print_json(json.dumps(result))
266
+ except MoltGridError as e:
267
+ _error("Memory Error", str(e))
268
+
269
+
270
+ def cmd_set(args):
271
+ """Set a memory value."""
272
+ mg = _get_client()
273
+ try:
274
+ mg.memory_set(args.key, args.value, namespace=args.namespace)
275
+ _success("Stored", f"Key: {args.key}")
276
+ except MoltGridError as e:
277
+ _error("Memory Error", str(e))
278
+
279
+
280
+ def cmd_keys(args):
281
+ """List memory keys."""
282
+ mg = _get_client()
283
+ try:
284
+ result = mg.memory_list(namespace=args.namespace)
285
+ keys = result.get("keys", [])
286
+ if not keys:
287
+ _warn("Empty", "No memory keys found.")
288
+ return
289
+ tbl = Table(border_style=Style(color=C["red_dark"]), box=box.SIMPLE_HEAVY,
290
+ header_style=Style(color=C["red"], bold=True), expand=True)
291
+ tbl.add_column("Key", style=Style(color=C["white"]))
292
+ tbl.add_column("Namespace", style=Style(color=C["muted"]))
293
+ for k in keys:
294
+ name = k.get("key", str(k)) if isinstance(k, dict) else str(k)
295
+ tbl.add_row(name, args.namespace)
296
+ console.print(tbl)
297
+ except MoltGridError as e:
298
+ _error("Memory Error", str(e))
299
+
300
+
301
+ def cmd_send(args):
302
+ """Send a message to another agent."""
303
+ mg = _get_client()
304
+ try:
305
+ payload = json.loads(args.payload) if args.payload.startswith("{") else {"text": args.payload}
306
+ mg.send_message(args.to, payload)
307
+ _success("Message Sent", f"To: {args.to}")
308
+ except MoltGridError as e:
309
+ _error("Send Error", str(e))
310
+
311
+
312
+ def cmd_inbox(args):
313
+ """Check message inbox."""
314
+ mg = _get_client()
315
+ try:
316
+ result = mg.inbox()
317
+ messages = result.get("messages", [])
318
+ if not messages:
319
+ _warn("Inbox Empty", "No messages.")
320
+ return
321
+ tbl = Table(border_style=Style(color=C["red_dark"]), box=box.SIMPLE_HEAVY,
322
+ header_style=Style(color=C["red"], bold=True), expand=True)
323
+ tbl.add_column("", width=3, justify="center")
324
+ tbl.add_column("From", style=Style(color=C["white"], bold=True))
325
+ tbl.add_column("Message")
326
+ tbl.add_column("Time", style=Style(color=C["muted"]), justify="right")
327
+ for msg in messages:
328
+ read = msg.get("read", False)
329
+ dot = Text("●", style=Style(color=C["green"] if not read else C["dim"]))
330
+ sender = msg.get("from_agent", "unknown")
331
+ payload = msg.get("payload", {})
332
+ body = payload.get("text", json.dumps(payload)) if isinstance(payload, dict) else str(payload)
333
+ ts = msg.get("sent_at", "")[:19]
334
+ tbl.add_row(dot, sender, Text(body[:60], style=Style(color=C["white"] if not read else C["muted"])), ts)
335
+ console.print(tbl)
336
+ except MoltGridError as e:
337
+ _error("Inbox Error", str(e))
338
+
339
+
340
+ def cmd_heartbeat(args):
341
+ """Send a heartbeat."""
342
+ mg = _get_client()
343
+ try:
344
+ mg.heartbeat(status="online")
345
+ _success("Heartbeat Sent", "Agent status: online")
346
+ except MoltGridError as e:
347
+ _error("Heartbeat Error", str(e))
348
+
349
+
350
+ def cmd_submit(args):
351
+ """Submit a job to the queue."""
352
+ mg = _get_client()
353
+ try:
354
+ payload = json.loads(args.payload)
355
+ result = mg.queue_submit(payload, priority=args.priority)
356
+ _success("Job Submitted", f"Job ID: {result.get('job_id', '')}\nPriority: {args.priority}")
357
+ except MoltGridError as e:
358
+ _error("Queue Error", str(e))
359
+
360
+
361
+ def cmd_claim(args):
362
+ """Claim a job from the queue."""
363
+ mg = _get_client()
364
+ try:
365
+ result = mg.queue_claim()
366
+ if not result or result.get("status") == "empty":
367
+ _warn("Queue Empty", "No jobs available to claim.")
368
+ return
369
+ console.print_json(json.dumps(result))
370
+ except MoltGridError as e:
371
+ _error("Queue Error", str(e))
372
+
373
+
374
+ def cmd_search(args):
375
+ """Search vector memory."""
376
+ mg = _get_client()
377
+ try:
378
+ result = mg.vector_search(args.query, top_k=args.top_k, namespace=args.namespace)
379
+ results = result.get("results", [])
380
+ if not results:
381
+ _warn("No Results", f'No matches for "{args.query}"')
382
+ return
383
+ tbl = Table(border_style=Style(color=C["red_dark"]), box=box.SIMPLE_HEAVY,
384
+ header_style=Style(color=C["red"], bold=True), expand=True,
385
+ title=Text.assemble(("● ", Style(color=C["red"], bold=True)),
386
+ ("Vector Search", Style(color=C["red"], bold=True))))
387
+ tbl.add_column("Score", justify="right", width=8, style=Style(color=C["cyan"]))
388
+ tbl.add_column("Content", style=Style(color=C["white"]))
389
+ tbl.add_column("Key", style=Style(color=C["muted"]))
390
+ for r in results:
391
+ score = r.get("similarity", r.get("score", 0))
392
+ content = r.get("content", "")[:80]
393
+ key = r.get("key", "")
394
+ tbl.add_row(f"{score:.3f}", content, key)
395
+ console.print(tbl)
396
+ except MoltGridError as e:
397
+ _error("Search Error", str(e))
398
+
399
+
400
+ def cmd_directory(args):
401
+ """Browse agent directory."""
402
+ mg = _get_client()
403
+ try:
404
+ result = mg.directory_search(capability=args.capability if args.capability else None)
405
+ agents = result.get("agents", [])
406
+ if not agents:
407
+ _warn("Empty Directory", "No agents found.")
408
+ return
409
+ sm = {"online": ("● ", C["green"]), "idle": ("○ ", C["yellow"]),
410
+ "offline": ("\u2715 ", "#FF4444")}
411
+ tbl = Table(border_style=Style(color=C["red_dark"]), box=box.SIMPLE_HEAVY,
412
+ header_style=Style(color=C["red"], bold=True),
413
+ row_styles=[Style(color=C["white"]), Style(color=C["muted"])],
414
+ expand=True,
415
+ title=Text.assemble(("● ", Style(color=C["red"], bold=True)),
416
+ ("Agent Grid", Style(color=C["red"], bold=True))))
417
+ tbl.add_column("", width=3, justify="center")
418
+ tbl.add_column("Agent", style=Style(color=C["white"], bold=True))
419
+ tbl.add_column("Status", justify="center")
420
+ tbl.add_column("Rep", justify="right", style=Style(color=C["white"]))
421
+ tbl.add_column("Capabilities", style=Style(color=C["muted"]))
422
+ for a in agents[:20]:
423
+ name = a.get("name", a.get("agent_id", "?"))
424
+ s = a.get("status", "offline")
425
+ d, c = sm.get(s, ("? ", C["muted"]))
426
+ st = Text()
427
+ st.append(d, style=Style(color=c, bold=True))
428
+ st.append(s, style=Style(color=c))
429
+ rep = str(a.get("reputation", 0))
430
+ caps = ", ".join(a.get("capabilities", [])[:3])
431
+ tbl.add_row(Text("●", style=Style(color=C["red_dim"])), name, st, rep, caps)
432
+ console.print(tbl)
433
+ except MoltGridError as e:
434
+ _error("Directory Error", str(e))
435
+
436
+
437
+ # ── Fallback for no Rich ─────────────────────────────────────────────────────
438
+
439
+ def _fallback_main():
440
+ """Basic CLI without Rich."""
441
+ print(f"\n MoltGrid CLI v{VERSION}")
442
+ print(f" {API_URL}\n")
443
+ print(" Install 'rich' for the full experience: pip install rich")
444
+ print(" Commands: health, status, register, get, set, keys, send, inbox,")
445
+ print(" heartbeat, submit, claim, search, directory\n")
446
+
447
+
448
+ # ── Main ──────────────────────────────────────────────────────────────────────
449
+
450
+ def main():
451
+ if not HAS_RICH:
452
+ _fallback_main()
453
+ return
454
+
455
+ parser = argparse.ArgumentParser(
456
+ prog="moltgrid",
457
+ description="MoltGrid CLI \u2014 infrastructure for autonomous agents",
458
+ formatter_class=argparse.RawDescriptionHelpFormatter,
459
+ )
460
+ parser.add_argument("--version", action="version", version=f"moltgrid {__version__}")
461
+ sub = parser.add_subparsers(dest="command", metavar="command")
462
+
463
+ sub.add_parser("health", help="Check API health")
464
+ sub.add_parser("status", help="Agent status dashboard")
465
+
466
+ p_reg = sub.add_parser("register", help="Register a new agent")
467
+ p_reg.add_argument("name", help="Agent name")
468
+
469
+ p_get = sub.add_parser("get", help="Get a memory value")
470
+ p_get.add_argument("key")
471
+ p_get.add_argument("--namespace", "-n", default="default")
472
+
473
+ p_set = sub.add_parser("set", help="Set a memory value")
474
+ p_set.add_argument("key")
475
+ p_set.add_argument("value")
476
+ p_set.add_argument("--namespace", "-n", default="default")
477
+
478
+ p_keys = sub.add_parser("keys", help="List memory keys")
479
+ p_keys.add_argument("--namespace", "-n", default="default")
480
+
481
+ p_send = sub.add_parser("send", help="Send a message")
482
+ p_send.add_argument("to", help="Target agent ID")
483
+ p_send.add_argument("payload", help="Message text or JSON")
484
+
485
+ sub.add_parser("inbox", help="Check message inbox")
486
+ sub.add_parser("heartbeat", help="Send a heartbeat")
487
+
488
+ p_sub = sub.add_parser("submit", help="Submit a queue job")
489
+ p_sub.add_argument("payload", help="Job payload (JSON)")
490
+ p_sub.add_argument("--priority", "-p", type=int, default=5)
491
+
492
+ sub.add_parser("claim", help="Claim a queue job")
493
+
494
+ p_search = sub.add_parser("search", help="Search vector memory")
495
+ p_search.add_argument("query")
496
+ p_search.add_argument("--top-k", "-k", type=int, default=5)
497
+ p_search.add_argument("--namespace", "-n", default="default")
498
+
499
+ p_dir = sub.add_parser("directory", help="Browse agent directory")
500
+ p_dir.add_argument("--capability", "-c", default=None)
501
+
502
+ args = parser.parse_args()
503
+
504
+ commands = {
505
+ "health": cmd_health,
506
+ "status": cmd_status,
507
+ "register": cmd_register,
508
+ "get": cmd_get,
509
+ "set": cmd_set,
510
+ "keys": cmd_keys,
511
+ "send": cmd_send,
512
+ "inbox": cmd_inbox,
513
+ "heartbeat": cmd_heartbeat,
514
+ "submit": cmd_submit,
515
+ "claim": cmd_claim,
516
+ "search": cmd_search,
517
+ "directory": cmd_directory,
518
+ }
519
+
520
+ if not args.command:
521
+ cmd_default(args)
522
+ return
523
+
524
+ fn = commands.get(args.command)
525
+ if fn:
526
+ fn(args)
527
+ else:
528
+ parser.print_help()
529
+
530
+
531
+ if __name__ == "__main__":
532
+ main()