minder-cli 0.3.2__tar.gz → 0.3.3__tar.gz

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 (132) hide show
  1. {minder_cli-0.3.2 → minder_cli-0.3.3}/PKG-INFO +1 -1
  2. {minder_cli-0.3.2 → minder_cli-0.3.3}/pyproject.toml +1 -1
  3. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/cli.py +208 -74
  4. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/config.py +2 -2
  5. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/continuity.py +1 -1
  6. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/embedding/local.py +2 -2
  7. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/llm/local.py +1 -1
  8. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/prompts/formatter.py +1 -1
  9. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/repo_scanner.py +29 -20
  10. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/skills.py +11 -6
  11. {minder_cli-0.3.2 → minder_cli-0.3.3}/.gitignore +0 -0
  12. {minder_cli-0.3.2 → minder_cli-0.3.3}/LICENSE +0 -0
  13. {minder_cli-0.3.2 → minder_cli-0.3.3}/README.md +0 -0
  14. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/__init__.py +0 -0
  15. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/api/routers/prompts.py +0 -0
  16. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/application/__init__.py +0 -0
  17. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/application/admin/__init__.py +0 -0
  18. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/application/admin/dto.py +0 -0
  19. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/application/admin/jobs.py +0 -0
  20. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/application/admin/use_cases.py +0 -0
  21. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/auth/__init__.py +0 -0
  22. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/auth/context.py +0 -0
  23. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/auth/middleware.py +0 -0
  24. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/auth/principal.py +0 -0
  25. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/auth/rate_limiter.py +0 -0
  26. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/auth/rbac.py +0 -0
  27. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/auth/service.py +0 -0
  28. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/bootstrap/__init__.py +0 -0
  29. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/bootstrap/providers.py +0 -0
  30. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/bootstrap/transport.py +0 -0
  31. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/cache/__init__.py +0 -0
  32. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/cache/providers.py +0 -0
  33. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/chunking/__init__.py +0 -0
  34. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/chunking/code_splitter.py +0 -0
  35. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/chunking/splitter.py +0 -0
  36. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/dev.py +0 -0
  37. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/embedding/__init__.py +0 -0
  38. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/embedding/base.py +0 -0
  39. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/embedding/openai.py +0 -0
  40. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/__init__.py +0 -0
  41. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/edges.py +0 -0
  42. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/executor.py +0 -0
  43. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/graph.py +0 -0
  44. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/__init__.py +0 -0
  45. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/evaluator.py +0 -0
  46. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/guard.py +0 -0
  47. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/llm.py +0 -0
  48. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/planning.py +0 -0
  49. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/reasoning.py +0 -0
  50. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/reranker.py +0 -0
  51. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/retriever.py +0 -0
  52. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/verification.py +0 -0
  53. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/nodes/workflow_planner.py +0 -0
  54. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/runtime.py +0 -0
  55. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/graph/state.py +0 -0
  56. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/llm/__init__.py +0 -0
  57. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/llm/base.py +0 -0
  58. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/llm/openai.py +0 -0
  59. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/__init__.py +0 -0
  60. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/base.py +0 -0
  61. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/client.py +0 -0
  62. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/document.py +0 -0
  63. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/error.py +0 -0
  64. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/graph.py +0 -0
  65. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/history.py +0 -0
  66. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/job.py +0 -0
  67. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/prompt.py +0 -0
  68. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/repository.py +0 -0
  69. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/rule.py +0 -0
  70. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/session.py +0 -0
  71. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/skill.py +0 -0
  72. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/user.py +0 -0
  73. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/models/workflow.py +0 -0
  74. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/observability/__init__.py +0 -0
  75. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/observability/audit.py +0 -0
  76. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/observability/logging.py +0 -0
  77. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/observability/metrics.py +0 -0
  78. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/observability/tracing.py +0 -0
  79. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/__init__.py +0 -0
  80. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/__init__.py +0 -0
  81. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/__init__.py +0 -0
  82. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/api.py +0 -0
  83. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/context.py +0 -0
  84. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/dashboard.py +0 -0
  85. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/jobs.py +0 -0
  86. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/memories.py +0 -0
  87. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/prompts.py +0 -0
  88. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/routes.py +0 -0
  89. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/runtime.py +0 -0
  90. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/search.py +0 -0
  91. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/presentation/http/admin/skills.py +0 -0
  92. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/prompts/__init__.py +0 -0
  93. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/resources/__init__.py +0 -0
  94. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/retrieval/__init__.py +0 -0
  95. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/retrieval/hybrid.py +0 -0
  96. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/retrieval/mmr.py +0 -0
  97. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/retrieval/multi_hop.py +0 -0
  98. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/runtime.py +0 -0
  99. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/server.py +0 -0
  100. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/__init__.py +0 -0
  101. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/document.py +0 -0
  102. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/error.py +0 -0
  103. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/feedback.py +0 -0
  104. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/graph.py +0 -0
  105. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/history.py +0 -0
  106. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/interfaces.py +0 -0
  107. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/milvus/__init__.py +0 -0
  108. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/milvus/client.py +0 -0
  109. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/milvus/collections.py +0 -0
  110. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/milvus/vector_store.py +0 -0
  111. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/mongodb/__init__.py +0 -0
  112. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/mongodb/client.py +0 -0
  113. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/mongodb/indexes.py +0 -0
  114. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/mongodb/operational_store.py +0 -0
  115. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/relational.py +0 -0
  116. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/repo_state.py +0 -0
  117. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/rule.py +0 -0
  118. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/store/vector.py +0 -0
  119. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/__init__.py +0 -0
  120. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/auth.py +0 -0
  121. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/graph.py +0 -0
  122. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/ingest.py +0 -0
  123. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/memory.py +0 -0
  124. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/query.py +0 -0
  125. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/registry.py +0 -0
  126. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/search.py +0 -0
  127. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/session.py +0 -0
  128. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/tools/workflow.py +0 -0
  129. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/transport/__init__.py +0 -0
  130. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/transport/base.py +0 -0
  131. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/transport/sse.py +0 -0
  132. {minder_cli-0.3.2 → minder_cli-0.3.3}/src/minder/transport/stdio.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: minder-cli
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Minder CLI is the command-line interface for the Minder self-hosted MCP platform.
5
5
  Project-URL: Homepage, https://github.com/hiimtrung/minder
6
6
  Project-URL: Repository, https://github.com/hiimtrung/minder
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "minder-cli"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Minder CLI is the command-line interface for the Minder self-hosted MCP platform."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -24,6 +24,7 @@ import httpx
24
24
  from minder.tools.repo_scanner import RepoScanner
25
25
 
26
26
  _DEFAULT_SERVER_URL = "http://localhost:8801/sse"
27
+ _DEFAULT_PROTOCOL = "sse"
27
28
  _LOCAL_MCP_TARGETS = ("vscode", "cursor", "claude-code")
28
29
  _PYPI_JSON_URL = "https://pypi.org/pypi/minder/json"
29
30
  _IDE_GITIGNORE_KEY = "minder-ide-bootstrap"
@@ -120,14 +121,28 @@ def _prompt_client_key() -> str:
120
121
  return client_key
121
122
 
122
123
 
124
+ def _prompt_protocol(default: str = _DEFAULT_PROTOCOL) -> str:
125
+ value = input(f"Minder protocol [sse/stdio] (default: {default}): ").strip().lower()
126
+ return value or default
127
+
128
+
129
+ def _normalize_protocol(raw_protocol: str | None) -> str:
130
+ protocol = (raw_protocol or "").strip().lower() or _DEFAULT_PROTOCOL
131
+ if protocol not in {"sse", "stdio"}:
132
+ raise ValueError("Protocol must be either 'sse' or 'stdio'")
133
+ return protocol
134
+
135
+
123
136
  def _require_client_settings(config_path: Path) -> dict[str, Any]:
124
137
  payload = _load_json(config_path)
138
+ protocol = _normalize_protocol(str(payload.get("protocol", _DEFAULT_PROTOCOL)))
125
139
  client_key = str(payload.get("client_api_key", "")).strip()
126
140
  server_url = str(payload.get("server_url", "")).strip()
127
141
  if not client_key:
128
142
  raise ValueError(f"No client_api_key found in {config_path}")
129
- if not server_url:
143
+ if protocol == "sse" and not server_url:
130
144
  raise ValueError(f"No server_url found in {config_path}")
145
+ payload["protocol"] = protocol
131
146
  return payload
132
147
 
133
148
 
@@ -393,29 +408,31 @@ def _cli_update_commands(manager: str) -> list[list[str]]:
393
408
 
394
409
 
395
410
  def _self_update_cli(manager: str) -> None:
411
+ before_version = _installed_package_version()
412
+ target_version = _latest_pypi_version()
396
413
  failures: list[str] = []
397
414
  for command in _cli_update_commands(manager):
398
415
  executable = command[0]
399
416
  if executable != sys.executable and shutil.which(executable) is None:
400
417
  failures.append(f"{' '.join(command)} -> command not available")
401
418
  continue
419
+ print(f"Executing CLI update: {' '.join(command)}")
402
420
  result = subprocess.run(
403
421
  command,
404
- capture_output=True,
422
+ capture_output=False,
405
423
  text=True,
406
424
  check=False,
407
425
  )
408
426
  if result.returncode == 0:
409
- output = result.stdout.strip()
410
- if output:
411
- print(output)
412
- print(f"CLI self-update completed via: {' '.join(command)}")
427
+ after_version = _installed_package_version() or target_version
428
+ print(
429
+ "CLI update completed: "
430
+ f"{before_version or 'unknown'} -> {after_version or 'unknown'} "
431
+ f"via {' '.join(command)}"
432
+ )
413
433
  return
414
- details = (
415
- result.stderr.strip() or result.stdout.strip() or str(result.returncode)
416
- )
417
- failures.append(f"{' '.join(command)} -> {details}")
418
- raise RuntimeError("CLI self-update failed: " + "; ".join(failures))
434
+ failures.append(f"{' '.join(command)} -> failed with exit code {result.returncode}")
435
+ raise RuntimeError("CLI update failed: " + "; ".join(failures))
419
436
 
420
437
 
421
438
  def _installer_asset_filename(release_tag: str, *, installer: str) -> str:
@@ -448,7 +465,7 @@ def _run_bash_installer(script: str, env: dict[str, str]) -> subprocess.Complete
448
465
  return subprocess.run(
449
466
  ["bash"],
450
467
  input=script,
451
- capture_output=True,
468
+ capture_output=False,
452
469
  text=True,
453
470
  env=env,
454
471
  check=False,
@@ -462,7 +479,7 @@ def _run_powershell_installer(
462
479
  return subprocess.run(
463
480
  [executable, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "-"],
464
481
  input=script,
465
- capture_output=True,
482
+ capture_output=False,
466
483
  text=True,
467
484
  env=env,
468
485
  check=False,
@@ -491,20 +508,16 @@ def _self_update_server(install_dir: Path) -> None:
491
508
  value = env_payload.get(key)
492
509
  if value:
493
510
  update_env[key] = value
511
+ print(f"Executing server update for {install_dir}...")
494
512
  if installer_variant == "powershell":
495
513
  result = _run_powershell_installer(installer_script, update_env)
496
514
  else:
497
515
  result = _run_bash_installer(installer_script, update_env)
498
516
  if result.returncode != 0:
499
- details = (
500
- result.stderr.strip() or result.stdout.strip() or str(result.returncode)
501
- )
502
- raise RuntimeError(f"Server self-update failed: {details}")
503
- output = result.stdout.strip()
504
- if output:
505
- print(output)
517
+ raise RuntimeError(f"Server update failed with exit code {result.returncode}")
518
+ # Output is already streamed to stdout/stderr in installer helpers if not captured
506
519
  print(
507
- f"Server self-update completed for {install_dir}: "
520
+ f"Server update completed for {install_dir}: "
508
521
  f"{current or 'unknown'} -> {latest}"
509
522
  )
510
523
  print(f"Release installer: {installer_url}")
@@ -529,7 +542,7 @@ def _check_update(args: argparse.Namespace) -> int:
529
542
  return 0
530
543
 
531
544
 
532
- def _self_update(args: argparse.Namespace) -> int:
545
+ def _update(args: argparse.Namespace) -> int:
533
546
  components = ["cli", "server"] if args.component == "all" else [args.component]
534
547
  if "cli" in components:
535
548
  _self_update_cli(args.manager)
@@ -543,35 +556,42 @@ def _self_update(args: argparse.Namespace) -> int:
543
556
  return 0
544
557
 
545
558
 
559
+ def _run_git(
560
+ args: list[str],
561
+ *,
562
+ cwd: Path | str | None = None,
563
+ capture_output: bool = True,
564
+ check: bool = True,
565
+ ) -> subprocess.CompletedProcess[str]:
566
+ try:
567
+ return subprocess.run(
568
+ ["git", *args],
569
+ cwd=cwd,
570
+ capture_output=capture_output,
571
+ text=True,
572
+ check=check,
573
+ )
574
+ except FileNotFoundError:
575
+ raise RuntimeError(
576
+ "Git executable not found. Please install git to support this command."
577
+ ) from None
578
+
579
+
546
580
  def _repo_root(path: str) -> Path:
547
- result = subprocess.run(
548
- ["git", "rev-parse", "--show-toplevel"],
549
- cwd=path,
550
- capture_output=True,
551
- text=True,
552
- check=True,
553
- )
581
+ result = _run_git(["rev-parse", "--show-toplevel"], cwd=path)
554
582
  return Path(result.stdout.strip()).resolve()
555
583
 
556
584
 
557
585
  def _git_branch(repo_root: Path) -> str | None:
558
- result = subprocess.run(
559
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
560
- cwd=repo_root,
561
- capture_output=True,
562
- text=True,
563
- check=True,
564
- )
586
+ result = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root)
565
587
  branch = result.stdout.strip()
566
588
  return branch or None
567
589
 
568
590
 
569
591
  def _git_remote_url(repo_root: Path) -> str | None:
570
- result = subprocess.run(
571
- ["git", "config", "--get", "remote.origin.url"],
592
+ result = _run_git(
593
+ ["config", "--get", "remote.origin.url"],
572
594
  cwd=repo_root,
573
- capture_output=True,
574
- text=True,
575
595
  check=False,
576
596
  )
577
597
  remote_url = result.stdout.strip()
@@ -623,39 +643,26 @@ def _git_file_delta(
623
643
  diff_command.append(f"{diff_base}...HEAD")
624
644
  else:
625
645
  diff_command.append("HEAD")
626
- diff_result = subprocess.run(
627
- diff_command,
628
- cwd=repo_root,
629
- capture_output=True,
630
- text=True,
631
- check=True,
632
- )
646
+ diff_result = _run_git(diff_command, cwd=repo_root)
633
647
  changed = {line.strip() for line in diff_result.stdout.splitlines() if line.strip()}
634
648
 
635
- deleted_result = subprocess.run(
649
+ deleted_result = _run_git(
636
650
  [
637
- "git",
638
651
  "diff",
639
652
  "--name-only",
640
653
  "--diff-filter=D",
641
654
  *([f"{diff_base}...HEAD"] if diff_base else ["HEAD"]),
642
655
  ],
643
656
  cwd=repo_root,
644
- capture_output=True,
645
- text=True,
646
- check=True,
647
657
  )
648
658
  deleted = {
649
659
  line.strip() for line in deleted_result.stdout.splitlines() if line.strip()
650
660
  }
651
661
  changed.difference_update(deleted)
652
662
 
653
- untracked_result = subprocess.run(
654
- ["git", "ls-files", "--others", "--exclude-standard"],
663
+ untracked_result = _run_git(
664
+ ["ls-files", "--others", "--exclude-standard"],
655
665
  cwd=repo_root,
656
- capture_output=True,
657
- text=True,
658
- check=True,
659
666
  )
660
667
  changed.update(
661
668
  line.strip() for line in untracked_result.stdout.splitlines() if line.strip()
@@ -1078,7 +1085,29 @@ def _target_root_key(target: str) -> str:
1078
1085
  return "mcpServers"
1079
1086
 
1080
1087
 
1081
- def _target_entry(target: str, server_url: str, client_key: str) -> dict[str, Any]:
1088
+ def _target_entry(
1089
+ target: str,
1090
+ *,
1091
+ protocol: str,
1092
+ server_url: str,
1093
+ client_key: str,
1094
+ cwd: Path,
1095
+ ) -> dict[str, Any]:
1096
+ normalized_protocol = _normalize_protocol(protocol)
1097
+ if normalized_protocol == "stdio":
1098
+ entry: dict[str, Any] = {
1099
+ "command": "uv",
1100
+ "args": ["run", "python", "-m", "minder.server"],
1101
+ "cwd": str(cwd),
1102
+ "env": {
1103
+ "MINDER_SERVER__TRANSPORT": "stdio",
1104
+ "MINDER_CLIENT_API_KEY": client_key,
1105
+ },
1106
+ }
1107
+ if target in {"vscode", "cursor"}:
1108
+ entry["type"] = "stdio"
1109
+ return entry
1110
+
1082
1111
  sse_url = _sse_url(server_url)
1083
1112
  mcp_url = _mcp_url(server_url)
1084
1113
  if target == "vscode":
@@ -1101,11 +1130,25 @@ def _target_entry(target: str, server_url: str, client_key: str) -> dict[str, An
1101
1130
  raise ValueError(f"Unsupported MCP target: {target}")
1102
1131
 
1103
1132
 
1104
- def _install_target(path: Path, target: str, server_url: str, client_key: str) -> None:
1133
+ def _install_target(
1134
+ path: Path,
1135
+ target: str,
1136
+ *,
1137
+ protocol: str,
1138
+ server_url: str,
1139
+ client_key: str,
1140
+ cwd: Path,
1141
+ ) -> None:
1105
1142
  payload = _load_json(path)
1106
1143
  root_key = _target_root_key(target)
1107
1144
  payload.setdefault(root_key, {})
1108
- payload[root_key]["minder"] = _target_entry(target, server_url, client_key)
1145
+ payload[root_key]["minder"] = _target_entry(
1146
+ target,
1147
+ protocol=protocol,
1148
+ server_url=server_url,
1149
+ client_key=client_key,
1150
+ cwd=cwd,
1151
+ )
1109
1152
  if target == "vscode":
1110
1153
  payload.setdefault("inputs", [])
1111
1154
  _write_json(path, payload)
@@ -1159,9 +1202,19 @@ def _login(args: argparse.Namespace) -> int:
1159
1202
 
1160
1203
  config_path = Path(args.config_path).expanduser()
1161
1204
  existing = _load_json(config_path)
1205
+ default_protocol = _normalize_protocol(
1206
+ str(existing.get("protocol", _DEFAULT_PROTOCOL))
1207
+ )
1208
+ protocol = _normalize_protocol(args.protocol or _prompt_protocol(default_protocol))
1209
+ default_server_url = str(existing.get("server_url", _DEFAULT_SERVER_URL)).strip()
1210
+ if protocol == "sse":
1211
+ server_url = (args.server_url or "").strip() or default_server_url
1212
+ else:
1213
+ server_url = (args.server_url or "").strip() or default_server_url
1162
1214
  payload = {
1163
1215
  **existing,
1164
- "server_url": args.server_url,
1216
+ "protocol": protocol,
1217
+ "server_url": server_url,
1165
1218
  "client_api_key": client_key,
1166
1219
  "default_headers": {
1167
1220
  "X-Minder-Client-Key": client_key,
@@ -1169,6 +1222,8 @@ def _login(args: argparse.Namespace) -> int:
1169
1222
  }
1170
1223
  _write_json(config_path, payload)
1171
1224
  print(f"Stored client credentials in {config_path}")
1225
+ print(f"Protocol: {protocol}")
1226
+ print(f"Server URL: {server_url}")
1172
1227
  print(f"export MINDER_CLIENT_API_KEY={client_key}")
1173
1228
  return 0
1174
1229
 
@@ -1176,6 +1231,7 @@ def _login(args: argparse.Namespace) -> int:
1176
1231
  def _install_mcp(args: argparse.Namespace) -> int:
1177
1232
  config_path = Path(args.config_path).expanduser()
1178
1233
  settings = _require_client_settings(config_path)
1234
+ protocol = str(settings.get("protocol", _DEFAULT_PROTOCOL))
1179
1235
  targets = _parse_targets(args.target)
1180
1236
  install_root = Path(args.cwd).resolve()
1181
1237
  installed_paths: list[Path] = []
@@ -1189,8 +1245,10 @@ def _install_mcp(args: argparse.Namespace) -> int:
1189
1245
  _install_target(
1190
1246
  path,
1191
1247
  target,
1192
- str(settings["server_url"]),
1193
- str(settings["client_api_key"]),
1248
+ protocol=protocol,
1249
+ server_url=str(settings.get("server_url", "")),
1250
+ client_key=str(settings["client_api_key"]),
1251
+ cwd=install_root,
1194
1252
  )
1195
1253
  installed_paths.append(path)
1196
1254
 
@@ -1222,6 +1280,7 @@ def _uninstall_mcp(args: argparse.Namespace) -> int:
1222
1280
  def _install_ide(args: argparse.Namespace) -> int:
1223
1281
  config_path = Path(args.config_path).expanduser()
1224
1282
  settings = _require_client_settings(config_path)
1283
+ protocol = str(settings.get("protocol", _DEFAULT_PROTOCOL))
1225
1284
  targets = _parse_targets(args.target)
1226
1285
  install_root = Path(args.cwd).resolve()
1227
1286
  installed_paths: list[Path] = []
@@ -1231,8 +1290,10 @@ def _install_ide(args: argparse.Namespace) -> int:
1231
1290
  _install_target(
1232
1291
  path,
1233
1292
  target,
1234
- str(settings["server_url"]),
1235
- str(settings["client_api_key"]),
1293
+ protocol=protocol,
1294
+ server_url=str(settings.get("server_url", "")),
1295
+ client_key=str(settings["client_api_key"]),
1296
+ cwd=install_root,
1236
1297
  )
1237
1298
  installed_paths.append(path)
1238
1299
 
@@ -1298,6 +1359,11 @@ def _resolve_repo_id(
1298
1359
  def _sync(args: argparse.Namespace) -> int:
1299
1360
  config_path = Path(args.config_path).expanduser()
1300
1361
  settings = _require_client_settings(config_path)
1362
+ protocol = str(settings.get("protocol", _DEFAULT_PROTOCOL))
1363
+ if protocol != "sse":
1364
+ raise ValueError(
1365
+ "minder sync requires protocol 'sse' with a reachable server_url."
1366
+ )
1301
1367
  if not args.skip_upgrade_check:
1302
1368
  _maybe_print_upgrade_notice()
1303
1369
  repo_root = _repo_root(args.repo_path)
@@ -1332,23 +1398,65 @@ def _sync(args: argparse.Namespace) -> int:
1332
1398
  return 0
1333
1399
 
1334
1400
 
1401
+ def _version(args: argparse.Namespace) -> int:
1402
+ version = _installed_package_version()
1403
+ if version:
1404
+ print(f"minder {version}")
1405
+ else:
1406
+ print("minder version unknown (not installed as a package)")
1407
+ return 0
1408
+
1409
+
1410
+ def _check_version(args: argparse.Namespace) -> int: # noqa: ARG001
1411
+ installed, latest, has_update = _cli_update_available()
1412
+ print("CLI version:")
1413
+ print(f" installed: {installed or 'unknown'}")
1414
+ print(f" latest: {latest or 'unknown'}")
1415
+ if installed and latest:
1416
+ if has_update:
1417
+ print(f" status: update available ({installed} -> {latest})")
1418
+ else:
1419
+ print(" status: up to date")
1420
+ else:
1421
+ print(" status: unable to determine latest version")
1422
+ return 0
1423
+
1424
+
1335
1425
  def build_parser() -> argparse.ArgumentParser:
1336
1426
  parser = argparse.ArgumentParser(description="Minder CLI")
1427
+ parser.add_argument(
1428
+ "-v",
1429
+ "--version",
1430
+ action="version",
1431
+ version=f"minder {_installed_package_version() or 'unknown'}",
1432
+ )
1337
1433
  subparsers = parser.add_subparsers(dest="command", required=True)
1338
1434
 
1435
+ subparsers.add_parser("version", help="Show the Minder CLI version.")
1436
+ subparsers.add_parser(
1437
+ "check-version",
1438
+ help="Show installed CLI version and latest published version.",
1439
+ )
1440
+
1339
1441
  login = subparsers.add_parser(
1340
1442
  "login",
1341
- help="Store a Minder client API key for CLI-authenticated commands.",
1443
+ help="Store Minder client auth + transport settings for CLI commands.",
1342
1444
  )
1343
1445
  login.add_argument(
1344
1446
  "--client-key",
1345
1447
  default=None,
1346
1448
  help="Client API key in mkc_... format. If omitted, prompt securely.",
1347
1449
  )
1450
+ login.add_argument(
1451
+ "--protocol",
1452
+ choices=("sse", "stdio"),
1453
+ default=None,
1454
+ help="Client protocol mode: sse (remote) or stdio (local process).",
1455
+ )
1348
1456
  login.add_argument(
1349
1457
  "--server-url",
1350
- default=_DEFAULT_SERVER_URL,
1351
- help="Default Minder server URL to pair with the stored client key.",
1458
+ default=None,
1459
+ help="Minder server URL. Used for SSE mode and remote sync APIs.",
1352
1460
  )
1353
1461
  login.add_argument(
1354
1462
  "--config-path",
@@ -1454,26 +1562,48 @@ def build_parser() -> argparse.ArgumentParser:
1454
1562
  help="Server install directory to inspect. Defaults to ~/.minder/current or the newest release directory.",
1455
1563
  )
1456
1564
 
1565
+ update = subparsers.add_parser(
1566
+ "update",
1567
+ help="Apply an update for the Minder CLI, server deployment, or both.",
1568
+ )
1569
+ update.add_argument(
1570
+ "--component",
1571
+ choices=("cli", "server", "all"),
1572
+ default="cli",
1573
+ help="Which component to update.",
1574
+ )
1575
+ update.add_argument(
1576
+ "--manager",
1577
+ choices=("auto", "uv", "pipx", "pip"),
1578
+ default="auto",
1579
+ help="Preferred package manager for CLI update.",
1580
+ )
1581
+ update.add_argument(
1582
+ "--install-dir",
1583
+ default=None,
1584
+ help="Server install directory to update. Defaults to ~/.minder/current or the newest release directory.",
1585
+ )
1586
+
1457
1587
  self_update = subparsers.add_parser(
1458
1588
  "self-update",
1459
- help="Apply a self-update for the Minder CLI, server deployment, or both.",
1589
+ help=argparse.SUPPRESS,
1460
1590
  )
1461
1591
  self_update.add_argument(
1462
1592
  "--component",
1463
1593
  choices=("cli", "server", "all"),
1464
1594
  default="cli",
1465
- help="Which component to self-update.",
1595
+ help=argparse.SUPPRESS,
1466
1596
  )
1467
1597
  self_update.add_argument(
1468
1598
  "--manager",
1469
1599
  choices=("auto", "uv", "pipx", "pip"),
1470
1600
  default="auto",
1471
- help="Preferred package manager for CLI self-update.",
1601
+ help=argparse.SUPPRESS,
1472
1602
  )
1473
1603
  self_update.add_argument(
1474
1604
  "--install-dir",
1475
1605
  default=None,
1476
- help="Server install directory to update. Defaults to ~/.minder/current or the newest release directory.",
1606
+ help=argparse.SUPPRESS,
1477
1607
  )
1478
1608
 
1479
1609
  sync = subparsers.add_parser(
@@ -1529,10 +1659,14 @@ def main(argv: list[str] | None = None) -> int:
1529
1659
  return _uninstall_ide(args)
1530
1660
  if args.command == "check-update":
1531
1661
  return _check_update(args)
1532
- if args.command == "self-update":
1533
- return _self_update(args)
1662
+ if args.command in {"update", "self-update"}:
1663
+ return _update(args)
1534
1664
  if args.command == "sync":
1535
1665
  return _sync(args)
1666
+ if args.command == "version":
1667
+ return _version(args)
1668
+ if args.command == "check-version":
1669
+ return _check_version(args)
1536
1670
 
1537
1671
  parser.error(f"Unknown command: {args.command}")
1538
1672
  return 2
@@ -33,7 +33,7 @@ class AuthConfig(BaseModel):
33
33
  class EmbeddingConfig(BaseModel):
34
34
  provider: str = "ollama"
35
35
  ollama_url: str = "http://localhost:11434"
36
- ollama_model: str = "embeddinggemma"
36
+ ollama_model: str = "qwen3-embedding:0.6b"
37
37
  dimensions: int = 768
38
38
  openai_api_key: Optional[str] = None
39
39
  openai_model: str = "text-embedding-3-small"
@@ -42,7 +42,7 @@ class EmbeddingConfig(BaseModel):
42
42
  class LLMConfig(BaseModel):
43
43
  provider: str = "ollama"
44
44
  ollama_url: str = "http://localhost:11434"
45
- ollama_model: str = "gemma4:e4b"
45
+ ollama_model: str = "qwen3.5:4b"
46
46
  context_length: int = 131072
47
47
  temperature: float = 0.1
48
48
  openai_api_key: Optional[str] = None
@@ -330,7 +330,7 @@ class ContinuitySynthesizer:
330
330
  },
331
331
  }, {
332
332
  "provider": "local_llm",
333
- "model": "gemma-4-e2b-it",
333
+ "model": self._config.llm.ollama_model,
334
334
  "runtime": self._llm.runtime,
335
335
  }
336
336
 
@@ -18,7 +18,7 @@ class LocalEmbeddingProvider:
18
18
  def __init__(
19
19
  self,
20
20
  ollama_url: str = "http://localhost:11434",
21
- ollama_model: str = "embeddinggemma",
21
+ ollama_model: str = "qwen3-embedding:0.6b",
22
22
  dimensions: int = 768,
23
23
  runtime: str = "auto",
24
24
  ) -> None:
@@ -73,4 +73,4 @@ class LocalEmbeddingProvider:
73
73
  return None
74
74
  except Exception:
75
75
  logger.warning("Ollama embedding failed, using hash fallback")
76
- return None
76
+ return None
@@ -24,7 +24,7 @@ class LocalModelLLM:
24
24
  def __init__(
25
25
  self,
26
26
  ollama_url: str = "http://localhost:11434",
27
- ollama_model: str = "gemma4:e4b",
27
+ ollama_model: str = "qwen3.5:4b",
28
28
  fail: bool = False,
29
29
  context_length: int = 131072,
30
30
  ) -> None:
@@ -137,6 +137,6 @@ Make the prompt direct, structured, and useful for a coding workflow.
137
137
  )
138
138
  return merged, {
139
139
  "provider": "local_llm",
140
- "model": "gemma-4-e2b-it",
140
+ "model": config.llm.ollama_model,
141
141
  "runtime": runtime,
142
142
  }
@@ -132,6 +132,28 @@ class RepoScanner:
132
132
  self._git_metadata_cache: dict[str, dict[str, Any]] = {}
133
133
  self._git_line_commit_cache: dict[tuple[str, int], dict[str, str] | None] = {}
134
134
  self._git_commit_detail_cache: dict[str, dict[str, str]] = {}
135
+ self._git_enabled = True
136
+
137
+ def _run_git(
138
+ self,
139
+ args: list[str],
140
+ *,
141
+ capture_output: bool = True,
142
+ check: bool = True,
143
+ ) -> subprocess.CompletedProcess[str]:
144
+ if not self._git_enabled:
145
+ return subprocess.CompletedProcess(args, 1, stdout="", stderr="")
146
+ try:
147
+ return subprocess.run(
148
+ ["git", *args],
149
+ cwd=self._root,
150
+ capture_output=capture_output,
151
+ text=True,
152
+ check=check,
153
+ )
154
+ except FileNotFoundError:
155
+ self._git_enabled = False
156
+ return subprocess.CompletedProcess(args, 1, stdout="", stderr="")
135
157
 
136
158
  async def scan(self) -> dict[str, Any]:
137
159
  service_dirs = self._discover_service_boundaries()
@@ -258,6 +280,7 @@ class RepoScanner:
258
280
  builder._git_metadata_cache = {}
259
281
  builder._git_line_commit_cache = {}
260
282
  builder._git_commit_detail_cache = {}
283
+ builder._git_enabled = True
261
284
 
262
285
  service_dirs = builder._discover_service_boundaries()
263
286
  source_files = builder._resolve_source_files(changed_files)
@@ -526,9 +549,8 @@ class RepoScanner:
526
549
  }
527
550
 
528
551
  def _git_recent_commits(self, rel_path: str, limit: int = 5) -> list[dict[str, str]]:
529
- result = subprocess.run(
552
+ result = self._run_git(
530
553
  [
531
- "git",
532
554
  "log",
533
555
  "--follow",
534
556
  "--format=%H%x1f%cI%x1f%s",
@@ -537,9 +559,6 @@ class RepoScanner:
537
559
  "--",
538
560
  rel_path,
539
561
  ],
540
- cwd=self._root,
541
- capture_output=True,
542
- text=True,
543
562
  check=False,
544
563
  )
545
564
  if result.returncode != 0:
@@ -564,9 +583,8 @@ class RepoScanner:
564
583
  if cache_key in self._git_line_commit_cache:
565
584
  return self._git_line_commit_cache[cache_key]
566
585
 
567
- result = subprocess.run(
586
+ result = self._run_git(
568
587
  [
569
- "git",
570
588
  "blame",
571
589
  "--line-porcelain",
572
590
  "-L",
@@ -574,9 +592,6 @@ class RepoScanner:
574
592
  "--",
575
593
  rel_path,
576
594
  ],
577
- cwd=self._root,
578
- capture_output=True,
579
- text=True,
580
595
  check=False,
581
596
  )
582
597
  if result.returncode != 0:
@@ -598,11 +613,8 @@ class RepoScanner:
598
613
  if cached is not None:
599
614
  return cached
600
615
 
601
- result = subprocess.run(
602
- ["git", "show", "-s", "--format=%H%x1f%cI%x1f%s", sha],
603
- cwd=self._root,
604
- capture_output=True,
605
- text=True,
616
+ result = self._run_git(
617
+ ["show", "-s", "--format=%H%x1f%cI%x1f%s", sha],
606
618
  check=False,
607
619
  )
608
620
  if result.returncode != 0:
@@ -627,11 +639,8 @@ class RepoScanner:
627
639
  return details
628
640
 
629
641
  def _git_status(self, rel_path: str, *, tracked: bool) -> str:
630
- result = subprocess.run(
631
- ["git", "status", "--short", "--", rel_path],
632
- cwd=self._root,
633
- capture_output=True,
634
- text=True,
642
+ result = self._run_git(
643
+ ["status", "--short", "--", rel_path],
635
644
  check=False,
636
645
  )
637
646
  if result.returncode != 0:
@@ -294,12 +294,17 @@ class SkillTools:
294
294
  if ref:
295
295
  command += ["--branch", ref]
296
296
  command += [repo_url, tmp_dir]
297
- result = subprocess.run(
298
- command,
299
- capture_output=True,
300
- text=True,
301
- check=False,
302
- )
297
+ try:
298
+ result = subprocess.run(
299
+ command,
300
+ capture_output=True,
301
+ text=True,
302
+ check=False,
303
+ )
304
+ except FileNotFoundError:
305
+ raise RuntimeError(
306
+ "Git executable not found. Please install git to support skill importation from remote repositories."
307
+ ) from None
303
308
  if result.returncode != 0:
304
309
  message = (
305
310
  result.stderr.strip() or result.stdout.strip() or "git clone failed"
File without changes
File without changes
File without changes
File without changes