minder-cli 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.
Files changed (132) hide show
  1. minder/__init__.py +12 -0
  2. minder/api/routers/prompts.py +177 -0
  3. minder/application/__init__.py +1 -0
  4. minder/application/admin/__init__.py +11 -0
  5. minder/application/admin/dto.py +453 -0
  6. minder/application/admin/jobs.py +327 -0
  7. minder/application/admin/use_cases.py +1895 -0
  8. minder/auth/__init__.py +12 -0
  9. minder/auth/context.py +26 -0
  10. minder/auth/middleware.py +70 -0
  11. minder/auth/principal.py +59 -0
  12. minder/auth/rate_limiter.py +89 -0
  13. minder/auth/rbac.py +60 -0
  14. minder/auth/service.py +541 -0
  15. minder/bootstrap/__init__.py +9 -0
  16. minder/bootstrap/providers.py +109 -0
  17. minder/bootstrap/transport.py +807 -0
  18. minder/cache/__init__.py +10 -0
  19. minder/cache/providers.py +140 -0
  20. minder/chunking/__init__.py +4 -0
  21. minder/chunking/code_splitter.py +184 -0
  22. minder/chunking/splitter.py +136 -0
  23. minder/cli.py +1542 -0
  24. minder/config.py +179 -0
  25. minder/continuity.py +363 -0
  26. minder/dev.py +160 -0
  27. minder/embedding/__init__.py +9 -0
  28. minder/embedding/base.py +7 -0
  29. minder/embedding/local.py +65 -0
  30. minder/embedding/openai.py +7 -0
  31. minder/graph/__init__.py +11 -0
  32. minder/graph/edges.py +13 -0
  33. minder/graph/executor.py +127 -0
  34. minder/graph/graph.py +263 -0
  35. minder/graph/nodes/__init__.py +27 -0
  36. minder/graph/nodes/evaluator.py +21 -0
  37. minder/graph/nodes/guard.py +64 -0
  38. minder/graph/nodes/llm.py +59 -0
  39. minder/graph/nodes/planning.py +30 -0
  40. minder/graph/nodes/reasoning.py +87 -0
  41. minder/graph/nodes/reranker.py +141 -0
  42. minder/graph/nodes/retriever.py +86 -0
  43. minder/graph/nodes/verification.py +230 -0
  44. minder/graph/nodes/workflow_planner.py +250 -0
  45. minder/graph/runtime.py +15 -0
  46. minder/graph/state.py +26 -0
  47. minder/llm/__init__.py +5 -0
  48. minder/llm/base.py +14 -0
  49. minder/llm/local.py +381 -0
  50. minder/llm/openai.py +89 -0
  51. minder/models/__init__.py +109 -0
  52. minder/models/base.py +10 -0
  53. minder/models/client.py +137 -0
  54. minder/models/document.py +34 -0
  55. minder/models/error.py +32 -0
  56. minder/models/graph.py +114 -0
  57. minder/models/history.py +32 -0
  58. minder/models/job.py +62 -0
  59. minder/models/prompt.py +41 -0
  60. minder/models/repository.py +62 -0
  61. minder/models/rule.py +68 -0
  62. minder/models/session.py +51 -0
  63. minder/models/skill.py +52 -0
  64. minder/models/user.py +41 -0
  65. minder/models/workflow.py +35 -0
  66. minder/observability/__init__.py +57 -0
  67. minder/observability/audit.py +243 -0
  68. minder/observability/logging.py +253 -0
  69. minder/observability/metrics.py +448 -0
  70. minder/observability/tracing.py +215 -0
  71. minder/presentation/__init__.py +1 -0
  72. minder/presentation/http/__init__.py +1 -0
  73. minder/presentation/http/admin/__init__.py +3 -0
  74. minder/presentation/http/admin/api.py +1309 -0
  75. minder/presentation/http/admin/context.py +94 -0
  76. minder/presentation/http/admin/dashboard.py +111 -0
  77. minder/presentation/http/admin/jobs.py +208 -0
  78. minder/presentation/http/admin/memories.py +185 -0
  79. minder/presentation/http/admin/prompts.py +219 -0
  80. minder/presentation/http/admin/routes.py +127 -0
  81. minder/presentation/http/admin/runtime.py +650 -0
  82. minder/presentation/http/admin/search.py +368 -0
  83. minder/presentation/http/admin/skills.py +230 -0
  84. minder/prompts/__init__.py +646 -0
  85. minder/prompts/formatter.py +142 -0
  86. minder/resources/__init__.py +318 -0
  87. minder/retrieval/__init__.py +5 -0
  88. minder/retrieval/hybrid.py +178 -0
  89. minder/retrieval/mmr.py +116 -0
  90. minder/retrieval/multi_hop.py +115 -0
  91. minder/runtime.py +15 -0
  92. minder/server.py +145 -0
  93. minder/store/__init__.py +64 -0
  94. minder/store/document.py +115 -0
  95. minder/store/error.py +82 -0
  96. minder/store/feedback.py +114 -0
  97. minder/store/graph.py +588 -0
  98. minder/store/history.py +57 -0
  99. minder/store/interfaces.py +512 -0
  100. minder/store/milvus/__init__.py +11 -0
  101. minder/store/milvus/client.py +26 -0
  102. minder/store/milvus/collections.py +15 -0
  103. minder/store/milvus/vector_store.py +232 -0
  104. minder/store/mongodb/__init__.py +11 -0
  105. minder/store/mongodb/client.py +49 -0
  106. minder/store/mongodb/indexes.py +90 -0
  107. minder/store/mongodb/operational_store.py +993 -0
  108. minder/store/relational.py +1087 -0
  109. minder/store/repo_state.py +58 -0
  110. minder/store/rule.py +93 -0
  111. minder/store/vector.py +79 -0
  112. minder/tools/__init__.py +47 -0
  113. minder/tools/auth.py +94 -0
  114. minder/tools/graph.py +839 -0
  115. minder/tools/ingest.py +353 -0
  116. minder/tools/memory.py +381 -0
  117. minder/tools/query.py +307 -0
  118. minder/tools/registry.py +269 -0
  119. minder/tools/repo_scanner.py +1266 -0
  120. minder/tools/search.py +15 -0
  121. minder/tools/session.py +316 -0
  122. minder/tools/skills.py +899 -0
  123. minder/tools/workflow.py +215 -0
  124. minder/transport/__init__.py +4 -0
  125. minder/transport/base.py +286 -0
  126. minder/transport/sse.py +252 -0
  127. minder/transport/stdio.py +29 -0
  128. minder_cli-0.2.0.dist-info/METADATA +318 -0
  129. minder_cli-0.2.0.dist-info/RECORD +132 -0
  130. minder_cli-0.2.0.dist-info/WHEEL +4 -0
  131. minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
  132. minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ class RepoStateStore:
9
+ def __init__(self, state_dir_name: str = ".minder") -> None:
10
+ self._state_dir_name = state_dir_name
11
+
12
+ async def read_all(self, repo_path: str) -> dict[str, Any]:
13
+ state_dir = self._ensure_state_dir(repo_path)
14
+ return {
15
+ "workflow": self._read_json(state_dir / "workflow.json", default={}),
16
+ "context": self._read_json(state_dir / "context.json", default={}),
17
+ "relationships": self._read_json(state_dir / "relationships.json", default={}),
18
+ "artifacts": self._read_artifacts(state_dir / "artifacts"),
19
+ }
20
+
21
+ async def write_workflow_state(self, repo_path: str, payload: dict[str, Any]) -> None:
22
+ self._write_json(self._ensure_state_dir(repo_path) / "workflow.json", payload)
23
+
24
+ async def write_context(self, repo_path: str, payload: dict[str, Any]) -> None:
25
+ self._write_json(self._ensure_state_dir(repo_path) / "context.json", payload)
26
+
27
+ async def write_relationships(self, repo_path: str, payload: dict[str, Any]) -> None:
28
+ self._write_json(self._ensure_state_dir(repo_path) / "relationships.json", payload)
29
+
30
+ async def write_artifact(self, repo_path: str, name: str, content: str) -> None:
31
+ artifacts_dir = self._ensure_state_dir(repo_path) / "artifacts"
32
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
33
+ (artifacts_dir / name).write_text(content, encoding="utf-8")
34
+
35
+ def _ensure_state_dir(self, repo_path: str) -> Path:
36
+ state_dir = Path(repo_path) / self._state_dir_name
37
+ state_dir.mkdir(parents=True, exist_ok=True)
38
+ return state_dir
39
+
40
+ @staticmethod
41
+ def _read_json(path: Path, *, default: dict[str, Any]) -> dict[str, Any]:
42
+ if not path.exists():
43
+ return default
44
+ return json.loads(path.read_text(encoding="utf-8"))
45
+
46
+ @staticmethod
47
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
48
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
49
+
50
+ @staticmethod
51
+ def _read_artifacts(path: Path) -> dict[str, str]:
52
+ if not path.exists():
53
+ return {}
54
+ return {
55
+ item.name: item.read_text(encoding="utf-8")
56
+ for item in sorted(path.iterdir())
57
+ if item.is_file()
58
+ }
minder/store/rule.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ RuleStore — async SQLAlchemy CRUD for the Rule domain model.
3
+
4
+ Supports filtering by scope and active flag.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from contextlib import asynccontextmanager
11
+ from typing import AsyncGenerator
12
+
13
+ from sqlalchemy import delete, select, update
14
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
15
+
16
+ from minder.models import Base, Rule
17
+
18
+
19
+ class RuleStore:
20
+ """Async store for :class:`~minder.models.Rule` entities."""
21
+
22
+ def __init__(self, db_url: str, echo: bool = False) -> None:
23
+ self._engine = create_async_engine(db_url, echo=echo)
24
+ self._session_factory = async_sessionmaker(
25
+ self._engine,
26
+ expire_on_commit=False,
27
+ class_=AsyncSession,
28
+ )
29
+
30
+ # ------------------------------------------------------------------
31
+ # Lifecycle
32
+ # ------------------------------------------------------------------
33
+
34
+ async def init_db(self) -> None:
35
+ async with self._engine.begin() as conn:
36
+ await conn.run_sync(Base.metadata.create_all)
37
+
38
+ async def dispose(self) -> None:
39
+ await self._engine.dispose()
40
+
41
+ @asynccontextmanager
42
+ async def _session(self) -> AsyncGenerator[AsyncSession, None]:
43
+ async with self._session_factory() as sess:
44
+ try:
45
+ yield sess
46
+ await sess.commit()
47
+ except Exception:
48
+ await sess.rollback()
49
+ raise
50
+
51
+ # ------------------------------------------------------------------
52
+ # CRUD
53
+ # ------------------------------------------------------------------
54
+
55
+ async def create_rule(self, **kwargs) -> Rule:
56
+ async with self._session() as sess:
57
+ rule = Rule(**kwargs)
58
+ sess.add(rule)
59
+ await sess.flush()
60
+ await sess.refresh(rule)
61
+ return rule
62
+
63
+ async def get_rule_by_id(self, rule_id: uuid.UUID) -> Rule | None:
64
+ async with self._session() as sess:
65
+ result = await sess.execute(select(Rule).where(Rule.id == rule_id))
66
+ return result.scalar_one_or_none()
67
+
68
+ async def list_rules(self) -> list[Rule]:
69
+ async with self._session() as sess:
70
+ result = await sess.execute(select(Rule))
71
+ return list(result.scalars().all())
72
+
73
+ async def list_by_scope(self, scope: str) -> list[Rule]:
74
+ """Return all rules matching the given scope."""
75
+ async with self._session() as sess:
76
+ result = await sess.execute(select(Rule).where(Rule.scope == scope))
77
+ return list(result.scalars().all())
78
+
79
+ async def list_active(self) -> list[Rule]:
80
+ """Return all active rules (active=True)."""
81
+ async with self._session() as sess:
82
+ result = await sess.execute(select(Rule).where(Rule.active.is_(True)))
83
+ return list(result.scalars().all())
84
+
85
+ async def update_rule(self, rule_id: uuid.UUID, **kwargs) -> Rule | None:
86
+ async with self._session() as sess:
87
+ await sess.execute(update(Rule).where(Rule.id == rule_id).values(**kwargs))
88
+ result = await sess.execute(select(Rule).where(Rule.id == rule_id))
89
+ return result.scalar_one_or_none()
90
+
91
+ async def delete_rule(self, rule_id: uuid.UUID) -> None:
92
+ async with self._session() as sess:
93
+ await sess.execute(delete(Rule).where(Rule.id == rule_id))
minder/store/vector.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import uuid
5
+ from typing import Any
6
+
7
+ from minder.store.interfaces import IVectorStore, IDocumentRepository, IErrorRepository
8
+
9
+
10
+ class VectorStore(IVectorStore):
11
+ def __init__(self, document_store: IDocumentRepository, error_store: IErrorRepository) -> None:
12
+ self._document_store = document_store
13
+ self._error_store = error_store
14
+
15
+ async def upsert_document(
16
+ self,
17
+ doc_id: uuid.UUID,
18
+ embedding: list[float],
19
+ payload: dict[str, Any],
20
+ ) -> None:
21
+ """Not supported in Relational VectorStore."""
22
+ pass
23
+
24
+ async def delete_documents(self, doc_ids: list[uuid.UUID]) -> None:
25
+ """Not supported in Relational VectorStore."""
26
+ pass
27
+
28
+ async def setup(self) -> None:
29
+ """No setup needed for Relational VectorStore."""
30
+ pass
31
+
32
+ async def search_documents(
33
+ self,
34
+ query_embedding: list[float],
35
+ *,
36
+ project: str | None = None,
37
+ doc_types: set[str] | None = None,
38
+ limit: int = 5,
39
+ score_threshold: float = 0.0,
40
+ ) -> list[dict[str, Any]]:
41
+ docs = await self._document_store.list_documents(project=project)
42
+ ranked: list[dict[str, Any]] = []
43
+ for doc in docs:
44
+ if doc_types is not None and doc.doc_type not in doc_types:
45
+ continue
46
+ embedding = doc.embedding if isinstance(doc.embedding, list) else None
47
+ if not embedding:
48
+ continue
49
+ score = self._cosine_similarity(query_embedding, embedding)
50
+ if score < score_threshold:
51
+ continue
52
+ ranked.append(
53
+ {
54
+ "id": doc.id,
55
+ "title": doc.title,
56
+ "path": doc.source_path,
57
+ "content": doc.content,
58
+ "score": round(score, 4),
59
+ "doc_type": doc.doc_type,
60
+ }
61
+ )
62
+ ranked.sort(key=lambda item: float(item["score"]), reverse=True)
63
+ return ranked[:limit]
64
+
65
+ async def search_errors(
66
+ self, query: str, *, limit: int = 5
67
+ ) -> list[dict[str, Any]]:
68
+ return await self._error_store.search_errors(query, limit=limit)
69
+
70
+ @staticmethod
71
+ def _cosine_similarity(left: list[float], right: list[float]) -> float:
72
+ if not left or not right or len(left) != len(right):
73
+ return 0.0
74
+ numerator = sum(a * b for a, b in zip(left, right, strict=False))
75
+ left_norm = math.sqrt(sum(value * value for value in left))
76
+ right_norm = math.sqrt(sum(value * value for value in right))
77
+ if left_norm == 0 or right_norm == 0:
78
+ return 0.0
79
+ return numerator / (left_norm * right_norm)
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import import_module
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from .auth import AuthTools
8
+ from .ingest import IngestTools
9
+ from .memory import MemoryTools
10
+ from .query import QueryTools
11
+ from .repo_scanner import RepoScanner
12
+ from .search import SearchTools
13
+ from .session import SessionTools
14
+ from .workflow import WorkflowTools
15
+
16
+ __all__ = [
17
+ "AuthTools",
18
+ "IngestTools",
19
+ "MemoryTools",
20
+ "QueryTools",
21
+ "RepoScanner",
22
+ "SearchTools",
23
+ "SessionTools",
24
+ "WorkflowTools",
25
+ ]
26
+
27
+ _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
28
+ "AuthTools": (".auth", "AuthTools"),
29
+ "IngestTools": (".ingest", "IngestTools"),
30
+ "MemoryTools": (".memory", "MemoryTools"),
31
+ "QueryTools": (".query", "QueryTools"),
32
+ "RepoScanner": (".repo_scanner", "RepoScanner"),
33
+ "SearchTools": (".search", "SearchTools"),
34
+ "SessionTools": (".session", "SessionTools"),
35
+ "WorkflowTools": (".workflow", "WorkflowTools"),
36
+ }
37
+
38
+
39
+ def __getattr__(name: str) -> Any:
40
+ module_spec = _LAZY_EXPORTS.get(name)
41
+ if module_spec is None:
42
+ raise AttributeError(name)
43
+ module_name, symbol_name = module_spec
44
+ module = import_module(module_name, __name__)
45
+ value = getattr(module, symbol_name)
46
+ globals()[name] = value
47
+ return value
minder/tools/auth.py ADDED
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+
5
+ from minder.auth.service import AuthService
6
+ from minder.store.interfaces import IOperationalStore
7
+
8
+
9
+ class AuthTools:
10
+ def __init__(self, store: IOperationalStore, auth_service: AuthService) -> None:
11
+ self._store = store
12
+ self._auth = auth_service
13
+
14
+ async def minder_auth_login(self, api_key: str) -> dict[str, str]:
15
+ user = await self._auth.authenticate_api_key(api_key)
16
+ return {"token": self._auth.issue_jwt(user), "user_id": str(user.id)}
17
+
18
+ async def minder_auth_exchange_client_key(
19
+ self,
20
+ client_api_key: str,
21
+ *,
22
+ requested_scopes: list[str] | None = None,
23
+ ) -> dict[str, object]:
24
+ return await self._auth.exchange_client_api_key(
25
+ client_api_key,
26
+ requested_scopes=requested_scopes,
27
+ )
28
+
29
+ async def minder_auth_whoami(self, token: str) -> dict[str, str]:
30
+ user = await self._auth.get_user_from_jwt(token)
31
+ return {
32
+ "user_id": str(user.id),
33
+ "email": user.email,
34
+ "username": user.username,
35
+ "role": user.role,
36
+ }
37
+
38
+ async def minder_auth_manage(
39
+ self,
40
+ *,
41
+ actor_user_id: uuid.UUID,
42
+ action: str,
43
+ ) -> dict[str, object]:
44
+ actor = await self._store.get_user_by_id(actor_user_id)
45
+ if actor is None or actor.role != "admin":
46
+ raise PermissionError("Admin role required")
47
+ if action == "list_users":
48
+ users = await self._store.list_users(active_only=False)
49
+ return {
50
+ "users": [
51
+ {
52
+ "id": str(user.id),
53
+ "email": user.email,
54
+ "username": user.username,
55
+ "role": user.role,
56
+ "is_active": user.is_active,
57
+ }
58
+ for user in users
59
+ ]
60
+ }
61
+ raise ValueError(f"Unsupported auth manage action: {action}")
62
+
63
+ async def minder_auth_create_client(
64
+ self,
65
+ *,
66
+ actor_user_id: uuid.UUID,
67
+ name: str,
68
+ slug: str,
69
+ description: str = "",
70
+ tool_scopes: list[str] | None = None,
71
+ repo_scopes: list[str] | None = None,
72
+ ) -> dict[str, object]:
73
+ actor = await self._store.get_user_by_id(actor_user_id)
74
+ if actor is None or actor.role != "admin":
75
+ raise PermissionError("Admin role required")
76
+ client, client_api_key = await self._auth.register_client(
77
+ name=name,
78
+ slug=slug,
79
+ description=description,
80
+ created_by_user_id=actor_user_id,
81
+ tool_scopes=tool_scopes,
82
+ repo_scopes=repo_scopes,
83
+ )
84
+ return {
85
+ "client": {
86
+ "id": str(client.id),
87
+ "name": client.name,
88
+ "slug": client.slug,
89
+ "status": client.status,
90
+ "tool_scopes": list(client.tool_scopes),
91
+ "repo_scopes": list(client.repo_scopes),
92
+ },
93
+ "client_api_key": client_api_key,
94
+ }