loki-mode 6.75.2 → 6.76.0

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 (75) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/loki +672 -0
  4. package/autonomy/run.sh +1 -1
  5. package/dashboard/__init__.py +1 -1
  6. package/docs/INSTALLATION.md +1 -1
  7. package/mcp/__init__.py +1 -1
  8. package/mcp/magic_tools.py +471 -0
  9. package/mcp/server.py +13 -0
  10. package/package.json +1 -1
  11. package/references/magic-modules-patterns.md +634 -0
  12. package/skills/00-index.md +1 -0
  13. package/skills/magic-modules.md +205 -0
  14. package/web-app/dist/assets/{AdminPage-D4QSV6Zi.js → AdminPage-DwVUK4v9.js} +1 -1
  15. package/web-app/dist/assets/{Avatar-88MlpLO5.js → Avatar-B7gqhcg3.js} +1 -1
  16. package/web-app/dist/assets/{Badge-DbGjLr4i.js → Badge-DA3xNJAS.js} +1 -1
  17. package/web-app/dist/assets/{Button-sp_FVGZj.js → Button-BPXURLaK.js} +1 -1
  18. package/web-app/dist/assets/{ComparePage-p2ENnfa7.js → ComparePage-B0JQMhKG.js} +1 -1
  19. package/web-app/dist/assets/GitHubIssuesPanel-D38-fy29.js +12 -0
  20. package/web-app/dist/assets/{GitHubPRsPanel-Bi_yrcAE.js → GitHubPRsPanel-DLPcW3N0.js} +2 -2
  21. package/web-app/dist/assets/{HomePage-BB83YPiX.js → HomePage-CzeoS2V_.js} +3 -3
  22. package/web-app/dist/assets/{LoginPage-BXUudCJ9.js → LoginPage-DqCzxsfx.js} +1 -1
  23. package/web-app/dist/assets/MagicPage-CBLqpa55.js +31 -0
  24. package/web-app/dist/assets/{MetricsPage-CX0Ahy-_.js → MetricsPage-CPYQR0zr.js} +1 -1
  25. package/web-app/dist/assets/{NotFoundPage-C4JqatEk.js → NotFoundPage-B62u4iCs.js} +1 -1
  26. package/web-app/dist/assets/{ProjectPage-t5J2XAJT.js → ProjectPage-DNujSl6j.js} +67 -72
  27. package/web-app/dist/assets/{ProjectsPage-Bzpz1clk.js → ProjectsPage-uHG7kxB-.js} +1 -1
  28. package/web-app/dist/assets/{SettingsPage-y_yl8FvH.js → SettingsPage-BaQJbOgL.js} +1 -1
  29. package/web-app/dist/assets/{ShowcasePage-B7d6pzMq.js → ShowcasePage-DQR_e-kg.js} +1 -1
  30. package/web-app/dist/assets/{SystemSettingsPage-C4tR33KU.js → SystemSettingsPage-C_Q_1WK4.js} +1 -1
  31. package/web-app/dist/assets/{TeamsPage-DIOCfZIP.js → TeamsPage-DOFErDqX.js} +1 -1
  32. package/web-app/dist/assets/{TemplatesPage-DlKyapXX.js → TemplatesPage-Ty72hILN.js} +1 -1
  33. package/web-app/dist/assets/{TerminalOutput-Czg-ZC2k.js → TerminalOutput-DqOVnR1p.js} +7 -12
  34. package/web-app/dist/assets/{activity-h1wU9a0L.js → activity-BgBZ4s4c.js} +1 -1
  35. package/web-app/dist/assets/{bell-Bu8lsWOp.js → bell-C-UezVWi.js} +1 -1
  36. package/web-app/dist/assets/{bot-rWO7KjkQ.js → bot-D70fEnm5.js} +1 -1
  37. package/web-app/dist/assets/{check-BWp8L5Cy.js → check-CBohulxQ.js} +1 -1
  38. package/web-app/dist/assets/{chevron-left-Bw4I1yGm.js → chevron-left-C-emzUhB.js} +1 -1
  39. package/web-app/dist/assets/{circle-alert-C37PKXiC.js → circle-alert-8SRY0_GX.js} +1 -1
  40. package/web-app/dist/assets/{clock-DDScLol4.js → clock-mfq4XnPQ.js} +1 -1
  41. package/web-app/dist/assets/{cloud-DaYKPLaM.js → cloud-DpRM7T8t.js} +1 -1
  42. package/web-app/dist/assets/code-xml-1N2Ui-4c.js +6 -0
  43. package/web-app/dist/assets/{copy-DKIRv0VK.js → copy-LXquTgzI.js} +1 -1
  44. package/web-app/dist/assets/{database-CYZBHz51.js → database-S1dyXnuT.js} +1 -1
  45. package/web-app/dist/assets/{dollar-sign-CydJu0kl.js → dollar-sign-CRqk0dW5.js} +1 -1
  46. package/web-app/dist/assets/{file-code-corner-DqZ9gpdv.js → file-code-corner-B99CwY_6.js} +1 -1
  47. package/web-app/dist/assets/{file-plus-CzeFJWp3.js → file-plus-DZ5qnz5b.js} +1 -1
  48. package/web-app/dist/assets/{folder-open-4YWk08dP.js → folder-open-DBCm7yuF.js} +1 -1
  49. package/web-app/dist/assets/{git-commit-horizontal-wbqFPNID.js → git-commit-horizontal-DM1ERuNd.js} +1 -1
  50. package/web-app/dist/assets/{globe-Cby-g5Yb.js → globe-B7xEJSL_.js} +1 -1
  51. package/web-app/dist/assets/{hammer-BNScgGdp.js → hammer-Cgi3LTuS.js} +1 -1
  52. package/web-app/dist/assets/{index-6Z4B0I6r.js → index-BN52-GQT.js} +22 -17
  53. package/web-app/dist/assets/index-BfZSDej1.css +1 -0
  54. package/web-app/dist/assets/{layers-XfssQc5V.js → layers-Bi8RPIBC.js} +1 -1
  55. package/web-app/dist/assets/{lightbulb-EhnzRw7M.js → lightbulb-Doc_n8JX.js} +1 -1
  56. package/web-app/dist/assets/{loader-circle-BA0QIVGA.js → loader-circle-BB932A7A.js} +1 -1
  57. package/web-app/dist/assets/{lock-BABtHe6K.js → lock-Bt6gpMrs.js} +1 -1
  58. package/web-app/dist/assets/{mail-Dokiey5S.js → mail-BuzAu1IP.js} +1 -1
  59. package/web-app/dist/assets/{package-DbJyS1Ft.js → package-BE5FHxQ8.js} +1 -1
  60. package/web-app/dist/assets/{plus-BcAN8Kaj.js → plus-CNqABexN.js} +1 -1
  61. package/web-app/dist/assets/{refresh-cw-B3dG1-Sb.js → refresh-cw-34B13ztx.js} +1 -1
  62. package/web-app/dist/assets/{rotate-ccw-Cs1Phctm.js → rotate-ccw-CrD2QB29.js} +1 -1
  63. package/web-app/dist/assets/{save-DsrNCZrP.js → save-DsJcqdnI.js} +1 -1
  64. package/web-app/dist/assets/{server-CpN2GX4G.js → server-BcgRMArA.js} +1 -1
  65. package/web-app/dist/assets/{shield-alert-CKJ1pzCz.js → shield-alert-DLYLdVJ0.js} +1 -1
  66. package/web-app/dist/assets/{trash-2-C9vZqTqw.js → trash-2-Cc-VTvzt.js} +1 -1
  67. package/web-app/dist/assets/{trending-down-BNLTrF5P.js → trending-down-CrDpO2a_.js} +1 -1
  68. package/web-app/dist/assets/{trending-up-DmFIdVOc.js → trending-up-CNVsmM3G.js} +1 -1
  69. package/web-app/dist/assets/upload-LuDuB7Wc.js +6 -0
  70. package/web-app/dist/assets/{usePolling-vUlY-o6P.js → usePolling-C8rvc-CG.js} +1 -1
  71. package/web-app/dist/assets/{user-Dh00W8De.js → user-BT79cI-o.js} +1 -1
  72. package/web-app/dist/index.html +2 -2
  73. package/web-app/server.py +120 -0
  74. package/web-app/dist/assets/GitHubIssuesPanel-DBbBTG9w.js +0 -17
  75. package/web-app/dist/assets/index-CVM4A1Fw.css +0 -1
package/autonomy/loki CHANGED
@@ -450,6 +450,7 @@ show_help() {
450
450
  echo " onboard [path] Analyze a repo and generate CLAUDE.md (structure, conventions, commands)"
451
451
  echo ' explain [path] Analyze any codebase and explain its architecture in plain English'
452
452
  echo " docs [cmd] Generate, update, check project documentation"
453
+ echo " magic [cmd] Spec-driven component generation (MagicModules + MoMoA)"
453
454
  echo " plan <PRD> Dry-run PRD analysis: complexity, cost, and execution plan"
454
455
  echo " ci [opts] CI/CD quality gate integration (--pr, --report, --github-comment)"
455
456
  echo " test [opts] AI-powered test generation (--file, --dir, --changed, --dry-run)"
@@ -540,6 +541,7 @@ cmd_start() {
540
541
  echo " --complex Force complex complexity tier (8 phases)"
541
542
  echo " --github Enable GitHub issue import"
542
543
  echo " --no-dashboard Disable web dashboard"
544
+ echo " --api Start dashboard API server alongside the build"
543
545
  echo " --sandbox Run in Docker sandbox"
544
546
  echo " --skip-memory Skip loading memory context at startup"
545
547
  echo " --compliance PRESET Enable compliance mode (default|healthcare|fintech|government)"
@@ -654,6 +656,10 @@ cmd_start() {
654
656
  export LOKI_DASHBOARD=false
655
657
  shift
656
658
  ;;
659
+ --api)
660
+ export LOKI_START_API=true
661
+ shift
662
+ ;;
657
663
  --sandbox)
658
664
  export LOKI_SANDBOX_MODE=true
659
665
  shift
@@ -1112,6 +1118,14 @@ cmd_start() {
1112
1118
  exit 1
1113
1119
  fi
1114
1120
 
1121
+ # --api flag: start the dashboard API server in background before the build
1122
+ if [ "${LOKI_START_API:-false}" = "true" ]; then
1123
+ local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
1124
+ echo -e "${GREEN}Starting dashboard API on port $dash_port...${NC}"
1125
+ cmd_dashboard_start 2>/dev/null &
1126
+ sleep 2
1127
+ fi
1128
+
1115
1129
  exec "$RUN_SH" "${args[@]}"
1116
1130
  }
1117
1131
 
@@ -11199,6 +11213,9 @@ main() {
11199
11213
  docs)
11200
11214
  cmd_docs "$@"
11201
11215
  ;;
11216
+ magic)
11217
+ cmd_magic "$@"
11218
+ ;;
11202
11219
  ci)
11203
11220
  cmd_ci "$@"
11204
11221
  ;;
@@ -19317,6 +19334,661 @@ _docs_human_size() {
19317
19334
  fi
19318
19335
  }
19319
19336
 
19337
+ #===============================================================================
19338
+ # Magic: Spec-driven component generation (MagicModules + MoMoA)
19339
+ #
19340
+ # Storage layout:
19341
+ # .loki/magic/specs/<name>.md - Canonical component specs
19342
+ # .loki/magic/generated/react/<name>.tsx - React output
19343
+ # .loki/magic/generated/webcomponent/<name>.js - Web Component output
19344
+ # .loki/magic/generated/tests/<name>.test.tsx - Generated tests
19345
+ # .loki/magic/registry.json - Component registry
19346
+ #===============================================================================
19347
+
19348
+ _magic_help() {
19349
+ echo -e "${BOLD}loki magic${NC} - Spec-driven component generation"
19350
+ echo ""
19351
+ echo "Usage: loki magic <command> [options]"
19352
+ echo ""
19353
+ echo "Commands:"
19354
+ echo " generate <name> Generate a component from a spec or screenshot"
19355
+ echo " update Regenerate components from their specs"
19356
+ echo " list List registered components"
19357
+ echo " remove <name> Remove a component and its artifacts"
19358
+ echo " registry <op> Registry operations (show|prune|stats)"
19359
+ echo " debate <name> Run multi-persona debate on an existing component"
19360
+ echo " help Show this help"
19361
+ echo ""
19362
+ echo "Options for 'generate':"
19363
+ echo " --from-spec PATH Generate from existing spec file"
19364
+ echo " --from-screenshot PATH Generate from a screenshot (vision-based)"
19365
+ echo " --target TARGET Output target: react|webcomponent|both (default: react)"
19366
+ echo " --placement PATH Where to place final component in project"
19367
+ echo ""
19368
+ echo "Options for 'update':"
19369
+ echo " --name NAME Update a specific component (default: all)"
19370
+ echo " --force Overwrite local edits if any"
19371
+ echo ""
19372
+ echo "Options for 'list':"
19373
+ echo " --format FMT Output format: json|table (default: table)"
19374
+ echo ""
19375
+ echo "Options for 'remove':"
19376
+ echo " --force Skip confirmation"
19377
+ echo ""
19378
+ echo "Options for 'debate':"
19379
+ echo " --rounds N Number of debate rounds (default: 3)"
19380
+ echo ""
19381
+ echo "Examples:"
19382
+ echo " loki magic generate LoginForm --from-spec specs/login.md"
19383
+ echo " loki magic generate Navbar --from-screenshot shots/nav.png --target both"
19384
+ echo " loki magic update --name LoginForm"
19385
+ echo " loki magic list --format json"
19386
+ echo " loki magic registry stats"
19387
+ echo " loki magic debate LoginForm --rounds 5"
19388
+ }
19389
+
19390
+ _magic_ensure_dirs() {
19391
+ mkdir -p ".loki/magic/specs" \
19392
+ ".loki/magic/generated/react" \
19393
+ ".loki/magic/generated/webcomponent" \
19394
+ ".loki/magic/generated/tests" 2>/dev/null || true
19395
+ if [ ! -f ".loki/magic/registry.json" ]; then
19396
+ # Schema matches magic/registry/schema.json: components as array
19397
+ echo '{"version": "1", "updated_at": "", "components": []}' > ".loki/magic/registry.json"
19398
+ fi
19399
+ }
19400
+
19401
+ _magic_python() {
19402
+ # Prefer python3.12 if present (matches the rest of the project), fall back to python3
19403
+ if [ -x "/opt/homebrew/bin/python3.12" ]; then
19404
+ echo "/opt/homebrew/bin/python3.12"
19405
+ elif command -v python3.12 &>/dev/null; then
19406
+ command -v python3.12
19407
+ else
19408
+ echo "python3"
19409
+ fi
19410
+ }
19411
+
19412
+ # Export PYTHONPATH so magic/* modules shipped with Loki are importable
19413
+ # no matter which project directory the user runs from.
19414
+ _magic_pypath() {
19415
+ local sk="${SKILL_DIR:-}"
19416
+ if [ -z "$sk" ]; then
19417
+ sk="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")"
19418
+ fi
19419
+ # Preserve any existing PYTHONPATH
19420
+ if [ -n "${PYTHONPATH:-}" ]; then
19421
+ echo "${sk}:${PYTHONPATH}"
19422
+ else
19423
+ echo "$sk"
19424
+ fi
19425
+ }
19426
+
19427
+ _magic_valid_name() {
19428
+ local name="$1"
19429
+ [[ "$name" =~ ^[A-Za-z][A-Za-z0-9_-]*$ ]]
19430
+ }
19431
+
19432
+ _magic_generate() {
19433
+ local name=""
19434
+ local from_spec=""
19435
+ local from_screenshot=""
19436
+ local target="react"
19437
+ local placement=""
19438
+ local description=""
19439
+ local tags=""
19440
+
19441
+ while [[ $# -gt 0 ]]; do
19442
+ case "$1" in
19443
+ --from-spec)
19444
+ from_spec="${2:-}"
19445
+ shift 2
19446
+ ;;
19447
+ --from-screenshot)
19448
+ from_screenshot="${2:-}"
19449
+ shift 2
19450
+ ;;
19451
+ --target)
19452
+ target="${2:-}"
19453
+ shift 2
19454
+ ;;
19455
+ --placement)
19456
+ placement="${2:-}"
19457
+ shift 2
19458
+ ;;
19459
+ --description|--desc)
19460
+ description="${2:-}"
19461
+ shift 2
19462
+ ;;
19463
+ --tags)
19464
+ tags="${2:-}"
19465
+ shift 2
19466
+ ;;
19467
+ --help|-h)
19468
+ _magic_help
19469
+ return 0
19470
+ ;;
19471
+ -*)
19472
+ log_error "Unknown option: $1"
19473
+ return 1
19474
+ ;;
19475
+ *)
19476
+ if [ -z "$name" ]; then
19477
+ name="$1"
19478
+ fi
19479
+ shift
19480
+ ;;
19481
+ esac
19482
+ done
19483
+
19484
+ if [ -z "$name" ]; then
19485
+ log_error "Component name is required"
19486
+ echo "Usage: loki magic generate <name> [--from-spec PATH] [--from-screenshot PATH] [--target react|webcomponent|both] [--placement PATH]"
19487
+ return 1
19488
+ fi
19489
+
19490
+ if ! _magic_valid_name "$name"; then
19491
+ log_error "Invalid component name: '$name' (must start with a letter, contain only letters, digits, _ or -)"
19492
+ return 1
19493
+ fi
19494
+
19495
+ case "$target" in
19496
+ react|webcomponent|both) ;;
19497
+ *)
19498
+ log_error "Invalid target: '$target' (must be react, webcomponent, or both)"
19499
+ return 1
19500
+ ;;
19501
+ esac
19502
+
19503
+ if [ -n "$from_spec" ] && [ ! -f "$from_spec" ]; then
19504
+ log_error "Spec file not found: $from_spec"
19505
+ return 1
19506
+ fi
19507
+ if [ -n "$from_screenshot" ] && [ ! -f "$from_screenshot" ]; then
19508
+ log_error "Screenshot file not found: $from_screenshot"
19509
+ return 1
19510
+ fi
19511
+
19512
+ _magic_ensure_dirs
19513
+ local py
19514
+ py=$(_magic_python)
19515
+
19516
+ log_info "Generating component '$name' (target: $target)"
19517
+
19518
+ # 1. Build/load the canonical spec
19519
+ local spec_path=".loki/magic/specs/${name}.md"
19520
+ if [ -n "$from_spec" ]; then
19521
+ cp "$from_spec" "$spec_path"
19522
+ log_info "Copied spec from $from_spec"
19523
+ else
19524
+ log_info "Synthesizing spec via magic.core.spec"
19525
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19526
+ import sys
19527
+ try:
19528
+ from magic.core.spec import generate_spec
19529
+ except Exception as exc:
19530
+ sys.stderr.write(f'[ERROR] magic.core.spec not available: {exc}\n')
19531
+ sys.exit(2)
19532
+ generate_spec(
19533
+ name='$name',
19534
+ out_path='$spec_path',
19535
+ from_spec='${from_spec}' or None,
19536
+ from_screenshot='${from_screenshot}' or None,
19537
+ )
19538
+ " || {
19539
+ log_error "Spec generation failed (module magic.core.spec missing or errored)"
19540
+ return 1
19541
+ }
19542
+ fi
19543
+
19544
+ # 2. Load design tokens (optional; module may not yet exist)
19545
+ log_info "Loading design tokens"
19546
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19547
+ try:
19548
+ from magic.core.design_tokens import load_tokens
19549
+ load_tokens()
19550
+ except Exception as exc:
19551
+ import sys
19552
+ sys.stderr.write(f'[WARN] design tokens unavailable: {exc}\n')
19553
+ " 2>&1 | grep -v '^$' || true
19554
+
19555
+ # 3. Generate component(s)
19556
+ log_info "Generating component via magic.core.generator"
19557
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19558
+ import sys
19559
+ try:
19560
+ from magic.core.generator import generate_component
19561
+ except Exception as exc:
19562
+ sys.stderr.write(f'[ERROR] magic.core.generator not available: {exc}\n')
19563
+ sys.exit(2)
19564
+ generate_component(
19565
+ name='$name',
19566
+ spec_path='$spec_path',
19567
+ target='$target',
19568
+ react_out='.loki/magic/generated/react/${name}.tsx',
19569
+ wc_out='.loki/magic/generated/webcomponent/${name}.js',
19570
+ test_out='.loki/magic/generated/tests/${name}.test.tsx',
19571
+ placement='${placement}' or None,
19572
+ )
19573
+ " || {
19574
+ log_error "Component generation failed"
19575
+ return 1
19576
+ }
19577
+
19578
+ # 4. Register in registry
19579
+ log_info "Registering component"
19580
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19581
+ import sys
19582
+ try:
19583
+ from magic.core.registry import register_component
19584
+ except Exception as exc:
19585
+ sys.stderr.write(f'[ERROR] magic.core.registry not available: {exc}\n')
19586
+ sys.exit(2)
19587
+ register_component(
19588
+ registry_path='.loki/magic/registry.json',
19589
+ name='$name',
19590
+ spec_path='$spec_path',
19591
+ target='$target',
19592
+ react_path='.loki/magic/generated/react/${name}.tsx' if '$target' in ('react','both') else '',
19593
+ webcomponent_path='.loki/magic/generated/webcomponent/${name}.js' if '$target' in ('webcomponent','both') else '',
19594
+ test_path='.loki/magic/generated/tests/${name}.test.tsx',
19595
+ description='''$description'''.strip(),
19596
+ tags=[t.strip() for t in '''$tags'''.split(',') if t.strip()],
19597
+ placement='${placement}' or None,
19598
+ )
19599
+ " || {
19600
+ log_warn "Registry update failed; component files are still present"
19601
+ }
19602
+
19603
+ log_info "Component '$name' generated successfully"
19604
+ echo " Spec: $spec_path"
19605
+ case "$target" in
19606
+ react) echo " React: .loki/magic/generated/react/${name}.tsx" ;;
19607
+ webcomponent) echo " WC: .loki/magic/generated/webcomponent/${name}.js" ;;
19608
+ both)
19609
+ echo " React: .loki/magic/generated/react/${name}.tsx"
19610
+ echo " WC: .loki/magic/generated/webcomponent/${name}.js"
19611
+ ;;
19612
+ esac
19613
+ echo " Tests: .loki/magic/generated/tests/${name}.test.tsx"
19614
+ }
19615
+
19616
+ _magic_update() {
19617
+ local name=""
19618
+ local force="false"
19619
+
19620
+ while [[ $# -gt 0 ]]; do
19621
+ case "$1" in
19622
+ --name)
19623
+ name="${2:-}"
19624
+ shift 2
19625
+ ;;
19626
+ --force)
19627
+ force="true"
19628
+ shift
19629
+ ;;
19630
+ --help|-h)
19631
+ _magic_help
19632
+ return 0
19633
+ ;;
19634
+ *)
19635
+ log_error "Unknown option: $1"
19636
+ return 1
19637
+ ;;
19638
+ esac
19639
+ done
19640
+
19641
+ _magic_ensure_dirs
19642
+ local py
19643
+ py=$(_magic_python)
19644
+
19645
+ log_info "Updating components from specs (name=${name:-<all>}, force=$force)"
19646
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19647
+ import sys
19648
+ try:
19649
+ from magic.core.generator import update_components
19650
+ except Exception as exc:
19651
+ sys.stderr.write(f'[ERROR] magic.core.generator not available: {exc}\n')
19652
+ sys.exit(2)
19653
+ update_components(
19654
+ registry_path='.loki/magic/registry.json',
19655
+ name='${name}' or None,
19656
+ force=$([ "$force" = "true" ] && echo True || echo False),
19657
+ )
19658
+ " || {
19659
+ log_error "Update failed"
19660
+ return 1
19661
+ }
19662
+
19663
+ log_info "Update complete"
19664
+ }
19665
+
19666
+ _magic_list() {
19667
+ local format="table"
19668
+
19669
+ while [[ $# -gt 0 ]]; do
19670
+ case "$1" in
19671
+ --format)
19672
+ format="${2:-}"
19673
+ shift 2
19674
+ ;;
19675
+ --help|-h)
19676
+ _magic_help
19677
+ return 0
19678
+ ;;
19679
+ *)
19680
+ log_error "Unknown option: $1"
19681
+ return 1
19682
+ ;;
19683
+ esac
19684
+ done
19685
+
19686
+ case "$format" in
19687
+ json|table) ;;
19688
+ *)
19689
+ log_error "Invalid format: '$format' (must be json or table)"
19690
+ return 1
19691
+ ;;
19692
+ esac
19693
+
19694
+ _magic_ensure_dirs
19695
+ local py
19696
+ py=$(_magic_python)
19697
+
19698
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19699
+ import json, sys
19700
+ try:
19701
+ with open('.loki/magic/registry.json') as fh:
19702
+ data = json.load(fh)
19703
+ except Exception as exc:
19704
+ sys.stderr.write(f'[ERROR] could not read registry: {exc}\n')
19705
+ sys.exit(1)
19706
+
19707
+ components_raw = data.get('components', [])
19708
+ # Normalize: registry schema uses list of entries; legacy format was dict
19709
+ if isinstance(components_raw, dict):
19710
+ components = [{'name': k, **(v or {})} for k, v in components_raw.items()]
19711
+ else:
19712
+ components = components_raw
19713
+
19714
+ fmt = '$format'
19715
+
19716
+ if fmt == 'json':
19717
+ print(json.dumps(components, indent=2, sort_keys=True))
19718
+ else:
19719
+ if not components:
19720
+ print('No components registered. Use: loki magic generate <name>')
19721
+ sys.exit(0)
19722
+ print(f'{\"NAME\":<24} {\"VERSION\":<10} {\"TARGETS\":<24} {\"TAGS\"}')
19723
+ print('-' * 100)
19724
+ for entry in sorted(components, key=lambda e: e.get('name', '')):
19725
+ name = entry.get('name', '-')
19726
+ version = entry.get('version', '-')
19727
+ tgts = entry.get('targets', [])
19728
+ targets = ','.join(tgts) if isinstance(tgts, list) else str(tgts)
19729
+ tags = entry.get('tags', [])
19730
+ tags_str = ','.join(tags) if isinstance(tags, list) else str(tags)
19731
+ print(f'{name:<24} {version:<10} {targets:<24} {tags_str}')
19732
+ "
19733
+ }
19734
+
19735
+ _magic_remove() {
19736
+ local name=""
19737
+ local force="false"
19738
+
19739
+ while [[ $# -gt 0 ]]; do
19740
+ case "$1" in
19741
+ --force)
19742
+ force="true"
19743
+ shift
19744
+ ;;
19745
+ --help|-h)
19746
+ _magic_help
19747
+ return 0
19748
+ ;;
19749
+ -*)
19750
+ log_error "Unknown option: $1"
19751
+ return 1
19752
+ ;;
19753
+ *)
19754
+ if [ -z "$name" ]; then
19755
+ name="$1"
19756
+ fi
19757
+ shift
19758
+ ;;
19759
+ esac
19760
+ done
19761
+
19762
+ if [ -z "$name" ]; then
19763
+ log_error "Component name is required"
19764
+ echo "Usage: loki magic remove <name> [--force]"
19765
+ return 1
19766
+ fi
19767
+
19768
+ if ! _magic_valid_name "$name"; then
19769
+ log_error "Invalid component name: '$name'"
19770
+ return 1
19771
+ fi
19772
+
19773
+ _magic_ensure_dirs
19774
+
19775
+ if [ "$force" != "true" ]; then
19776
+ echo -n "Remove component '$name' and all its artifacts? [y/N] "
19777
+ local reply
19778
+ read -r reply || reply=""
19779
+ case "$reply" in
19780
+ y|Y|yes|YES) ;;
19781
+ *)
19782
+ log_info "Aborted"
19783
+ return 0
19784
+ ;;
19785
+ esac
19786
+ fi
19787
+
19788
+ rm -f ".loki/magic/specs/${name}.md" \
19789
+ ".loki/magic/generated/react/${name}.tsx" \
19790
+ ".loki/magic/generated/webcomponent/${name}.js" \
19791
+ ".loki/magic/generated/tests/${name}.test.tsx" 2>/dev/null || true
19792
+
19793
+ local py
19794
+ py=$(_magic_python)
19795
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19796
+ import json, sys
19797
+ try:
19798
+ with open('.loki/magic/registry.json') as fh:
19799
+ data = json.load(fh)
19800
+ except Exception:
19801
+ data = {'components': {}, 'version': 1}
19802
+ components = data.setdefault('components', {})
19803
+ if '$name' in components:
19804
+ del components['$name']
19805
+ with open('.loki/magic/registry.json', 'w') as fh:
19806
+ json.dump(data, fh, indent=2, sort_keys=True)
19807
+ " || log_warn "Could not update registry.json"
19808
+
19809
+ log_info "Removed component '$name'"
19810
+ }
19811
+
19812
+ _magic_registry() {
19813
+ local op="${1:-show}"
19814
+ shift 2>/dev/null || true
19815
+
19816
+ case "$op" in
19817
+ --help|-h|help)
19818
+ _magic_help
19819
+ return 0
19820
+ ;;
19821
+ esac
19822
+
19823
+ _magic_ensure_dirs
19824
+ local py
19825
+ py=$(_magic_python)
19826
+
19827
+ case "$op" in
19828
+ show)
19829
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19830
+ import json
19831
+ with open('.loki/magic/registry.json') as fh:
19832
+ data = json.load(fh)
19833
+ print(json.dumps(data, indent=2, sort_keys=True))
19834
+ "
19835
+ ;;
19836
+ prune)
19837
+ log_info "Pruning registry entries with missing artifacts"
19838
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19839
+ import json, os, sys
19840
+ try:
19841
+ from magic.core.registry import prune_registry
19842
+ removed = prune_registry('.loki/magic/registry.json')
19843
+ print(f'Pruned {removed} stale entries')
19844
+ except Exception as exc:
19845
+ # Fallback prune: drop entries whose spec file is missing
19846
+ with open('.loki/magic/registry.json') as fh:
19847
+ data = json.load(fh)
19848
+ comps = data.get('components', {})
19849
+ removed = 0
19850
+ for name in list(comps.keys()):
19851
+ spec = (comps[name] or {}).get('spec_path')
19852
+ if not spec or not os.path.isfile(spec):
19853
+ del comps[name]
19854
+ removed += 1
19855
+ with open('.loki/magic/registry.json', 'w') as fh:
19856
+ json.dump(data, fh, indent=2, sort_keys=True)
19857
+ print(f'Pruned {removed} stale entries (fallback)')
19858
+ "
19859
+ ;;
19860
+ stats)
19861
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19862
+ import json, os
19863
+ with open('.loki/magic/registry.json') as fh:
19864
+ data = json.load(fh)
19865
+ raw = data.get('components', []) or []
19866
+ # Accept both array (new) and dict (legacy) shapes
19867
+ if isinstance(raw, dict):
19868
+ comps = [{'name': k, **(v or {})} for k, v in raw.items()]
19869
+ else:
19870
+ comps = raw
19871
+ total = len(comps)
19872
+ targets = {}
19873
+ missing = 0
19874
+ for entry in comps:
19875
+ entry = entry or {}
19876
+ for t in entry.get('targets', [entry.get('target', 'unknown')]):
19877
+ targets[t] = targets.get(t, 0) + 1
19878
+ spec = entry.get('spec_path')
19879
+ if not spec or not os.path.isfile(spec):
19880
+ missing += 1
19881
+ print(f'Components: {total}')
19882
+ print(f'Missing specs: {missing}')
19883
+ print('By target:')
19884
+ for t in sorted(targets):
19885
+ print(f' {t:<14} {targets[t]}')
19886
+ "
19887
+ ;;
19888
+ *)
19889
+ log_error "Unknown registry op: $op"
19890
+ echo "Available: show, prune, stats"
19891
+ return 1
19892
+ ;;
19893
+ esac
19894
+ }
19895
+
19896
+ _magic_debate() {
19897
+ local name=""
19898
+ local rounds="3"
19899
+
19900
+ while [[ $# -gt 0 ]]; do
19901
+ case "$1" in
19902
+ --rounds)
19903
+ rounds="${2:-}"
19904
+ shift 2
19905
+ ;;
19906
+ --help|-h)
19907
+ _magic_help
19908
+ return 0
19909
+ ;;
19910
+ -*)
19911
+ log_error "Unknown option: $1"
19912
+ return 1
19913
+ ;;
19914
+ *)
19915
+ if [ -z "$name" ]; then
19916
+ name="$1"
19917
+ fi
19918
+ shift
19919
+ ;;
19920
+ esac
19921
+ done
19922
+
19923
+ if [ -z "$name" ]; then
19924
+ log_error "Component name is required"
19925
+ echo "Usage: loki magic debate <name> --rounds N"
19926
+ return 1
19927
+ fi
19928
+
19929
+ if ! _magic_valid_name "$name"; then
19930
+ log_error "Invalid component name: '$name'"
19931
+ return 1
19932
+ fi
19933
+
19934
+ if ! [[ "$rounds" =~ ^[0-9]+$ ]] || [ "$rounds" -lt 1 ]; then
19935
+ log_error "--rounds must be a positive integer"
19936
+ return 1
19937
+ fi
19938
+
19939
+ _magic_ensure_dirs
19940
+ local spec_path=".loki/magic/specs/${name}.md"
19941
+ if [ ! -f "$spec_path" ]; then
19942
+ log_error "No spec found for '$name' at $spec_path"
19943
+ return 1
19944
+ fi
19945
+
19946
+ local py
19947
+ py=$(_magic_python)
19948
+
19949
+ log_info "Running debate on '$name' ($rounds rounds)"
19950
+ PYTHONPATH="$(_magic_pypath)" "$py" -c "
19951
+ import sys
19952
+ try:
19953
+ from magic.core.debate import run_debate
19954
+ except Exception as exc:
19955
+ sys.stderr.write(f'[ERROR] magic.core.debate not available: {exc}\n')
19956
+ sys.exit(2)
19957
+ run_debate(
19958
+ name='$name',
19959
+ spec_path='$spec_path',
19960
+ rounds=$rounds,
19961
+ react_path='.loki/magic/generated/react/${name}.tsx',
19962
+ wc_path='.loki/magic/generated/webcomponent/${name}.js',
19963
+ )
19964
+ " || {
19965
+ log_error "Debate failed"
19966
+ return 1
19967
+ }
19968
+
19969
+ log_info "Debate complete"
19970
+ }
19971
+
19972
+ cmd_magic() {
19973
+ local subcmd="${1:-help}"
19974
+ shift 2>/dev/null || true
19975
+
19976
+ case "$subcmd" in
19977
+ generate|gen) _magic_generate "$@" ;;
19978
+ update|up) _magic_update "$@" ;;
19979
+ list|ls) _magic_list "$@" ;;
19980
+ remove|rm) _magic_remove "$@" ;;
19981
+ registry|reg) _magic_registry "$@" ;;
19982
+ debate) _magic_debate "$@" ;;
19983
+ help|--help|-h) _magic_help ;;
19984
+ *)
19985
+ log_error "Unknown magic subcommand: $subcmd"
19986
+ _magic_help
19987
+ return 1
19988
+ ;;
19989
+ esac
19990
+ }
19991
+
19320
19992
  # CI/CD quality gate integration (v6.22.0)
19321
19993
  cmd_ci() {
19322
19994
  local ci_pr=false