universal-memory 0.1.0__tar.gz → 0.1.2__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.
- {universal_memory-0.1.0 → universal_memory-0.1.2}/PKG-INFO +26 -7
- {universal_memory-0.1.0 → universal_memory-0.1.2}/README.md +16 -6
- {universal_memory-0.1.0 → universal_memory-0.1.2}/pyproject.toml +36 -1
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/__init__.py +1 -1
- universal_memory-0.1.2/src/universal_memory/application/diagnostics/__init__.py +15 -0
- universal_memory-0.1.2/src/universal_memory/application/diagnostics/doctor_use_case.py +365 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/host/setup_host_use_case.py +6 -1
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/get_memory_status_use_case.py +2 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/bootstrap/cli.py +3 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/bootstrap/mcp.py +3 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/cli/init_command.py +104 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/mcp/server.py +27 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/__main__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/host/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/host/drift_detector.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/host/sync_instructions_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/assemble_context_summary_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/context_hygiene_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/list_facts_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/purge_fact_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/remember_fact_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/memory/search_facts_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/onboarding/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/onboarding/setup_project.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/security/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/security/list_audit_log_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/security/list_snapshots_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/security/rollback_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/security/safe_write_use_case.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/skills/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/skills/generate_skill.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/skills/list_skills.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/skills/native_skill_sync.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/skills/propose_skill.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/skills/track_latent_skill.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/skills/update_skill.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/update/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/update/update_use_cases.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/bootstrap/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/audit_event.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/base.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/context_summary.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/fact.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/host.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/instruction_target.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/latent_skill.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/rule.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/runtime.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/safe_write_result.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/snapshot.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/exceptions.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/audit_log_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/config_validation_port.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/context_summary_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/fact_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/latent_skill_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/project_layout_port.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/rule_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/secret_scanner_port.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/snapshot_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/project_layout.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/config/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/config/adapters.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/config/project_layout.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/config/toml_loader.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/security/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/security/entropy_secret_scanner.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/security/local_audit_log_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/security/local_snapshot_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/storage/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/storage/local_context_summary_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/storage/local_fact_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/storage/local_latent_skill_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/storage/local_rule_repository.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/cli/__init__.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/cli/message_catalog.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/errors.py +0 -0
- {universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/mcp/__init__.py +0 -0
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: universal-memory
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Vendor-agnostic cognitive persistence layer for AI agents.
|
|
5
|
+
Keywords: agent-memory,ai,ai-agents,agent-skills,claude-code,codex,context-engineering,developer-tools,llm,mcp,memory
|
|
5
6
|
Author: Yan L. Amorelli
|
|
6
7
|
Author-email: Yan L. Amorelli <dev@amorelliaoyan.com>
|
|
7
8
|
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
8
17
|
Requires-Dist: fastmcp>=0.1.0
|
|
9
18
|
Requires-Dist: pydantic>=2.0
|
|
10
19
|
Requires-Dist: rich>=15.0.0
|
|
@@ -15,11 +24,15 @@ Project-URL: Homepage, https://github.com/YanAmorelli/universal-memory
|
|
|
15
24
|
Project-URL: Repository, https://github.com/YanAmorelli/universal-memory
|
|
16
25
|
Description-Content-Type: text/markdown
|
|
17
26
|
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="assets/umem-logo.png" alt="Universal Memory logo" width="720">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
18
31
|
# Universal Memory (umem)
|
|
19
32
|
|
|
20
33
|
[](https://pypi.org/project/universal-memory/)
|
|
21
|
-
[](https://
|
|
22
|
-
[](https://
|
|
34
|
+
[](https://pypi.org/project/universal-memory/)
|
|
35
|
+
[](https://github.com/YanAmorelli/universal-memory/blob/dev/LICENSE)
|
|
23
36
|
|
|
24
37
|
A vendor-agnostic cognitive persistence layer for AI agents. Eliminate the "repetition tax" by transporting your context, preferences, guidelines, and history seamlessly across sessions, IDEs, and LLM models.
|
|
25
38
|
|
|
@@ -27,7 +40,7 @@ To see the core idea visually, check out the [Excalidraw design](https://excalid
|
|
|
27
40
|
|
|
28
41
|

|
|
29
42
|
|
|
30
|
-
### Diagram Breakdown
|
|
43
|
+
### Diagram Breakdown
|
|
31
44
|
|
|
32
45
|
* **Short-Term Memory (Ephemeral):** Project-specific (folder-level) memories. A simple summary of recent changes, pending tasks, and project or task-level constraints.
|
|
33
46
|
* **Agents Behaviours:** Comports the user's expected agent behaviors. Instead of requesting the same settings in every session, the agent understands the user by their traits, thoughts, and any context key to enhancing the overall experience. This encompasses:
|
|
@@ -72,12 +85,18 @@ Encapsulates complex, repetitive procedural instructions into formal Agent Skill
|
|
|
72
85
|
|
|
73
86
|
Ensure you have Python 3.12+ installed. You can run or install `umem` using your preferred package manager.
|
|
74
87
|
|
|
75
|
-
###
|
|
76
|
-
You
|
|
88
|
+
### Try instantly with `uvx`
|
|
89
|
+
You can run `umem` without installing it permanently:
|
|
77
90
|
```bash
|
|
78
|
-
uvx umem --help
|
|
91
|
+
uvx --from universal-memory umem --help
|
|
79
92
|
```
|
|
80
93
|
|
|
94
|
+
> [!WARNING]
|
|
95
|
+
> `uvx` is best for quick trials. For ongoing use, install Universal Memory as a persistent tool so `umem` is always available and can fully manage long-lived global memories and synced agent skills:
|
|
96
|
+
> ```bash
|
|
97
|
+
> uv tool install universal-memory
|
|
98
|
+
> ```
|
|
99
|
+
|
|
81
100
|
### Install via PyPI
|
|
82
101
|
```bash
|
|
83
102
|
pip install universal-memory
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/umem-logo.png" alt="Universal Memory logo" width="720">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# Universal Memory (umem)
|
|
2
6
|
|
|
3
7
|
[](https://pypi.org/project/universal-memory/)
|
|
4
|
-
[](https://
|
|
5
|
-
[](https://
|
|
8
|
+
[](https://pypi.org/project/universal-memory/)
|
|
9
|
+
[](https://github.com/YanAmorelli/universal-memory/blob/dev/LICENSE)
|
|
6
10
|
|
|
7
11
|
A vendor-agnostic cognitive persistence layer for AI agents. Eliminate the "repetition tax" by transporting your context, preferences, guidelines, and history seamlessly across sessions, IDEs, and LLM models.
|
|
8
12
|
|
|
@@ -10,7 +14,7 @@ To see the core idea visually, check out the [Excalidraw design](https://excalid
|
|
|
10
14
|
|
|
11
15
|

|
|
12
16
|
|
|
13
|
-
### Diagram Breakdown
|
|
17
|
+
### Diagram Breakdown
|
|
14
18
|
|
|
15
19
|
* **Short-Term Memory (Ephemeral):** Project-specific (folder-level) memories. A simple summary of recent changes, pending tasks, and project or task-level constraints.
|
|
16
20
|
* **Agents Behaviours:** Comports the user's expected agent behaviors. Instead of requesting the same settings in every session, the agent understands the user by their traits, thoughts, and any context key to enhancing the overall experience. This encompasses:
|
|
@@ -55,12 +59,18 @@ Encapsulates complex, repetitive procedural instructions into formal Agent Skill
|
|
|
55
59
|
|
|
56
60
|
Ensure you have Python 3.12+ installed. You can run or install `umem` using your preferred package manager.
|
|
57
61
|
|
|
58
|
-
###
|
|
59
|
-
You
|
|
62
|
+
### Try instantly with `uvx`
|
|
63
|
+
You can run `umem` without installing it permanently:
|
|
60
64
|
```bash
|
|
61
|
-
uvx umem --help
|
|
65
|
+
uvx --from universal-memory umem --help
|
|
62
66
|
```
|
|
63
67
|
|
|
68
|
+
> [!WARNING]
|
|
69
|
+
> `uvx` is best for quick trials. For ongoing use, install Universal Memory as a persistent tool so `umem` is always available and can fully manage long-lived global memories and synced agent skills:
|
|
70
|
+
> ```bash
|
|
71
|
+
> uv tool install universal-memory
|
|
72
|
+
> ```
|
|
73
|
+
|
|
64
74
|
### Install via PyPI
|
|
65
75
|
```bash
|
|
66
76
|
pip install universal-memory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "universal-memory"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Vendor-agnostic cognitive persistence layer for AI agents."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -8,6 +8,29 @@ authors = [
|
|
|
8
8
|
{ name = "Yan L. Amorelli", email = "dev@amorelliaoyan.com" }
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
|
+
keywords = [
|
|
12
|
+
"agent-memory",
|
|
13
|
+
"ai",
|
|
14
|
+
"ai-agents",
|
|
15
|
+
"agent-skills",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"codex",
|
|
18
|
+
"context-engineering",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"llm",
|
|
21
|
+
"mcp",
|
|
22
|
+
"memory",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
"Topic :: Utilities",
|
|
33
|
+
]
|
|
11
34
|
dependencies = [
|
|
12
35
|
"fastmcp>=0.1.0",
|
|
13
36
|
"pydantic>=2.0",
|
|
@@ -44,6 +67,18 @@ line-length = 100
|
|
|
44
67
|
select = ["E", "F", "I", "N", "UP", "PL", "RUF", "B", "S"]
|
|
45
68
|
|
|
46
69
|
[tool.ruff.lint.per-file-ignores]
|
|
70
|
+
"scripts/alpha_tester.py" = [
|
|
71
|
+
"E501",
|
|
72
|
+
"F841",
|
|
73
|
+
"PLR0912",
|
|
74
|
+
"PLR0915",
|
|
75
|
+
"PLW1510",
|
|
76
|
+
"RUF005",
|
|
77
|
+
"RUF059",
|
|
78
|
+
"S101",
|
|
79
|
+
"S603",
|
|
80
|
+
"S607",
|
|
81
|
+
]
|
|
47
82
|
"tests/**/*.py" = ["S101"]
|
|
48
83
|
"src/universal_memory/interfaces/cli/init_command.py" = ["E501"]
|
|
49
84
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from universal_memory.application.diagnostics.doctor_use_case import (
|
|
2
|
+
DoctorCheck,
|
|
3
|
+
DoctorCommand,
|
|
4
|
+
DoctorResult,
|
|
5
|
+
DoctorSummary,
|
|
6
|
+
DoctorUseCase,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DoctorCheck",
|
|
11
|
+
"DoctorCommand",
|
|
12
|
+
"DoctorResult",
|
|
13
|
+
"DoctorSummary",
|
|
14
|
+
"DoctorUseCase",
|
|
15
|
+
]
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from universal_memory.application.host import ConfigureHostCommand, ConfigureHostResult
|
|
11
|
+
from universal_memory.infrastructure.config.project_layout import (
|
|
12
|
+
DIRECTORY_LAYOUT_PATHS,
|
|
13
|
+
PROJECT_LAYOUT_PATHS,
|
|
14
|
+
)
|
|
15
|
+
from universal_memory.infrastructure.config.toml_loader import load_config
|
|
16
|
+
|
|
17
|
+
CheckStatus = str
|
|
18
|
+
HostCheckCommand = Callable[[ConfigureHostCommand], ConfigureHostResult]
|
|
19
|
+
WhichResolver = Callable[[str], str | None]
|
|
20
|
+
CONFIG_LOAD_ERROR_PREFIX = "__config_load_error__:"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class DoctorCommand:
|
|
25
|
+
project_root: Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class DoctorCheck:
|
|
30
|
+
name: str
|
|
31
|
+
status: CheckStatus
|
|
32
|
+
detail: str | None = None
|
|
33
|
+
error: str | None = None
|
|
34
|
+
recovery_hint: str | None = None
|
|
35
|
+
|
|
36
|
+
def to_payload(self) -> dict[str, str]:
|
|
37
|
+
payload = {"name": self.name, "status": self.status}
|
|
38
|
+
if self.detail is not None:
|
|
39
|
+
payload["detail"] = self.detail
|
|
40
|
+
if self.error is not None:
|
|
41
|
+
payload["error"] = self.error
|
|
42
|
+
if self.recovery_hint is not None:
|
|
43
|
+
payload["recovery_hint"] = self.recovery_hint
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class DoctorSummary:
|
|
49
|
+
total_checks: int
|
|
50
|
+
passed: int
|
|
51
|
+
failed: int
|
|
52
|
+
|
|
53
|
+
def to_payload(self) -> dict[str, int]:
|
|
54
|
+
return {
|
|
55
|
+
"total_checks": self.total_checks,
|
|
56
|
+
"passed": self.passed,
|
|
57
|
+
"failed": self.failed,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class DoctorResult:
|
|
63
|
+
checks: list[DoctorCheck]
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def ok(self) -> bool:
|
|
67
|
+
return all(check.status == "success" for check in self.checks)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def summary(self) -> DoctorSummary:
|
|
71
|
+
passed = sum(1 for check in self.checks if check.status == "success")
|
|
72
|
+
failed = len(self.checks) - passed
|
|
73
|
+
return DoctorSummary(total_checks=len(self.checks), passed=passed, failed=failed)
|
|
74
|
+
|
|
75
|
+
def to_payload(self) -> dict[str, object]:
|
|
76
|
+
return {
|
|
77
|
+
"checks": [check.to_payload() for check in self.checks],
|
|
78
|
+
"summary": self.summary.to_payload(),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DoctorUseCase:
|
|
83
|
+
def __init__( # noqa: PLR0913
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
host_check_command: HostCheckCommand | None = None,
|
|
87
|
+
which: WhichResolver = shutil.which,
|
|
88
|
+
version_info: tuple[int, int, int] | None = None,
|
|
89
|
+
home: Path | None = None,
|
|
90
|
+
xdg_data_home: Path | None = None,
|
|
91
|
+
xdg_config_home: Path | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
self.host_check_command = host_check_command
|
|
94
|
+
self.which = which
|
|
95
|
+
self.version_info = version_info or (
|
|
96
|
+
sys.version_info.major,
|
|
97
|
+
sys.version_info.minor,
|
|
98
|
+
sys.version_info.micro,
|
|
99
|
+
)
|
|
100
|
+
self.home = home or Path.home()
|
|
101
|
+
self.xdg_data_home = xdg_data_home
|
|
102
|
+
self.xdg_config_home = xdg_config_home
|
|
103
|
+
|
|
104
|
+
def execute(self, command: DoctorCommand) -> DoctorResult:
|
|
105
|
+
project_root = command.project_root.resolve()
|
|
106
|
+
checks = [
|
|
107
|
+
self._python_version_check(),
|
|
108
|
+
self._path_permissions_check(project_root),
|
|
109
|
+
self._project_layout_check(project_root),
|
|
110
|
+
self._path_executables_check(),
|
|
111
|
+
self._hosts_integration_check(project_root),
|
|
112
|
+
]
|
|
113
|
+
return DoctorResult(checks=checks)
|
|
114
|
+
|
|
115
|
+
def _python_version_check(self) -> DoctorCheck:
|
|
116
|
+
major, minor, micro = self.version_info
|
|
117
|
+
detail = f"Python {major}.{minor}.{micro}"
|
|
118
|
+
if (major, minor) >= (3, 12):
|
|
119
|
+
return DoctorCheck(name="python_version", status="success", detail=detail)
|
|
120
|
+
return DoctorCheck(
|
|
121
|
+
name="python_version",
|
|
122
|
+
status="failed",
|
|
123
|
+
detail=detail,
|
|
124
|
+
error="Python 3.12 or higher is required.",
|
|
125
|
+
recovery_hint="Configure a virtual environment with Python 3.12 or higher.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _path_permissions_check(self, project_root: Path) -> DoctorCheck:
|
|
129
|
+
paths = [
|
|
130
|
+
(self._global_data_root(), "directory"),
|
|
131
|
+
(self._global_config_path(), "file"),
|
|
132
|
+
]
|
|
133
|
+
if (project_root / ".umem").exists():
|
|
134
|
+
paths.append((project_root / ".umem", "directory"))
|
|
135
|
+
else:
|
|
136
|
+
paths.append((project_root, "directory"))
|
|
137
|
+
|
|
138
|
+
failures: list[str] = []
|
|
139
|
+
for path, expected_kind in paths:
|
|
140
|
+
failure = self._permission_failure(path, expected_kind, project_root)
|
|
141
|
+
if failure is not None:
|
|
142
|
+
failures.append(failure)
|
|
143
|
+
|
|
144
|
+
if not failures:
|
|
145
|
+
return DoctorCheck(
|
|
146
|
+
name="filesystem_permissions",
|
|
147
|
+
status="success",
|
|
148
|
+
detail="Canonical local and global paths are writable.",
|
|
149
|
+
)
|
|
150
|
+
return DoctorCheck(
|
|
151
|
+
name="filesystem_permissions",
|
|
152
|
+
status="failed",
|
|
153
|
+
error="; ".join(failures),
|
|
154
|
+
recovery_hint="Fix filesystem permissions with chmod -R u+rw <path>.",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _project_layout_check(self, project_root: Path) -> DoctorCheck:
|
|
158
|
+
umem_root = project_root / ".umem"
|
|
159
|
+
if not umem_root.exists():
|
|
160
|
+
return DoctorCheck(
|
|
161
|
+
name="project_layout",
|
|
162
|
+
status="failed",
|
|
163
|
+
error="Missing project layout: .umem",
|
|
164
|
+
recovery_hint="Run umem init --yes to create the project layout.",
|
|
165
|
+
)
|
|
166
|
+
if not umem_root.is_dir():
|
|
167
|
+
return DoctorCheck(
|
|
168
|
+
name="project_layout",
|
|
169
|
+
status="failed",
|
|
170
|
+
error="Project layout root .umem is not a directory.",
|
|
171
|
+
recovery_hint="Move the conflicting .umem file and run umem init --yes.",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
failures: list[str] = []
|
|
175
|
+
for relative_path in PROJECT_LAYOUT_PATHS:
|
|
176
|
+
target = project_root / relative_path
|
|
177
|
+
expected = "directory" if relative_path in DIRECTORY_LAYOUT_PATHS else "file"
|
|
178
|
+
if not target.exists():
|
|
179
|
+
failures.append(f"Missing path: {relative_path}")
|
|
180
|
+
elif expected == "directory" and not target.is_dir():
|
|
181
|
+
failures.append(f"Wrong path kind: {relative_path} must be a directory")
|
|
182
|
+
elif expected == "file" and not target.is_file():
|
|
183
|
+
failures.append(f"Wrong path kind: {relative_path} must be a file")
|
|
184
|
+
|
|
185
|
+
if not failures:
|
|
186
|
+
return DoctorCheck(
|
|
187
|
+
name="project_layout",
|
|
188
|
+
status="success",
|
|
189
|
+
detail="Project layout is complete.",
|
|
190
|
+
)
|
|
191
|
+
return DoctorCheck(
|
|
192
|
+
name="project_layout",
|
|
193
|
+
status="failed",
|
|
194
|
+
error="; ".join(failures),
|
|
195
|
+
recovery_hint="Run umem init --yes to rebuild missing default paths.",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _path_executables_check(self) -> DoctorCheck:
|
|
199
|
+
required = ["umem", "umem-mcp"]
|
|
200
|
+
missing = [name for name in required if self.which(name) is None]
|
|
201
|
+
if not missing:
|
|
202
|
+
return DoctorCheck(
|
|
203
|
+
name="path_executables",
|
|
204
|
+
status="success",
|
|
205
|
+
detail="umem and umem-mcp are available on PATH.",
|
|
206
|
+
)
|
|
207
|
+
return DoctorCheck(
|
|
208
|
+
name="path_executables",
|
|
209
|
+
status="failed",
|
|
210
|
+
error=f"Missing executables on PATH: {', '.join(missing)}",
|
|
211
|
+
recovery_hint="Activate the virtual environment or install with uv sync.",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _hosts_integration_check(self, project_root: Path) -> DoctorCheck:
|
|
215
|
+
host_ids = self._configured_host_ids(project_root)
|
|
216
|
+
config_errors = [
|
|
217
|
+
host_id.removeprefix(CONFIG_LOAD_ERROR_PREFIX)
|
|
218
|
+
for host_id in host_ids
|
|
219
|
+
if host_id.startswith(CONFIG_LOAD_ERROR_PREFIX)
|
|
220
|
+
]
|
|
221
|
+
if config_errors:
|
|
222
|
+
return DoctorCheck(
|
|
223
|
+
name="hosts_integration",
|
|
224
|
+
status="failed",
|
|
225
|
+
error="; ".join(config_errors),
|
|
226
|
+
recovery_hint="Fix .umem/config.toml and re-run umem doctor.",
|
|
227
|
+
)
|
|
228
|
+
if self.host_check_command is None:
|
|
229
|
+
return DoctorCheck(
|
|
230
|
+
name="hosts_integration",
|
|
231
|
+
status="success",
|
|
232
|
+
detail="Host integration check is not configured.",
|
|
233
|
+
)
|
|
234
|
+
if not host_ids:
|
|
235
|
+
return DoctorCheck(
|
|
236
|
+
name="hosts_integration",
|
|
237
|
+
status="success",
|
|
238
|
+
detail="No hosts configured.",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
failures: list[str] = []
|
|
242
|
+
for host_id in host_ids:
|
|
243
|
+
try:
|
|
244
|
+
result = self.host_check_command(
|
|
245
|
+
ConfigureHostCommand(
|
|
246
|
+
host_id=host_id,
|
|
247
|
+
apply=False,
|
|
248
|
+
check=True,
|
|
249
|
+
record_audit=False,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
except Exception as error:
|
|
253
|
+
failures.append(f"{host_id}: {error}")
|
|
254
|
+
continue
|
|
255
|
+
if result.validation_status != "success":
|
|
256
|
+
detail = ", ".join(result.warnings or result.manual_steps)
|
|
257
|
+
suffix = f" ({detail})" if detail else ""
|
|
258
|
+
failures.append(f"{host_id}: {result.validation_status}{suffix}")
|
|
259
|
+
|
|
260
|
+
if not failures:
|
|
261
|
+
return DoctorCheck(
|
|
262
|
+
name="hosts_integration",
|
|
263
|
+
status="success",
|
|
264
|
+
detail=f"Validated hosts: {', '.join(host_ids)}.",
|
|
265
|
+
)
|
|
266
|
+
return DoctorCheck(
|
|
267
|
+
name="hosts_integration",
|
|
268
|
+
status="failed",
|
|
269
|
+
error="; ".join(failures),
|
|
270
|
+
recovery_hint="Run umem host setup <host_id> --yes to repair host instructions.",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _configured_host_ids(self, project_root: Path) -> list[str]:
|
|
274
|
+
try:
|
|
275
|
+
loaded = load_config(project_root)
|
|
276
|
+
except Exception as error:
|
|
277
|
+
return [f"{CONFIG_LOAD_ERROR_PREFIX}{error}"]
|
|
278
|
+
|
|
279
|
+
host_ids: list[str] = []
|
|
280
|
+
for section_name in ("runtimes", "hosts"):
|
|
281
|
+
section = loaded.merged.get(section_name)
|
|
282
|
+
if not isinstance(section, dict):
|
|
283
|
+
continue
|
|
284
|
+
enabled = section.get("enabled")
|
|
285
|
+
if isinstance(enabled, list):
|
|
286
|
+
host_ids.extend(str(item) for item in enabled)
|
|
287
|
+
for key, value in section.items():
|
|
288
|
+
if isinstance(value, dict) and value.get("enabled") is True:
|
|
289
|
+
host_ids.append(str(key))
|
|
290
|
+
|
|
291
|
+
supported = {"codex", "claude_code"}
|
|
292
|
+
deduped: list[str] = []
|
|
293
|
+
for host_id in host_ids:
|
|
294
|
+
if host_id in supported and host_id not in deduped:
|
|
295
|
+
deduped.append(host_id)
|
|
296
|
+
return deduped
|
|
297
|
+
|
|
298
|
+
def _global_data_root(self) -> Path:
|
|
299
|
+
configured = self.xdg_data_home or _env_path("XDG_DATA_HOME")
|
|
300
|
+
return (configured if configured is not None else self.home / ".local" / "share") / "umem"
|
|
301
|
+
|
|
302
|
+
def _global_config_path(self) -> Path:
|
|
303
|
+
configured = self.xdg_config_home or _env_path("XDG_CONFIG_HOME")
|
|
304
|
+
root = configured if configured is not None else self.home / ".config"
|
|
305
|
+
return root / "umem" / "config.toml"
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _relative_or_user_path(path: Path, project_root: Path) -> str:
|
|
309
|
+
try:
|
|
310
|
+
return path.resolve().relative_to(project_root.resolve()).as_posix()
|
|
311
|
+
except ValueError:
|
|
312
|
+
return path.expanduser().as_posix()
|
|
313
|
+
|
|
314
|
+
def _permission_failure(
|
|
315
|
+
self,
|
|
316
|
+
path: Path,
|
|
317
|
+
expected_kind: str,
|
|
318
|
+
project_root: Path,
|
|
319
|
+
) -> str | None:
|
|
320
|
+
display_path = self._relative_or_user_path(path, project_root)
|
|
321
|
+
if path.exists():
|
|
322
|
+
return self._existing_path_permission_failure(path, expected_kind, display_path)
|
|
323
|
+
|
|
324
|
+
return self._missing_path_permission_failure(path, display_path)
|
|
325
|
+
|
|
326
|
+
@staticmethod
|
|
327
|
+
def _existing_path_permission_failure(
|
|
328
|
+
path: Path,
|
|
329
|
+
expected_kind: str,
|
|
330
|
+
display_path: str,
|
|
331
|
+
) -> str | None:
|
|
332
|
+
if expected_kind == "directory":
|
|
333
|
+
if not path.is_dir():
|
|
334
|
+
return f"{display_path}: path must be a directory"
|
|
335
|
+
has_permissions = os.access(path, os.R_OK | os.W_OK | os.X_OK)
|
|
336
|
+
return (
|
|
337
|
+
None
|
|
338
|
+
if has_permissions
|
|
339
|
+
else f"{display_path}: directory is not readable, writable, and searchable"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if not path.is_file():
|
|
343
|
+
return f"{display_path}: path must be a file"
|
|
344
|
+
|
|
345
|
+
has_permissions = os.access(path, os.R_OK | os.W_OK)
|
|
346
|
+
return None if has_permissions else f"{display_path}: file is not readable and writable"
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _missing_path_permission_failure(path: Path, display_path: str) -> str | None:
|
|
350
|
+
parent = path.parent
|
|
351
|
+
parent_has_permissions = (
|
|
352
|
+
parent.exists() and parent.is_dir() and os.access(parent, os.R_OK | os.W_OK | os.X_OK)
|
|
353
|
+
)
|
|
354
|
+
if parent_has_permissions:
|
|
355
|
+
return None
|
|
356
|
+
if not parent.exists():
|
|
357
|
+
return f"{display_path}: parent directory is missing"
|
|
358
|
+
return f"{display_path}: parent directory is not readable, writable, and searchable"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _env_path(name: str) -> Path | None:
|
|
362
|
+
value = os.environ.get(name)
|
|
363
|
+
if not value:
|
|
364
|
+
return None
|
|
365
|
+
return Path(value)
|
|
@@ -114,6 +114,7 @@ class ConfigureHostCommand:
|
|
|
114
114
|
host_id: str
|
|
115
115
|
apply: bool = False
|
|
116
116
|
check: bool = False
|
|
117
|
+
record_audit: bool = True
|
|
117
118
|
instruction_blocks: list[InstructionBlock] = field(default_factory=list)
|
|
118
119
|
shared_manifest_available: bool | None = None
|
|
119
120
|
max_managed_lines: int = DEFAULT_MAX_MANAGED_LINES
|
|
@@ -244,7 +245,11 @@ class ConfigureHostUseCase:
|
|
|
244
245
|
max_lines=command.max_managed_lines,
|
|
245
246
|
max_chars=command.max_managed_chars,
|
|
246
247
|
)
|
|
247
|
-
audit_reference =
|
|
248
|
+
audit_reference = (
|
|
249
|
+
self._record_host_validation(command, host, validation)
|
|
250
|
+
if command.record_audit
|
|
251
|
+
else "not-recorded"
|
|
252
|
+
)
|
|
248
253
|
return ConfigureHostResult(
|
|
249
254
|
host_id=host.name.value,
|
|
250
255
|
instruction_targets=[target.name.value],
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from datetime import UTC, datetime
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from universal_memory import __version__
|
|
10
11
|
from universal_memory.domain.entities import (
|
|
11
12
|
AuditEventScope,
|
|
12
13
|
FactScope,
|
|
@@ -40,6 +41,7 @@ class GetMemoryStatusResult:
|
|
|
40
41
|
last_health_check: str | None
|
|
41
42
|
host_validation: dict[str, dict[str, str | None]]
|
|
42
43
|
recommended_action: str | None = None
|
|
44
|
+
installed_version: str = __version__
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
class GetMemoryStatusUseCase:
|
|
@@ -2,6 +2,7 @@ from collections.abc import Sequence
|
|
|
2
2
|
from datetime import UTC, datetime
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from universal_memory.application.diagnostics import DoctorUseCase
|
|
5
6
|
from universal_memory.application.host import (
|
|
6
7
|
ConfigureHostUseCase,
|
|
7
8
|
SyncInstructionsUseCase,
|
|
@@ -176,6 +177,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
176
177
|
safe_write_use_case=safe_write_use_case,
|
|
177
178
|
fact_repository=fact_repository,
|
|
178
179
|
)
|
|
180
|
+
doctor_use_case = DoctorUseCase(host_check_command=host_use_case.execute)
|
|
179
181
|
host_sync_use_case = SyncInstructionsUseCase(
|
|
180
182
|
project_root=project_root,
|
|
181
183
|
safe_write_use_case=safe_write_use_case,
|
|
@@ -262,6 +264,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
262
264
|
rollback_command=rollback_use_case.execute,
|
|
263
265
|
rollback_preview_command=rollback_preview,
|
|
264
266
|
status_command=status_use_case.execute,
|
|
267
|
+
doctor_command=doctor_use_case.execute,
|
|
265
268
|
context_command=context_use_case.execute,
|
|
266
269
|
remember_command=remember_use_case.execute,
|
|
267
270
|
facts_list_command=facts_list_use_case.execute,
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from fastmcp import FastMCP
|
|
6
6
|
|
|
7
|
+
from universal_memory.application.diagnostics import DoctorUseCase
|
|
7
8
|
from universal_memory.application.host import (
|
|
8
9
|
ConfigureHostUseCase,
|
|
9
10
|
SyncInstructionsUseCase,
|
|
@@ -122,6 +123,7 @@ def build_server(project_root: Path | None = None) -> FastMCP:
|
|
|
122
123
|
safe_write_use_case=safe_write_use_case,
|
|
123
124
|
fact_repository=fact_repository,
|
|
124
125
|
)
|
|
126
|
+
doctor_use_case = DoctorUseCase(host_check_command=host_use_case.execute)
|
|
125
127
|
host_sync_use_case = SyncInstructionsUseCase(
|
|
126
128
|
project_root=root,
|
|
127
129
|
safe_write_use_case=safe_write_use_case,
|
|
@@ -182,6 +184,7 @@ def build_server(project_root: Path | None = None) -> FastMCP:
|
|
|
182
184
|
MCPUseCases(
|
|
183
185
|
initialize_project=initialize_project,
|
|
184
186
|
status=status_use_case.execute,
|
|
187
|
+
doctor=doctor_use_case.execute,
|
|
185
188
|
context=context_use_case.execute,
|
|
186
189
|
remember=remember_use_case.execute,
|
|
187
190
|
list_facts=facts_list_use_case.execute,
|
|
@@ -16,6 +16,11 @@ from rich.panel import Panel
|
|
|
16
16
|
from rich.table import Table
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
|
|
19
|
+
from universal_memory import __version__
|
|
20
|
+
from universal_memory.application.diagnostics import (
|
|
21
|
+
DoctorCommand,
|
|
22
|
+
DoctorResult,
|
|
23
|
+
)
|
|
19
24
|
from universal_memory.application.host import (
|
|
20
25
|
ConfigureHostCommand,
|
|
21
26
|
ConfigureHostResult,
|
|
@@ -146,6 +151,7 @@ ListSnapshotsCommandHandler = Callable[[ListSnapshotsCommand], ListSnapshotsResu
|
|
|
146
151
|
RollbackCommandHandler = Callable[[RollbackCommand], RollbackResult]
|
|
147
152
|
RollbackPreviewHandler = Callable[[SnapshotScope], Snapshot]
|
|
148
153
|
StatusCommandHandler = Callable[[GetMemoryStatusCommand], GetMemoryStatusResult]
|
|
154
|
+
DoctorCommandHandler = Callable[[DoctorCommand], DoctorResult]
|
|
149
155
|
ContextCommandHandler = Callable[[AssembleContextSummaryCommand], AssembleContextSummaryResult]
|
|
150
156
|
RememberFactCommandHandler = Callable[[RememberFactCommand], RememberFactResult]
|
|
151
157
|
ListFactsCommandHandler = Callable[[ListFactsCommand], ListFactsResult]
|
|
@@ -199,6 +205,7 @@ def main( # noqa: PLR0913
|
|
|
199
205
|
rollback_command: RollbackCommandHandler | None = None,
|
|
200
206
|
rollback_preview_command: RollbackPreviewHandler | None = None,
|
|
201
207
|
status_command: StatusCommandHandler | None = None,
|
|
208
|
+
doctor_command: DoctorCommandHandler | None = None,
|
|
202
209
|
context_command: ContextCommandHandler | None = None,
|
|
203
210
|
remember_command: RememberFactCommandHandler | None = None,
|
|
204
211
|
facts_list_command: ListFactsCommandHandler | None = None,
|
|
@@ -227,6 +234,7 @@ def main( # noqa: PLR0913
|
|
|
227
234
|
rollback_command=rollback_command,
|
|
228
235
|
rollback_preview_command=rollback_preview_command,
|
|
229
236
|
status_command=status_command,
|
|
237
|
+
doctor_command=doctor_command,
|
|
230
238
|
context_command=context_command,
|
|
231
239
|
remember_command=remember_command,
|
|
232
240
|
facts_list_command=facts_list_command,
|
|
@@ -287,6 +295,12 @@ def main( # noqa: PLR0913
|
|
|
287
295
|
return 0
|
|
288
296
|
|
|
289
297
|
|
|
298
|
+
def _version_callback(value: bool) -> None:
|
|
299
|
+
if value:
|
|
300
|
+
typer.echo(f"umem {__version__}")
|
|
301
|
+
raise typer.Exit()
|
|
302
|
+
|
|
303
|
+
|
|
290
304
|
def create_typer_app( # noqa: PLR0913, PLR0915
|
|
291
305
|
*,
|
|
292
306
|
setup_project_command: SetupProjectCommand | None = None,
|
|
@@ -295,6 +309,7 @@ def create_typer_app( # noqa: PLR0913, PLR0915
|
|
|
295
309
|
rollback_command: RollbackCommandHandler | None = None,
|
|
296
310
|
rollback_preview_command: RollbackPreviewHandler | None = None,
|
|
297
311
|
status_command: StatusCommandHandler | None = None,
|
|
312
|
+
doctor_command: DoctorCommandHandler | None = None,
|
|
298
313
|
context_command: ContextCommandHandler | None = None,
|
|
299
314
|
remember_command: RememberFactCommandHandler | None = None,
|
|
300
315
|
facts_list_command: ListFactsCommandHandler | None = None,
|
|
@@ -332,6 +347,15 @@ def create_typer_app( # noqa: PLR0913, PLR0915
|
|
|
332
347
|
@app.callback()
|
|
333
348
|
def callback(
|
|
334
349
|
ctx: typer.Context,
|
|
350
|
+
version: Annotated[
|
|
351
|
+
bool,
|
|
352
|
+
typer.Option(
|
|
353
|
+
"--version",
|
|
354
|
+
help="Show installed version and exit.",
|
|
355
|
+
callback=_version_callback,
|
|
356
|
+
is_eager=True,
|
|
357
|
+
),
|
|
358
|
+
] = False,
|
|
335
359
|
output_format: Annotated[
|
|
336
360
|
str,
|
|
337
361
|
typer.Option(
|
|
@@ -343,6 +367,7 @@ def create_typer_app( # noqa: PLR0913, PLR0915
|
|
|
343
367
|
),
|
|
344
368
|
] = "human",
|
|
345
369
|
) -> None:
|
|
370
|
+
_ = version
|
|
346
371
|
ctx.obj = {"output_format": output_format.lower()}
|
|
347
372
|
|
|
348
373
|
@app.command("init")
|
|
@@ -384,6 +409,15 @@ def create_typer_app( # noqa: PLR0913, PLR0915
|
|
|
384
409
|
code=_run_status(status_command, output_format=_effective_format(ctx, output_format))
|
|
385
410
|
)
|
|
386
411
|
|
|
412
|
+
@app.command("doctor")
|
|
413
|
+
def doctor(ctx: typer.Context, output_format: OutputFormatOption = None) -> None:
|
|
414
|
+
if doctor_command is None:
|
|
415
|
+
msg = "CLI doctor_command dependency was not configured."
|
|
416
|
+
raise RuntimeError(msg)
|
|
417
|
+
raise typer.Exit(
|
|
418
|
+
code=_run_doctor(doctor_command, output_format=_effective_format(ctx, output_format))
|
|
419
|
+
)
|
|
420
|
+
|
|
387
421
|
@app.command("update")
|
|
388
422
|
def update( # noqa: PLR0913
|
|
389
423
|
ctx: typer.Context,
|
|
@@ -950,6 +984,7 @@ def build_main( # noqa: PLR0913
|
|
|
950
984
|
rollback_command: RollbackCommandHandler,
|
|
951
985
|
rollback_preview_command: RollbackPreviewHandler,
|
|
952
986
|
status_command: StatusCommandHandler,
|
|
987
|
+
doctor_command: DoctorCommandHandler,
|
|
953
988
|
context_command: ContextCommandHandler,
|
|
954
989
|
remember_command: RememberFactCommandHandler,
|
|
955
990
|
facts_list_command: ListFactsCommandHandler,
|
|
@@ -985,6 +1020,7 @@ def build_main( # noqa: PLR0913
|
|
|
985
1020
|
rollback_command=rollback_command,
|
|
986
1021
|
rollback_preview_command=rollback_preview_command,
|
|
987
1022
|
status_command=status_command,
|
|
1023
|
+
doctor_command=doctor_command,
|
|
988
1024
|
context_command=context_command,
|
|
989
1025
|
remember_command=remember_command,
|
|
990
1026
|
facts_list_command=facts_list_command,
|
|
@@ -1371,6 +1407,30 @@ def _run_status(command: StatusCommandHandler, *, output_format: str) -> int:
|
|
|
1371
1407
|
return 0
|
|
1372
1408
|
|
|
1373
1409
|
|
|
1410
|
+
def _run_doctor(command: DoctorCommandHandler, *, output_format: str) -> int:
|
|
1411
|
+
try:
|
|
1412
|
+
result = command(DoctorCommand(project_root=Path.cwd()))
|
|
1413
|
+
except OSError as error:
|
|
1414
|
+
_print_expected_error(StorageError(str(error)), output_format=output_format)
|
|
1415
|
+
return 1
|
|
1416
|
+
except DOMAIN_ERROR_TYPES as error:
|
|
1417
|
+
_print_expected_error(error, output_format=output_format)
|
|
1418
|
+
return 1
|
|
1419
|
+
except ValidationError as error:
|
|
1420
|
+
_print_expected_error(ValidationFailedError(str(error)), output_format=output_format)
|
|
1421
|
+
return 1
|
|
1422
|
+
except Exception as error:
|
|
1423
|
+
_print_unexpected_error(error, output_format=output_format)
|
|
1424
|
+
return 1
|
|
1425
|
+
|
|
1426
|
+
if output_format == "json":
|
|
1427
|
+
print(json.dumps(_doctor_success_envelope(result), sort_keys=True))
|
|
1428
|
+
else:
|
|
1429
|
+
_stdout_console().print(_format_human_doctor_output(result))
|
|
1430
|
+
|
|
1431
|
+
return 0 if result.ok else 1
|
|
1432
|
+
|
|
1433
|
+
|
|
1374
1434
|
def _run_context(
|
|
1375
1435
|
command: ContextCommandHandler,
|
|
1376
1436
|
*,
|
|
@@ -2629,6 +2689,16 @@ def _status_success_envelope(result: GetMemoryStatusResult) -> dict[str, Any]:
|
|
|
2629
2689
|
}
|
|
2630
2690
|
|
|
2631
2691
|
|
|
2692
|
+
def _doctor_success_envelope(result: DoctorResult) -> dict[str, Any]:
|
|
2693
|
+
return {
|
|
2694
|
+
"ok": result.ok,
|
|
2695
|
+
"operation": "doctor",
|
|
2696
|
+
"scope": "environment",
|
|
2697
|
+
"data": result.to_payload(),
|
|
2698
|
+
"warnings": [],
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
|
|
2632
2702
|
def _skill_proposal_payload(result: ProposeSkillResult) -> dict[str, Any]:
|
|
2633
2703
|
return {
|
|
2634
2704
|
"skill_id": result.latent_skill.id,
|
|
@@ -2845,12 +2915,14 @@ def _status_payload(result: GetMemoryStatusResult) -> dict[str, Any]:
|
|
|
2845
2915
|
return {
|
|
2846
2916
|
"initialized": False,
|
|
2847
2917
|
"project_path": result.project_path,
|
|
2918
|
+
"installed_version": result.installed_version,
|
|
2848
2919
|
"recommended_action": result.recommended_action,
|
|
2849
2920
|
}
|
|
2850
2921
|
|
|
2851
2922
|
return {
|
|
2852
2923
|
"initialized": True,
|
|
2853
2924
|
"project_path": result.project_path,
|
|
2925
|
+
"installed_version": result.installed_version,
|
|
2854
2926
|
"fact_counts": result.fact_counts,
|
|
2855
2927
|
"active_rules_count": result.active_rules_count,
|
|
2856
2928
|
"registered_skills_count": result.registered_skills_count,
|
|
@@ -2936,6 +3008,7 @@ def _format_human_status_output(result: GetMemoryStatusResult) -> str:
|
|
|
2936
3008
|
[
|
|
2937
3009
|
"Local memory is not initialized.",
|
|
2938
3010
|
f"Project: {result.project_path}",
|
|
3011
|
+
f"Installed version: {result.installed_version}",
|
|
2939
3012
|
f"Next action: {result.recommended_action}",
|
|
2940
3013
|
]
|
|
2941
3014
|
)
|
|
@@ -2943,6 +3016,7 @@ def _format_human_status_output(result: GetMemoryStatusResult) -> str:
|
|
|
2943
3016
|
lines = [
|
|
2944
3017
|
"Local memory initialized.",
|
|
2945
3018
|
f"Project: {result.project_path}",
|
|
3019
|
+
f"Installed version: {result.installed_version}",
|
|
2946
3020
|
f"Approximate size: {result.approximate_size_bytes} bytes",
|
|
2947
3021
|
f"Last health check: {result.last_health_check}",
|
|
2948
3022
|
f"Active rules: {result.active_rules_count}",
|
|
@@ -2967,6 +3041,36 @@ def _format_human_status_output(result: GetMemoryStatusResult) -> str:
|
|
|
2967
3041
|
return "\n".join(lines)
|
|
2968
3042
|
|
|
2969
3043
|
|
|
3044
|
+
def _format_human_doctor_output(result: DoctorResult) -> str:
|
|
3045
|
+
lines = [
|
|
3046
|
+
"universal-memory Doctor - Health Report",
|
|
3047
|
+
"========================================",
|
|
3048
|
+
"",
|
|
3049
|
+
]
|
|
3050
|
+
for check in result.checks:
|
|
3051
|
+
marker = "[OK]" if check.status == "success" else "[FAIL]"
|
|
3052
|
+
label = " ".join(check.name.split("_")).title()
|
|
3053
|
+
detail = f" - {check.detail}" if check.detail else ""
|
|
3054
|
+
lines.append(f"{marker} {label}{detail}")
|
|
3055
|
+
if check.error:
|
|
3056
|
+
lines.append(f" Error: {check.error}")
|
|
3057
|
+
if check.recovery_hint:
|
|
3058
|
+
lines.append(f" Recovery: {check.recovery_hint}")
|
|
3059
|
+
|
|
3060
|
+
summary = result.summary
|
|
3061
|
+
lines.extend(
|
|
3062
|
+
[
|
|
3063
|
+
"",
|
|
3064
|
+
(
|
|
3065
|
+
"Final status: all checks passed."
|
|
3066
|
+
if result.ok
|
|
3067
|
+
else f"Final status: {summary.failed} failure(s) found."
|
|
3068
|
+
),
|
|
3069
|
+
]
|
|
3070
|
+
)
|
|
3071
|
+
return "\n".join(lines)
|
|
3072
|
+
|
|
3073
|
+
|
|
2970
3074
|
def _format_human_context_output(result: AssembleContextSummaryResult) -> str:
|
|
2971
3075
|
summary = result.context_summary
|
|
2972
3076
|
lines = [
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/mcp/server.py
RENAMED
|
@@ -9,6 +9,10 @@ from typing import Any, Literal
|
|
|
9
9
|
|
|
10
10
|
from fastmcp import FastMCP
|
|
11
11
|
|
|
12
|
+
from universal_memory.application.diagnostics import (
|
|
13
|
+
DoctorCommand,
|
|
14
|
+
DoctorResult,
|
|
15
|
+
)
|
|
12
16
|
from universal_memory.application.host import (
|
|
13
17
|
ConfigureHostCommand,
|
|
14
18
|
ConfigureHostResult,
|
|
@@ -86,6 +90,7 @@ DEFAULT_CONTEXT_MAX_SIZE_CHARS = 4000
|
|
|
86
90
|
TOKEN_ESTIMATE_CHARS = 4
|
|
87
91
|
SetupProjectCommandHandler = Callable[[Path], SetupProjectResult]
|
|
88
92
|
StatusCommandHandler = Callable[[GetMemoryStatusCommand], GetMemoryStatusResult]
|
|
93
|
+
DoctorCommandHandler = Callable[[DoctorCommand], DoctorResult]
|
|
89
94
|
ContextCommandHandler = Callable[[AssembleContextSummaryCommand], AssembleContextSummaryResult]
|
|
90
95
|
RememberCommandHandler = Callable[[RememberFactCommand], RememberFactResult]
|
|
91
96
|
ListFactsCommandHandler = Callable[[ListFactsCommand], ListFactsResult]
|
|
@@ -115,6 +120,7 @@ def _missing_use_case(_command: Any) -> Any:
|
|
|
115
120
|
class MCPUseCases:
|
|
116
121
|
status: StatusCommandHandler
|
|
117
122
|
context: ContextCommandHandler
|
|
123
|
+
doctor: DoctorCommandHandler = _missing_use_case
|
|
118
124
|
initialize_project: SetupProjectCommandHandler = _missing_use_case
|
|
119
125
|
remember: RememberCommandHandler = _missing_use_case
|
|
120
126
|
list_facts: ListFactsCommandHandler = _missing_use_case
|
|
@@ -184,6 +190,15 @@ def configure_server( # noqa: PLR0915
|
|
|
184
190
|
except Exception as error:
|
|
185
191
|
return _mcp_tool_error(error, operation="status", scope="project")
|
|
186
192
|
|
|
193
|
+
@server.tool(name="doctor")
|
|
194
|
+
def doctor() -> ToolResponse:
|
|
195
|
+
"""Run read-only environment diagnostics for Universal Memory."""
|
|
196
|
+
try:
|
|
197
|
+
result = use_cases.doctor(DoctorCommand(project_root=root))
|
|
198
|
+
return _doctor_success_envelope(result)
|
|
199
|
+
except Exception as error:
|
|
200
|
+
return _mcp_tool_error(error, operation="doctor", scope="environment")
|
|
201
|
+
|
|
187
202
|
@server.tool(name="context")
|
|
188
203
|
def context(
|
|
189
204
|
scope: Literal["project", "global"] = "project",
|
|
@@ -669,12 +684,14 @@ def _status_payload(result: GetMemoryStatusResult) -> dict[str, Any]:
|
|
|
669
684
|
return {
|
|
670
685
|
"initialized": False,
|
|
671
686
|
"project_path": result.project_path,
|
|
687
|
+
"installed_version": result.installed_version,
|
|
672
688
|
"recommended_action": result.recommended_action,
|
|
673
689
|
}
|
|
674
690
|
|
|
675
691
|
return {
|
|
676
692
|
"initialized": True,
|
|
677
693
|
"project_path": result.project_path,
|
|
694
|
+
"installed_version": result.installed_version,
|
|
678
695
|
"fact_counts": result.fact_counts,
|
|
679
696
|
"active_rules_count": result.active_rules_count,
|
|
680
697
|
"registered_skills_count": result.registered_skills_count,
|
|
@@ -684,6 +701,16 @@ def _status_payload(result: GetMemoryStatusResult) -> dict[str, Any]:
|
|
|
684
701
|
}
|
|
685
702
|
|
|
686
703
|
|
|
704
|
+
def _doctor_success_envelope(result: DoctorResult) -> dict[str, Any]:
|
|
705
|
+
return {
|
|
706
|
+
"ok": result.ok,
|
|
707
|
+
"operation": "doctor",
|
|
708
|
+
"scope": "environment",
|
|
709
|
+
"data": result.to_payload(),
|
|
710
|
+
"warnings": [],
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
687
714
|
def _context_payload(
|
|
688
715
|
result: AssembleContextSummaryResult,
|
|
689
716
|
*,
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/__init__.py
RENAMED
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/application/host/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/bootstrap/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/fact.py
RENAMED
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/host.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/rule.py
RENAMED
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/runtime.py
RENAMED
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/entities/snapshot.py
RENAMED
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/ports/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/domain/project_layout.py
RENAMED
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/infrastructure/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/__init__.py
RENAMED
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/cli/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{universal_memory-0.1.0 → universal_memory-0.1.2}/src/universal_memory/interfaces/mcp/__init__.py
RENAMED
|
File without changes
|