magmascript 1.0.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 (39) hide show
  1. magmascript/__init__.py +41 -0
  2. magmascript/cli.py +628 -0
  3. magmascript/core/__init__.py +40 -0
  4. magmascript/core/cache.py +193 -0
  5. magmascript/core/config.py +150 -0
  6. magmascript/core/exceptions.py +59 -0
  7. magmascript/core/github.py +179 -0
  8. magmascript/core/output.py +136 -0
  9. magmascript/core/registry.py +31 -0
  10. magmascript/core/rpc.py +184 -0
  11. magmascript/domains/__init__.py +10 -0
  12. magmascript/domains/gh/__init__.py +15 -0
  13. magmascript/domains/gh/client.py +212 -0
  14. magmascript/domains/gh/tools.py +116 -0
  15. magmascript/domains/mcp/__init__.py +35 -0
  16. magmascript/domains/mcp/client.py +251 -0
  17. magmascript/domains/mcp/tools.py +411 -0
  18. magmascript/domains/media/__init__.py +21 -0
  19. magmascript/domains/media/client.py +202 -0
  20. magmascript/domains/media/providers/__init__.py +33 -0
  21. magmascript/domains/media/providers/archive.py +89 -0
  22. magmascript/domains/media/providers/met_museum.py +110 -0
  23. magmascript/domains/media/providers/openverse.py +90 -0
  24. magmascript/domains/media/providers/pexels.py +106 -0
  25. magmascript/domains/media/providers/pixabay.py +116 -0
  26. magmascript/domains/media/providers/smithsonian.py +94 -0
  27. magmascript/domains/media/tools.py +53 -0
  28. magmascript/domains/pi/__init__.py +19 -0
  29. magmascript/domains/pi/client.py +259 -0
  30. magmascript/domains/pi/tools.py +98 -0
  31. magmascript/domains/scores/__init__.py +21 -0
  32. magmascript/domains/scores/client.py +201 -0
  33. magmascript/domains/scores/tools.py +55 -0
  34. magmascript-1.0.0.dist-info/METADATA +140 -0
  35. magmascript-1.0.0.dist-info/RECORD +39 -0
  36. magmascript-1.0.0.dist-info/WHEEL +5 -0
  37. magmascript-1.0.0.dist-info/entry_points.txt +2 -0
  38. magmascript-1.0.0.dist-info/licenses/LICENSE +21 -0
  39. magmascript-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,41 @@
1
+ """magmascript — a scripting toolkit with domain-first subcommands."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from magmascript.core.config import Config, GHConfig, MediaConfig, PIConfig, get_config, load_config, set_config
6
+ from magmascript.core.registry import get_domain, list_domains, register_domain
7
+ from magmascript.core.rpc import RPCClient, RPCError, RPCResponse
8
+ from magmascript.core.output import format_output, format_table, format_json
9
+
10
+ # Import domains to trigger registration
11
+ from magmascript import domains # noqa: F401
12
+
13
+ # Convenience: expose clients at top level
14
+ from magmascript.domains.mcp import MCPClient
15
+ from magmascript.domains.pi import PIClient
16
+ from magmascript.domains.gh import GHClient
17
+ from magmascript.domains.media import MediaClient
18
+
19
+ __all__ = [
20
+ "__version__",
21
+ "Config",
22
+ "GHClient",
23
+ "GHConfig",
24
+ "MCPClient",
25
+ "MediaClient",
26
+ "MediaConfig",
27
+ "PIClient",
28
+ "PIConfig",
29
+ "RPCClient",
30
+ "RPCError",
31
+ "RPCResponse",
32
+ "format_output",
33
+ "format_json",
34
+ "format_table",
35
+ "get_config",
36
+ "get_domain",
37
+ "list_domains",
38
+ "load_config",
39
+ "register_domain",
40
+ "set_config",
41
+ ]
magmascript/cli.py ADDED
@@ -0,0 +1,628 @@
1
+ """CLI entry point for magmascript.
2
+
3
+ Usage:
4
+ magmascript mcp search "aphex twin"
5
+ magmascript mcp scores tetris
6
+ magmascript pi status
7
+ magmascript pi logs arcade-chat
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+
14
+ from magmascript.core.config import get_config
15
+ from magmascript.core.exceptions import MagmascriptError
16
+ from magmascript.core.output import format_output
17
+
18
+
19
+ def usage():
20
+ print("""magmascript — scripting toolkit with domain-first subcommands
21
+
22
+ Usage:
23
+ magmascript <domain> <action> [args...]
24
+
25
+ Domains:
26
+ mcp MagmaCrunch MCP server tools
27
+ pi Raspberry Pi management (direct SSH)
28
+ gh GitHub operations (direct API)
29
+ media Multi-provider media search
30
+ scores Game high scores (direct SSH)
31
+ cache Cache management (stats, clear)
32
+
33
+ MCP Actions:
34
+ search <query> Search cached MusicBrainz entities
35
+ entities [type] List cached entities (artists, places, etc.)
36
+ entity <type> <key> Get full entity data
37
+ scoreboards List all game leaderboards
38
+ scores <game> [limit] Get leaderboard for a game
39
+ games List all arcade games
40
+ archive List all archive pages
41
+ bots List GitHub Actions workflows
42
+ bot-status <name> Get workflow details
43
+ trigger <name> Trigger a workflow
44
+ bot-runs <name> [limit] Get workflow run history
45
+ discogs <query> [type] Search Discogs
46
+ jukebox List jukebox songs
47
+ tv List TV channels
48
+ themes List theme catalog
49
+ plays List Last.fm play counts
50
+ artist-plays <name> Get artist play counts
51
+
52
+ Pi Actions:
53
+ status Check all arcade service statuses
54
+ logs <service> [lines] Get service logs
55
+ logs-errors [lines] Get error logs from all services
56
+ logs-today Get today's logs
57
+ restart <service> Restart a service
58
+ restart-all Restart all arcade services
59
+ info System info (uptime, memory, temp)
60
+ traffic [lines] Nginx access log analysis
61
+ deploy <path> [service] Deploy to Pi via rsync
62
+ reboot Reboot the Pi
63
+ shutdown Power off the Pi
64
+
65
+ GitHub Actions:
66
+ workflows List all workflows with status
67
+ workflow <name> Recent runs for one workflow
68
+ trigger <name> Trigger a workflow
69
+ issues [label] [state] List issues
70
+ issue create <title> [body] Create an issue
71
+ issue close <number> Close an issue
72
+ file <path> Read a file from the repo
73
+ repo Repo info (test connection)
74
+
75
+ Media Search:
76
+ search <query> Search all providers
77
+ search <query> --source s Search specific provider
78
+ providers List available providers
79
+ image <id> --source <src> Get single result by ID
80
+
81
+ Scores:
82
+ list List all games with entry counts
83
+ get <game> [limit] Get leaderboard for a game (default top 20)
84
+ report Generate full markdown report
85
+
86
+ Cache:
87
+ stats Show cache statistics
88
+ clear [--domain <name>] Clear cache entries
89
+
90
+ Options:
91
+ --json Output as JSON
92
+ --table Output as table (default)
93
+ --help Show this help
94
+ """)
95
+ sys.exit(0)
96
+
97
+
98
+ def main():
99
+ args = sys.argv[1:]
100
+
101
+ if not args or args[0] in ("--help", "-h", "help"):
102
+ usage()
103
+
104
+ domain = args[0]
105
+ action = args[1] if len(args) > 1 else ""
106
+ rest = args[2:]
107
+
108
+ # Parse output format
109
+ fmt = "table"
110
+ if "--json" in rest:
111
+ fmt = "json"
112
+ rest.remove("--json")
113
+ elif "--table" in rest:
114
+ rest.remove("--table")
115
+
116
+ # Parse --no-cache flag
117
+ no_cache = False
118
+ if "--no-cache" in rest:
119
+ no_cache = True
120
+ rest.remove("--no-cache")
121
+
122
+ config = get_config()
123
+
124
+ try:
125
+ if domain == "mcp":
126
+ from magmascript.domains.mcp import MCPClient
127
+ client = MCPClient(config)
128
+ try:
129
+ _dispatch_mcp(action, rest, client, fmt)
130
+ finally:
131
+ client.close()
132
+
133
+ elif domain == "pi":
134
+ from magmascript.domains.pi import PIClient
135
+ client = PIClient(config)
136
+ try:
137
+ _dispatch_pi(action, rest, client, fmt)
138
+ finally:
139
+ client.close()
140
+
141
+ elif domain == "gh":
142
+ from magmascript.domains.gh import GHClient
143
+ client = GHClient(config)
144
+ try:
145
+ _dispatch_gh(action, rest, client, fmt)
146
+ finally:
147
+ client.close()
148
+
149
+ elif domain == "media":
150
+ from magmascript.domains.media import MediaClient
151
+ client = MediaClient(config)
152
+ try:
153
+ _dispatch_media(action, rest, client, fmt)
154
+ finally:
155
+ client.close()
156
+
157
+ elif domain == "scores":
158
+ from magmascript.domains.scores import ScoresClient
159
+ client = ScoresClient(config)
160
+ try:
161
+ _dispatch_scores(action, rest, client, fmt)
162
+ finally:
163
+ client.close()
164
+
165
+ elif domain == "cache":
166
+ _dispatch_cache(action, rest, fmt)
167
+
168
+ else:
169
+ print(f"Unknown domain: {domain!r}. Available: mcp, pi, gh, media, scores, cache", file=sys.stderr)
170
+ sys.exit(1)
171
+
172
+ except KeyboardInterrupt:
173
+ print("\nInterrupted.", file=sys.stderr)
174
+ sys.exit(130)
175
+ except MagmascriptError as e:
176
+ print(f"Error: {e}", file=sys.stderr)
177
+ sys.exit(1)
178
+ except ValueError as e:
179
+ print(f"Invalid input: {e}", file=sys.stderr)
180
+ sys.exit(1)
181
+
182
+
183
+ def _dispatch_mcp(action: str, args: list[str], client, fmt: str):
184
+ """Dispatch MCP subcommands."""
185
+ if not action or action == "--help":
186
+ usage()
187
+
188
+ if action == "search":
189
+ if not args:
190
+ print("Usage: mcp search <query>", file=sys.stderr)
191
+ sys.exit(1)
192
+ results = client.search(args[0])
193
+ print(format_output(results, fmt))
194
+
195
+ elif action == "entities":
196
+ entity_type = args[0] if args else ""
197
+ results = client.list_entities(entity_type)
198
+ print(format_output(results, fmt))
199
+
200
+ elif action == "entity":
201
+ if len(args) < 2:
202
+ print("Usage: mcp entity <type> <key>", file=sys.stderr)
203
+ sys.exit(1)
204
+ result = client.get_entity(args[0], args[1])
205
+ print(format_output(result, fmt))
206
+
207
+ elif action == "scoreboards":
208
+ results = client.scoreboards()
209
+ print(format_output(results, fmt))
210
+
211
+ elif action == "scores":
212
+ if not args:
213
+ print("Usage: mcp scores <game> [limit]", file=sys.stderr)
214
+ sys.exit(1)
215
+ limit = int(args[1]) if len(args) > 1 else 10
216
+ results = client.scores(args[0], limit)
217
+ print(format_output(results, fmt))
218
+
219
+ elif action == "games":
220
+ results = client.arcade_games()
221
+ print(format_output(results, fmt))
222
+
223
+ elif action == "archive":
224
+ results = client.archive_pages()
225
+ print(format_output(results, fmt))
226
+
227
+ elif action == "bots":
228
+ results = client.bots()
229
+ print(format_output(results, fmt))
230
+
231
+ elif action == "bot-status":
232
+ if not args:
233
+ print("Usage: mcp bot-status <name>", file=sys.stderr)
234
+ sys.exit(1)
235
+ result = client.bot_status(args[0])
236
+ print(result)
237
+
238
+ elif action == "trigger":
239
+ if not args:
240
+ print("Usage: mcp trigger <name>", file=sys.stderr)
241
+ sys.exit(1)
242
+ result = client.trigger_bot(args[0])
243
+ print(result)
244
+
245
+ elif action == "bot-runs":
246
+ if not args:
247
+ print("Usage: mcp bot-runs <name> [limit]", file=sys.stderr)
248
+ sys.exit(1)
249
+ limit = int(args[1]) if len(args) > 1 else 10
250
+ result = client.bot_runs(args[0], limit)
251
+ print(result)
252
+
253
+ elif action == "discogs":
254
+ if not args:
255
+ print("Usage: mcp discogs <query> [type]", file=sys.stderr)
256
+ sys.exit(1)
257
+ search_type = args[1] if len(args) > 1 else "release"
258
+ results = client.discogs_search(args[0], search_type)
259
+ print(format_output(results, fmt))
260
+
261
+ elif action == "jukebox":
262
+ result = client.jukebox_songs()
263
+ print(result)
264
+
265
+ elif action == "tv":
266
+ result = client.tv_channels()
267
+ print(result)
268
+
269
+ elif action == "themes":
270
+ result = client.themes()
271
+ print(result)
272
+
273
+ elif action == "plays":
274
+ results = client.play_counts()
275
+ print(format_output(results, fmt))
276
+
277
+ elif action == "artist-plays":
278
+ if not args:
279
+ print("Usage: mcp artist-plays <name>", file=sys.stderr)
280
+ sys.exit(1)
281
+ result = client.artist_play_counts(args[0])
282
+ print(result)
283
+
284
+ else:
285
+ print(f"Unknown MCP action: {action!r}", file=sys.stderr)
286
+ print("Run 'magmascript mcp --help' for available actions.", file=sys.stderr)
287
+ sys.exit(1)
288
+
289
+
290
+ def _dispatch_pi(action: str, args: list[str], client, fmt: str):
291
+ """Dispatch Pi subcommands."""
292
+ if not action or action == "--help":
293
+ usage()
294
+
295
+ if action == "status":
296
+ results = client.services()
297
+ print(format_output(results, fmt))
298
+
299
+ elif action == "logs":
300
+ if not args:
301
+ print("Usage: pi logs <service> [lines]", file=sys.stderr)
302
+ sys.exit(1)
303
+ lines = int(args[1]) if len(args) > 1 else 50
304
+ result = client.logs(args[0], lines)
305
+ print(result)
306
+
307
+ elif action == "logs-errors":
308
+ lines = int(args[0]) if args else 100
309
+ result = client.logs_errors(lines)
310
+ print(result)
311
+
312
+ elif action == "logs-today":
313
+ result = client.logs_today()
314
+ print(result)
315
+
316
+ elif action == "restart":
317
+ if not args:
318
+ print("Usage: pi restart <service>", file=sys.stderr)
319
+ sys.exit(1)
320
+ result = client.restart(args[0])
321
+ print(result)
322
+
323
+ elif action == "restart-all":
324
+ result = client.restart_all()
325
+ print(result)
326
+
327
+ elif action == "info":
328
+ result = client.info()
329
+ print(format_output(result, fmt))
330
+
331
+ elif action == "traffic":
332
+ lines = int(args[0]) if args else 1000
333
+ result = client.traffic(lines)
334
+ print(f"=== Top IPs ===\n{result.top_ips}")
335
+ print(f"\n=== Status Codes ===\n{result.status_codes}")
336
+ print(f"\n=== User Agents ===\n{result.user_agents}")
337
+ print(f"\n=== Total Requests ===\n{result.total_requests}")
338
+
339
+ elif action == "deploy":
340
+ if not args:
341
+ print("Usage: pi deploy <path> [service]", file=sys.stderr)
342
+ sys.exit(1)
343
+ service = args[1] if len(args) > 1 else ""
344
+ result = client.deploy(args[0], service)
345
+ print(result)
346
+
347
+ elif action == "reboot":
348
+ result = client.reboot()
349
+ print(result)
350
+
351
+ elif action == "shutdown":
352
+ result = client.shutdown()
353
+ print(result)
354
+
355
+ else:
356
+ print(f"Unknown Pi action: {action!r}", file=sys.stderr)
357
+ print("Run 'magmascript pi --help' for available actions.", file=sys.stderr)
358
+ sys.exit(1)
359
+
360
+
361
+ def _dispatch_gh(action: str, args: list[str], client, fmt: str):
362
+ """Dispatch GitHub subcommands."""
363
+ if not action or action == "--help":
364
+ usage()
365
+
366
+ if action == "workflows":
367
+ results = client.workflows()
368
+ print(format_output(results, fmt))
369
+
370
+ elif action == "workflow":
371
+ if not args:
372
+ print("Usage: gh workflow <name>", file=sys.stderr)
373
+ sys.exit(1)
374
+ limit = int(args[1]) if len(args) > 1 else 10
375
+ results = client.workflow_runs(args[0], limit)
376
+ print(format_output(results, fmt))
377
+
378
+ elif action == "trigger":
379
+ if not args:
380
+ print("Usage: gh trigger <name>", file=sys.stderr)
381
+ sys.exit(1)
382
+ result = client.trigger(args[0])
383
+ print(result)
384
+
385
+ elif action == "issues":
386
+ labels = args[0] if args else ""
387
+ state = args[1] if len(args) > 1 else "open"
388
+ results = client.issues(labels=labels, state=state)
389
+ print(format_output(results, fmt))
390
+
391
+ elif action == "issue":
392
+ if not args:
393
+ print("Usage: gh issue create <title> [body] | gh issue close <number>", file=sys.stderr)
394
+ sys.exit(1)
395
+ sub = args[0]
396
+ if sub == "create":
397
+ if len(args) < 2:
398
+ print("Usage: gh issue create <title> [body]", file=sys.stderr)
399
+ sys.exit(1)
400
+ title = args[1]
401
+ body = args[2] if len(args) > 2 else ""
402
+ result = client.create_issue(title, body)
403
+ print(format_output(result, fmt))
404
+ elif sub == "close":
405
+ if len(args) < 2:
406
+ print("Usage: gh issue close <number>", file=sys.stderr)
407
+ sys.exit(1)
408
+ result = client.close_issue(int(args[1]))
409
+ print(result)
410
+ else:
411
+ print(f"Unknown issue action: {sub!r}. Use 'create' or 'close'.", file=sys.stderr)
412
+ sys.exit(1)
413
+
414
+ elif action == "file":
415
+ if not args:
416
+ print("Usage: gh file <path>", file=sys.stderr)
417
+ sys.exit(1)
418
+ content, sha = client.get_file(args[0])
419
+ print(content)
420
+
421
+ elif action == "repo":
422
+ info = client.repo_info()
423
+ print(f" Name: {info.get('full_name', '?')}")
424
+ print(f" Private: {info.get('private', '?')}")
425
+ print(f" Default branch: {info.get('default_branch', '?')}")
426
+ print(f" Stars: {info.get('stargazers_count', '?')}")
427
+ print(f" Forks: {info.get('forks_count', '?')}")
428
+
429
+ else:
430
+ print(f"Unknown GitHub action: {action!r}", file=sys.stderr)
431
+ print("Run 'magmascript gh --help' for available actions.", file=sys.stderr)
432
+ sys.exit(1)
433
+
434
+
435
+ def _dispatch_media(action: str, args: list[str], client, fmt: str):
436
+ """Dispatch media search subcommands."""
437
+ if not action or action == "--help":
438
+ usage()
439
+
440
+ if action == "search":
441
+ if not args:
442
+ print("Usage: media search <query> [--source <provider>]", file=sys.stderr)
443
+ sys.exit(1)
444
+
445
+ query = args[0]
446
+ source = ""
447
+ media_type = ""
448
+ orientation = ""
449
+ page = 1
450
+ per_page = 24
451
+
452
+ i = 1
453
+ while i < len(args):
454
+ if args[i] == "--source" and i + 1 < len(args):
455
+ source = args[i + 1]
456
+ i += 2
457
+ elif args[i] == "--type" and i + 1 < len(args):
458
+ media_type = args[i + 1]
459
+ i += 2
460
+ elif args[i] == "--orientation" and i + 1 < len(args):
461
+ orientation = args[i + 1]
462
+ i += 2
463
+ elif args[i] == "--page" and i + 1 < len(args):
464
+ page = int(args[i + 1])
465
+ i += 2
466
+ elif args[i] == "--per-page" and i + 1 < len(args):
467
+ per_page = int(args[i + 1])
468
+ i += 2
469
+ else:
470
+ i += 1
471
+
472
+ result = client.search(
473
+ query, source=source, media_type=media_type,
474
+ orientation=orientation, page=page, per_page=per_page,
475
+ )
476
+
477
+ parts = [f"{len(result.results)} results"]
478
+ if result.provider_totals:
479
+ detail = ", ".join(f"{k}: {v}" for k, v in result.provider_totals.items())
480
+ parts.append(f"({detail})")
481
+ print(" ".join(parts))
482
+
483
+ if result.errors:
484
+ for provider, error in result.errors.items():
485
+ print(f" ⚠ {provider}: {error}", file=sys.stderr)
486
+
487
+ print()
488
+ print(format_output(result.results, fmt))
489
+
490
+ elif action == "providers":
491
+ providers = client.list_providers()
492
+ for p in providers:
493
+ key_marker = " (needs key)" if p.needs_key else ""
494
+ types_str = ", ".join(p.types)
495
+ print(f" {p.key:<15} {p.label:<15} [{types_str}]{key_marker}")
496
+
497
+ elif action == "image":
498
+ if len(args) < 2:
499
+ print("Usage: media image <id> --source <provider>", file=sys.stderr)
500
+ sys.exit(1)
501
+ result_id = args[0]
502
+ source = ""
503
+ i = 1
504
+ while i < len(args):
505
+ if args[i] == "--source" and i + 1 < len(args):
506
+ source = args[i + 1]
507
+ i += 2
508
+ else:
509
+ i += 1
510
+ if not source:
511
+ print("Usage: media image <id> --source <provider>", file=sys.stderr)
512
+ sys.exit(1)
513
+ result = client.get(result_id, source)
514
+ if result:
515
+ print(format_output(result, fmt))
516
+ else:
517
+ print(f"Not found: {result_id} from {source}", file=sys.stderr)
518
+ sys.exit(1)
519
+
520
+ else:
521
+ print(f"Unknown media action: {action!r}", file=sys.stderr)
522
+ print("Run 'magmascript media --help' for available actions.", file=sys.stderr)
523
+ sys.exit(1)
524
+
525
+
526
+ def _dispatch_scores(action: str, args: list[str], client, fmt: str):
527
+ """Dispatch Scores subcommands."""
528
+ if not action or action == "--help":
529
+ usage()
530
+
531
+ if action == "list":
532
+ results = client.list_scoreboards()
533
+ print(format_output(results, fmt))
534
+
535
+ elif action == "get":
536
+ if not args:
537
+ print("Usage: scores get <game> [limit]", file=sys.stderr)
538
+ sys.exit(1)
539
+ limit = int(args[1]) if len(args) > 1 else 20
540
+ results = client.get_scores(args[0], limit)
541
+ print(format_output(results, fmt))
542
+
543
+ elif action == "report":
544
+ report = client.report()
545
+ lines = [f"# Weekly High Scores — {report.generated_at}", ""]
546
+ lines.append("## Leaderboards")
547
+ lines.append("")
548
+ for board in report.scoreboards:
549
+ lines.append(f"### {board.game}")
550
+ lines.append("")
551
+ lines.append("| Rank | Player | Score |")
552
+ lines.append("|------|--------|-------|")
553
+ entries = client.get_scores(board.game_id, limit=5)
554
+ for e in entries:
555
+ parts = [str(e.score)]
556
+ if e.level:
557
+ parts.append(f"L{e.level}")
558
+ if e.difficulty:
559
+ parts.append(f"D{e.difficulty}")
560
+ if e.time:
561
+ parts.append(e.time)
562
+ if e.moves:
563
+ parts.append(f"{e.moves} moves")
564
+ if e.won is False:
565
+ parts.append("lost")
566
+ lines.append(f"| {e.rank} | {e.initials} | {' · '.join(parts)} |")
567
+ if board.entries == 0:
568
+ lines.append("| - | No scores yet | - |")
569
+ lines.append("")
570
+
571
+ lines.append("## Stats")
572
+ lines.append("")
573
+ lines.append(f"- **Games tracked**: {report.total_games}")
574
+ lines.append(f"- **Total scores**: {report.total_scores}")
575
+ if report.player_stats:
576
+ top = report.player_stats[0]
577
+ game_word = "game" if top.games_played == 1 else "games"
578
+ lines.append(f"- **Most active player**: {top.name} ({top.total_entries} scores across {top.games_played} {game_word})")
579
+ names = ", ".join(p.name for p in report.player_stats)
580
+ lines.append(f"- **Players**: {names}")
581
+ print("\n".join(lines))
582
+
583
+ else:
584
+ print(f"Unknown scores action: {action!r}", file=sys.stderr)
585
+ print("Run 'magmascript scores --help' for available actions.", file=sys.stderr)
586
+ sys.exit(1)
587
+
588
+
589
+ def _dispatch_cache(action: str, args: list[str], fmt: str):
590
+ """Dispatch cache subcommands."""
591
+ from magmascript.core.cache import get_cache
592
+
593
+ cache = get_cache()
594
+
595
+ if not action or action == "--help":
596
+ print("""Cache management.
597
+
598
+ Usage:
599
+ magmascript cache stats Show cache statistics
600
+ magmascript cache clear Clear all cache
601
+ magmascript cache clear --domain m Clear specific domain (media/scores/gh)
602
+ """)
603
+ sys.exit(0)
604
+
605
+ if action == "stats":
606
+ data = cache.file_stats()
607
+ print(format_output(data, fmt))
608
+
609
+ elif action == "clear":
610
+ domain = None
611
+ if "--domain" in args:
612
+ idx = args.index("--domain")
613
+ if idx + 1 < len(args):
614
+ domain = args[idx + 1]
615
+ else:
616
+ print("Usage: cache clear --domain <name>", file=sys.stderr)
617
+ sys.exit(1)
618
+ count = cache.clear(domain=domain)
619
+ label = f" domain '{domain}'" if domain else ""
620
+ print(f"Cleared {count} cache entries{label}.")
621
+
622
+ else:
623
+ print(f"Unknown cache action: {action!r}", file=sys.stderr)
624
+ sys.exit(1)
625
+
626
+
627
+ if __name__ == "__main__":
628
+ main()
@@ -0,0 +1,40 @@
1
+ """Core framework for magmascript."""
2
+
3
+ from magmascript.core.cache import CacheStats, CacheStore, get_cache
4
+ from magmascript.core.config import Config, get_config, load_config, set_config
5
+ from magmascript.core.exceptions import (
6
+ APIError,
7
+ AuthError,
8
+ ConfigError,
9
+ MagmascriptError,
10
+ MCPError,
11
+ ProviderError,
12
+ RateLimitError,
13
+ SSHError,
14
+ )
15
+ from magmascript.core.registry import get_domain, list_domains, register_domain
16
+ from magmascript.core.rpc import RPCClient, RPCError, RPCResponse
17
+
18
+ __all__ = [
19
+ "APIError",
20
+ "AuthError",
21
+ "CacheStats",
22
+ "CacheStore",
23
+ "Config",
24
+ "ConfigError",
25
+ "MagmascriptError",
26
+ "MCPError",
27
+ "ProviderError",
28
+ "RPCClient",
29
+ "RPCError",
30
+ "RPCResponse",
31
+ "RateLimitError",
32
+ "SSHError",
33
+ "get_cache",
34
+ "get_config",
35
+ "get_domain",
36
+ "list_domains",
37
+ "load_config",
38
+ "register_domain",
39
+ "set_config",
40
+ ]